add profile edit
This commit is contained in:
@@ -23,6 +23,7 @@ import 'package:worker/features/quotes/presentation/pages/quotes_page.dart';
|
|||||||
import 'package:worker/features/price_policy/price_policy.dart';
|
import 'package:worker/features/price_policy/price_policy.dart';
|
||||||
import 'package:worker/features/news/presentation/pages/news_list_page.dart';
|
import 'package:worker/features/news/presentation/pages/news_list_page.dart';
|
||||||
import 'package:worker/features/news/presentation/pages/news_detail_page.dart';
|
import 'package:worker/features/news/presentation/pages/news_detail_page.dart';
|
||||||
|
import 'package:worker/features/account/presentation/pages/profile_edit_page.dart';
|
||||||
|
|
||||||
/// App Router
|
/// App Router
|
||||||
///
|
///
|
||||||
@@ -201,6 +202,14 @@ class AppRouter {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Profile Edit Route
|
||||||
|
GoRoute(
|
||||||
|
path: RouteNames.profile,
|
||||||
|
name: RouteNames.profile,
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
MaterialPage(key: state.pageKey, child: const ProfileEditPage()),
|
||||||
|
),
|
||||||
|
|
||||||
// TODO: Add more routes as features are implemented
|
// TODO: Add more routes as features are implemented
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -33,19 +33,15 @@ class AccountPage extends StatelessWidget {
|
|||||||
// Simple Header
|
// Simple Header
|
||||||
_buildHeader(),
|
_buildHeader(),
|
||||||
|
|
||||||
|
|
||||||
// User Profile Card
|
// User Profile Card
|
||||||
_buildProfileCard(context),
|
_buildProfileCard(context),
|
||||||
|
|
||||||
|
|
||||||
// Account Menu Section
|
// Account Menu Section
|
||||||
_buildAccountMenu(context),
|
_buildAccountMenu(context),
|
||||||
|
|
||||||
|
|
||||||
// Support Section
|
// Support Section
|
||||||
_buildSupportSection(context),
|
_buildSupportSection(context),
|
||||||
|
|
||||||
|
|
||||||
// Logout Button
|
// Logout Button
|
||||||
_buildLogoutButton(context),
|
_buildLogoutButton(context),
|
||||||
|
|
||||||
@@ -142,18 +138,12 @@ class AccountPage extends StatelessWidget {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
const Text(
|
const Text(
|
||||||
'Kiến trúc sư · Hạng Diamond',
|
'Kiến trúc sư · Hạng Diamond',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, color: AppColors.grey500),
|
||||||
fontSize: 13,
|
|
||||||
color: AppColors.grey500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
const Text(
|
const Text(
|
||||||
'0983 441 099',
|
'0983 441 099',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, color: AppColors.primaryBlue),
|
||||||
fontSize: 13,
|
|
||||||
color: AppColors.primaryBlue,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -185,7 +175,7 @@ class AccountPage extends StatelessWidget {
|
|||||||
title: 'Thông tin cá nhân',
|
title: 'Thông tin cá nhân',
|
||||||
subtitle: 'Cập nhật thông tin tài khoản',
|
subtitle: 'Cập nhật thông tin tài khoản',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_showComingSoon(context);
|
context.push(RouteNames.profile);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
AccountMenuItem(
|
AccountMenuItem(
|
||||||
@@ -353,20 +343,14 @@ class AccountPage extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'EuroTile & Vasta Stone Worker',
|
'EuroTile & Vasta Stone Worker',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text('Phiên bản: 1.0.0'),
|
const Text('Phiên bản: 1.0.0'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Ứng dụng dành cho thầu thợ, kiến trúc sư, đại lý và môi giới trong ngành gạch ốp lát và nội thất.',
|
'Ứng dụng dành cho thầu thợ, kiến trúc sư, đại lý và môi giới trong ngành gạch ốp lát và nội thất.',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||||
fontSize: 14,
|
|
||||||
color: AppColors.grey500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -402,9 +386,7 @@ class AccountPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(foregroundColor: AppColors.danger),
|
||||||
foregroundColor: AppColors.danger,
|
|
||||||
),
|
|
||||||
child: const Text('Đăng xuất'),
|
child: const Text('Đăng xuất'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
695
lib/features/account/presentation/pages/profile_edit_page.dart
Normal file
695
lib/features/account/presentation/pages/profile_edit_page.dart
Normal file
@@ -0,0 +1,695 @@
|
|||||||
|
/// Profile Edit Page
|
||||||
|
///
|
||||||
|
/// Allows users to edit their profile information.
|
||||||
|
/// Features:
|
||||||
|
/// - Avatar upload with image picker
|
||||||
|
/// - Form fields for personal information
|
||||||
|
/// - Form validation
|
||||||
|
/// - Save/cancel actions
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
|
||||||
|
/// Profile Edit Page
|
||||||
|
///
|
||||||
|
/// Page for editing user profile information with avatar upload.
|
||||||
|
class ProfileEditPage extends HookConsumerWidget {
|
||||||
|
const ProfileEditPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Form key for validation
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
|
||||||
|
// Image picker
|
||||||
|
final selectedImage = useState<File?>(null);
|
||||||
|
|
||||||
|
// Form controllers
|
||||||
|
final nameController = useTextEditingController(text: 'Hoàng Minh Hiệp');
|
||||||
|
final phoneController = useTextEditingController(text: '0347302911');
|
||||||
|
final emailController = useTextEditingController(
|
||||||
|
text: 'hoanghiep@example.com',
|
||||||
|
);
|
||||||
|
final birthDateController = useTextEditingController(text: '15/03/1985');
|
||||||
|
final idNumberController = useTextEditingController(text: '123456789012');
|
||||||
|
final taxIdController = useTextEditingController(text: '0359837618');
|
||||||
|
final companyController = useTextEditingController(
|
||||||
|
text: 'Công ty TNHH Xây dựng ABC',
|
||||||
|
);
|
||||||
|
final addressController = useTextEditingController(
|
||||||
|
text: '123 Man Thiện, Thủ Đức, Hồ Chí Minh',
|
||||||
|
);
|
||||||
|
final experienceController = useTextEditingController(text: '10');
|
||||||
|
|
||||||
|
// Dropdown values
|
||||||
|
final selectedGender = useState<String>('male');
|
||||||
|
final selectedPosition = useState<String>('contractor');
|
||||||
|
|
||||||
|
// Has unsaved changes
|
||||||
|
final hasChanges = useState<bool>(false);
|
||||||
|
|
||||||
|
return PopScope(
|
||||||
|
canPop: !hasChanges.value,
|
||||||
|
onPopInvokedWithResult: (didPop, result) async {
|
||||||
|
if (didPop) return;
|
||||||
|
|
||||||
|
final shouldPop = await _showUnsavedChangesDialog(context);
|
||||||
|
if (shouldPop == true && context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF4F6F8),
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||||
|
onPressed: () async {
|
||||||
|
if (hasChanges.value) {
|
||||||
|
final shouldPop = await _showUnsavedChangesDialog(context);
|
||||||
|
if (shouldPop == true && context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
'Thông tin cá nhân',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: false,
|
||||||
|
actions: const [SizedBox(width: AppSpacing.sm)],
|
||||||
|
),
|
||||||
|
body: Form(
|
||||||
|
key: formKey,
|
||||||
|
onChanged: () {
|
||||||
|
hasChanges.value = true;
|
||||||
|
},
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Profile Avatar Section
|
||||||
|
_buildAvatarSection(context, selectedImage),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Form Card
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Full Name
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Họ và tên',
|
||||||
|
controller: nameController,
|
||||||
|
required: true,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Vui lòng nhập họ và tên';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Phone
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Số điện thoại',
|
||||||
|
controller: phoneController,
|
||||||
|
required: true,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Vui lòng nhập số điện thoại';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Email
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Email',
|
||||||
|
controller: emailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Birth Date
|
||||||
|
_buildDateField(
|
||||||
|
context: context,
|
||||||
|
label: 'Ngày sinh',
|
||||||
|
controller: birthDateController,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Gender
|
||||||
|
_buildDropdownField(
|
||||||
|
label: 'Giới tính',
|
||||||
|
value: selectedGender.value,
|
||||||
|
items: const [
|
||||||
|
{'value': 'male', 'label': 'Nam'},
|
||||||
|
{'value': 'female', 'label': 'Nữ'},
|
||||||
|
{'value': 'other', 'label': 'Khác'},
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
selectedGender.value = value;
|
||||||
|
hasChanges.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// ID Number
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Số CMND/CCCD',
|
||||||
|
controller: idNumberController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Tax ID
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Mã số thuế',
|
||||||
|
controller: taxIdController,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Company
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Công ty',
|
||||||
|
controller: companyController,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Address
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Địa chỉ',
|
||||||
|
controller: addressController,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Position
|
||||||
|
_buildDropdownField(
|
||||||
|
label: 'Chức vụ',
|
||||||
|
value: selectedPosition.value,
|
||||||
|
items: const [
|
||||||
|
{'value': 'contractor', 'label': 'Thầu thợ'},
|
||||||
|
{'value': 'architect', 'label': 'Kiến trúc sư'},
|
||||||
|
{'value': 'dealer', 'label': 'Đại lý phân phối'},
|
||||||
|
{'value': 'broker', 'label': 'Môi giới'},
|
||||||
|
{'value': 'other', 'label': 'Khác'},
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
selectedPosition.value = value;
|
||||||
|
hasChanges.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Experience
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Kinh nghiệm (năm)',
|
||||||
|
controller: experienceController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.lg),
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
_buildActionButtons(
|
||||||
|
context: context,
|
||||||
|
formKey: formKey,
|
||||||
|
hasChanges: hasChanges,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.lg),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build avatar section with edit button
|
||||||
|
Widget _buildAvatarSection(
|
||||||
|
BuildContext context,
|
||||||
|
ValueNotifier<File?> selectedImage,
|
||||||
|
) {
|
||||||
|
return Center(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Avatar
|
||||||
|
Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
image: selectedImage.value != null
|
||||||
|
? DecorationImage(
|
||||||
|
image: FileImage(selectedImage.value!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: selectedImage.value == null
|
||||||
|
? const Center(
|
||||||
|
child: Text(
|
||||||
|
'HMH',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Edit Button
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
await _pickImage(context, selectedImage);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.camera_alt,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build text field
|
||||||
|
Widget _buildTextField({
|
||||||
|
required String label,
|
||||||
|
required TextEditingController controller,
|
||||||
|
bool required = false,
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
int maxLines = 1,
|
||||||
|
String? Function(String?)? validator,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color(0xFF1E293B),
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
if (required)
|
||||||
|
const TextSpan(
|
||||||
|
text: ' *',
|
||||||
|
style: TextStyle(color: AppColors.danger),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
maxLines: maxLines,
|
||||||
|
validator: validator,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Nhập $label',
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: AppColors.grey500.withValues(alpha: 0.6),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: const Color(0xFFF8FAFC),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(color: AppColors.danger),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(color: AppColors.danger, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build date field
|
||||||
|
Widget _buildDateField({
|
||||||
|
required BuildContext context,
|
||||||
|
required String label,
|
||||||
|
required TextEditingController controller,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color(0xFF1E293B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime(1985, 3, 15),
|
||||||
|
firstDate: DateTime(1940),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (date != null) {
|
||||||
|
controller.text =
|
||||||
|
'${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Chọn ngày sinh',
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: AppColors.grey500.withValues(alpha: 0.6),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: const Color(0xFFF8FAFC),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
suffixIcon: const Icon(Icons.calendar_today, size: 20),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build dropdown field
|
||||||
|
Widget _buildDropdownField({
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
required List<Map<String, String>> items,
|
||||||
|
required ValueChanged<String?> onChanged,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color(0xFF1E293B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
initialValue: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
filled: true,
|
||||||
|
fillColor: const Color(0xFFF8FAFC),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: items.map((item) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: item['value'],
|
||||||
|
child: Text(item['label']!, style: const TextStyle(fontSize: 14)),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build action buttons
|
||||||
|
Widget _buildActionButtons({
|
||||||
|
required BuildContext context,
|
||||||
|
required GlobalKey<FormState> formKey,
|
||||||
|
required ValueNotifier<bool> hasChanges,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Cancel Button
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
side: const BorderSide(color: AppColors.grey100),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Hủy bỏ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.grey900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: AppSpacing.sm),
|
||||||
|
|
||||||
|
// Save Button
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
if (formKey.currentState?.validate() ?? false) {
|
||||||
|
// TODO: Save profile data
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Thông tin đã được cập nhật thành công!'),
|
||||||
|
backgroundColor: AppColors.success,
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
hasChanges.value = false;
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.save, size: 20),
|
||||||
|
label: const Text(
|
||||||
|
'Lưu thay đổi',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.primaryBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pick image from gallery or camera
|
||||||
|
Future<void> _pickImage(
|
||||||
|
BuildContext context,
|
||||||
|
ValueNotifier<File?> selectedImage,
|
||||||
|
) async {
|
||||||
|
final ImagePicker picker = ImagePicker();
|
||||||
|
|
||||||
|
// Show dialog to choose source
|
||||||
|
final source = await showDialog<ImageSource>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Chọn ảnh từ'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.camera_alt),
|
||||||
|
title: const Text('Máy ảnh'),
|
||||||
|
onTap: () => Navigator.pop(context, ImageSource.camera),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.photo_library),
|
||||||
|
title: const Text('Thư viện ảnh'),
|
||||||
|
onTap: () => Navigator.pop(context, ImageSource.gallery),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (source != null) {
|
||||||
|
final XFile? image = await picker.pickImage(
|
||||||
|
source: source,
|
||||||
|
maxWidth: 512,
|
||||||
|
maxHeight: 512,
|
||||||
|
imageQuality: 85,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (image != null) {
|
||||||
|
selectedImage.value = File(image.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show unsaved changes dialog
|
||||||
|
Future<bool?> _showUnsavedChangesDialog(BuildContext context) {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Thay đổi chưa được lưu'),
|
||||||
|
content: const Text(
|
||||||
|
'Bạn có thay đổi chưa được lưu. Bạn có muốn thoát không?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('Ở lại'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: AppColors.danger),
|
||||||
|
child: const Text('Thoát'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user