/// 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:font_awesome_flutter/font_awesome_flutter.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'; import 'package:worker/features/account/presentation/providers/user_info_provider.dart' hide UserInfo; /// 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) { // Watch user info from API final userInfoAsync = ref.watch(userInfoProvider); // Form key for validation final formKey = useMemoized(() => GlobalKey()); // Image picker final selectedImage = useState(null); // Has unsaved changes final hasChanges = useState(false); return userInfoAsync.when( loading: () => Scaffold( backgroundColor: const Color(0xFFF4F6F8), appBar: AppBar( backgroundColor: Colors.white, elevation: 0, title: const Text( 'Thông tin cá nhân', style: TextStyle( color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold, ), ), centerTitle: false, ), body: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: AppColors.primaryBlue), SizedBox(height: AppSpacing.md), Text( 'Đang tải thông tin...', style: TextStyle( fontSize: 14, color: AppColors.grey500, ), ), ], ), ), ), error: (error, stack) => Scaffold( backgroundColor: const Color(0xFFF4F6F8), appBar: AppBar( backgroundColor: Colors.white, elevation: 0, leading: IconButton( icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), onPressed: () => context.pop(), ), title: const Text( 'Thông tin cá nhân', style: TextStyle( color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold, ), ), centerTitle: false, ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const FaIcon( FontAwesomeIcons.circleExclamation, size: 64, color: AppColors.danger, ), const SizedBox(height: AppSpacing.lg), const Text( 'Không thể tải thông tin người dùng', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), ), const SizedBox(height: AppSpacing.md), ElevatedButton.icon( onPressed: () => ref.read(userInfoProvider.notifier).refresh(), icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16), label: const Text('Thử lại'), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryBlue, foregroundColor: AppColors.white, ), ), ], ), ), ), data: (userInfo) { // Form controllers populated with user data final nameController = useTextEditingController(text: userInfo.fullName); final phoneController = useTextEditingController(text: userInfo.phoneNumber ?? ''); final emailController = useTextEditingController(text: userInfo.email ?? ''); final birthDateController = useTextEditingController(text: ''); // TODO: Add birthdate to API final idNumberController = useTextEditingController(text: userInfo.cccd ?? ''); final taxIdController = useTextEditingController(text: userInfo.taxId ?? ''); final companyController = useTextEditingController(text: userInfo.companyName ?? ''); final addressController = useTextEditingController(text: userInfo.address ?? ''); final experienceController = useTextEditingController(text: ''); // TODO: Add experience to API // Dropdown values final selectedGender = useState('male'); // TODO: Add gender to API final selectedPosition = useState('contractor'); // TODO: Map from userInfo.role 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 FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), 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, userInfo.initials, userInfo.avatarUrl, ), 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, String initials, String? avatarUrl, ) { return Padding( padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), child: Center( child: Stack( clipBehavior: Clip.none, 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, ) : avatarUrl != null ? DecorationImage( image: NetworkImage(avatarUrl), fit: BoxFit.cover, ) : null, ), child: selectedImage.value == null && avatarUrl == null ? Center( child: Text( initials, style: const 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: 36, height: 36, decoration: BoxDecoration( color: AppColors.primaryBlue, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 3), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.15), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: const Center( child: FaIcon( FontAwesomeIcons.camera, 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 Padding( padding: EdgeInsets.only(right: 12), child: FaIcon( FontAwesomeIcons.calendar, size: 20, color: AppColors.grey500, ), ), suffixIconConstraints: const BoxConstraints( minWidth: 48, minHeight: 48, ), 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, icon: const Padding( padding: EdgeInsets.only(right: 12), child: FaIcon( FontAwesomeIcons.chevronDown, size: 16, color: AppColors.grey500, ), ), 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 FaIcon(FontAwesomeIcons.floppyDisk, size: 18), 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 FaIcon(FontAwesomeIcons.camera, size: 18), title: const Text('Máy ảnh'), onTap: () => Navigator.pop(context, ImageSource.camera), ), ListTile( leading: const FaIcon(FontAwesomeIcons.images, size: 18), 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'), ), ], ), ); } }