diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 2805f84..c9aaebe 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -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/news/presentation/pages/news_list_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 /// @@ -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 ], diff --git a/lib/features/account/presentation/pages/account_page.dart b/lib/features/account/presentation/pages/account_page.dart index acd39e8..78a6115 100644 --- a/lib/features/account/presentation/pages/account_page.dart +++ b/lib/features/account/presentation/pages/account_page.dart @@ -33,19 +33,15 @@ class AccountPage extends StatelessWidget { // Simple Header _buildHeader(), - // User Profile Card _buildProfileCard(context), - // Account Menu Section _buildAccountMenu(context), - // Support Section _buildSupportSection(context), - // Logout Button _buildLogoutButton(context), @@ -142,18 +138,12 @@ class AccountPage extends StatelessWidget { const SizedBox(height: 4), const Text( 'Kiến trúc sư · Hạng Diamond', - style: TextStyle( - fontSize: 13, - color: AppColors.grey500, - ), + style: TextStyle(fontSize: 13, color: AppColors.grey500), ), const SizedBox(height: 4), const Text( '0983 441 099', - style: TextStyle( - fontSize: 13, - color: AppColors.primaryBlue, - ), + style: TextStyle(fontSize: 13, color: AppColors.primaryBlue), ), ], ), @@ -185,7 +175,7 @@ class AccountPage extends StatelessWidget { title: 'Thông tin cá nhân', subtitle: 'Cập nhật thông tin tài khoản', onTap: () { - _showComingSoon(context); + context.push(RouteNames.profile); }, ), AccountMenuItem( @@ -353,20 +343,14 @@ class AccountPage extends StatelessWidget { children: [ const Text( 'EuroTile & Vasta Stone Worker', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), const Text('Phiên bản: 1.0.0'), const SizedBox(height: 8), 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.', - style: TextStyle( - fontSize: 14, - color: AppColors.grey500, - ), + style: TextStyle(fontSize: 14, color: AppColors.grey500), ), ], ), @@ -402,9 +386,7 @@ class AccountPage extends StatelessWidget { ), ); }, - style: TextButton.styleFrom( - foregroundColor: AppColors.danger, - ), + style: TextButton.styleFrom(foregroundColor: AppColors.danger), child: const Text('Đăng xuất'), ), ], diff --git a/lib/features/account/presentation/pages/profile_edit_page.dart b/lib/features/account/presentation/pages/profile_edit_page.dart new file mode 100644 index 0000000..0669190 --- /dev/null +++ b/lib/features/account/presentation/pages/profile_edit_page.dart @@ -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()); + + // Image picker + final selectedImage = useState(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('male'); + final selectedPosition = useState('contractor'); + + // Has unsaved changes + final hasChanges = useState(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 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> items, + required ValueChanged 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( + 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( + value: item['value'], + child: Text(item['label']!, style: const TextStyle(fontSize: 14)), + ); + }).toList(), + ), + ], + ); + } + + /// Build action buttons + Widget _buildActionButtons({ + required BuildContext context, + required GlobalKey formKey, + required ValueNotifier 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 _pickImage( + BuildContext context, + ValueNotifier selectedImage, + ) async { + final ImagePicker picker = ImagePicker(); + + // Show dialog to choose source + final source = await showDialog( + 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 _showUnsavedChangesDialog(BuildContext context) { + return showDialog( + 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'), + ), + ], + ), + ); + } +}