/// 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:convert'; 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/image.dart' as img; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.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 ?? ''); // Format date of birth if available final birthDateText = userInfo.dateOfBirth != null ? '${userInfo.dateOfBirth!.day.toString().padLeft(2, '0')}/${userInfo.dateOfBirth!.month.toString().padLeft(2, '0')}/${userInfo.dateOfBirth!.year}' : ''; final birthDateController = useTextEditingController(text: birthDateText); final taxIdController = useTextEditingController(text: userInfo.taxId ?? ''); final companyController = useTextEditingController(text: userInfo.companyName ?? ''); // Update birthDateController when userInfo changes useEffect(() { if (userInfo.dateOfBirth != null) { final formattedDate = '${userInfo.dateOfBirth!.day.toString().padLeft(2, '0')}/${userInfo.dateOfBirth!.month.toString().padLeft(2, '0')}/${userInfo.dateOfBirth!.year}'; birthDateController.text = formattedDate; } return null; }, [userInfo.dateOfBirth]); // Dropdown values final selectedGender = useState(userInfo.gender ?? 'male'); // Verification images final idCardFrontImage = useState(null); final idCardBackImage = useState(null); final certificateImages = useState>([]); // Tab controller and selected index (dynamic length based on credential_display) final tabLength = userInfo.credentialDisplay ? 2 : 1; final tabController = useTabController(initialLength: tabLength); final selectedTabIndex = useState(0); // Listen to tab changes useEffect(() { void listener() { selectedTabIndex.value = tabController.index; } tabController.addListener(listener); return () => tabController.removeListener(listener); }, [tabController]); 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 with Name and Status _buildAvatarAndStatusSection( context, selectedImage, userInfo.initials, userInfo.avatarUrl, userInfo.fullName, userInfo.role.toString().split('.').last, // Extract role name userInfo.membershipStatus, userInfo.membershipStatusColor, ), const SizedBox(height: AppSpacing.md), // Tab Bar (only show if credential_display is true, otherwise just show info) if (userInfo.credentialDisplay) Container( margin: const EdgeInsets.symmetric(horizontal: 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: TabBar( controller: tabController, indicator: BoxDecoration( color: AppColors.primaryBlue, borderRadius: BorderRadius.circular(AppRadius.card), ), indicatorSize: TabBarIndicatorSize.tab, dividerColor: Colors.transparent, labelColor: Colors.white, unselectedLabelColor: AppColors.grey500, labelStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), tabs: const [ Tab(text: 'Thông tin'), Tab(text: 'Xác thực'), ], ), ), if (userInfo.credentialDisplay) const SizedBox(height: AppSpacing.md), // Tab Content (conditionally rendered based on selected tab) if (!userInfo.credentialDisplay || selectedTabIndex.value == 0) // Tab 1: Personal Information (always show if no tabs, or when selected) _buildPersonalInformationTab( ref: ref, nameController: nameController, phoneController: phoneController, emailController: emailController, birthDateController: birthDateController, taxIdController: taxIdController, companyController: companyController, selectedGender: selectedGender, hasChanges: hasChanges, context: context, formKey: formKey, selectedImage: selectedImage, idCardFrontImage: idCardFrontImage, idCardBackImage: idCardBackImage, certificateImages: certificateImages, ) else // Tab 2: Verification (only if credential_display is true) _buildVerificationTab( ref: ref, context: context, idCardFrontImage: idCardFrontImage, idCardBackImage: idCardBackImage, certificateImages: certificateImages, isVerified: userInfo.isVerified, existingIdCardFrontUrl: userInfo.idCardFront, existingIdCardBackUrl: userInfo.idCardBack, existingCertificateUrls: userInfo.certificates, formKey: formKey, hasChanges: hasChanges, nameController: nameController, birthDateController: birthDateController, selectedGender: selectedGender, companyController: companyController, taxIdController: taxIdController, selectedImage: selectedImage, ), const SizedBox(height: AppSpacing.lg), ], ), ), ), ), ); }, ); } /// Build Personal Information Tab Widget _buildPersonalInformationTab({ required WidgetRef ref, required TextEditingController nameController, required TextEditingController phoneController, required TextEditingController emailController, required TextEditingController birthDateController, required TextEditingController taxIdController, required TextEditingController companyController, required ValueNotifier selectedGender, required ValueNotifier hasChanges, required BuildContext context, required GlobalKey formKey, required ValueNotifier selectedImage, required ValueNotifier idCardFrontImage, required ValueNotifier idCardBackImage, required ValueNotifier> certificateImages, }) { return Column( children: [ // Personal Information Section 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: [ // Section Header const Row( children: [ FaIcon( FontAwesomeIcons.circleUser, color: AppColors.primaryBlue, size: 20, ), SizedBox(width: 12), Text( 'Thông tin cá nhân', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF212121), ), ), ], ), const Divider(height: 32), // 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 (Read-only) _buildTextField( label: 'Số điện thoại', controller: phoneController, readOnly: true, ), const SizedBox(height: AppSpacing.md), // Email (Read-only) _buildTextField( label: 'Email', controller: emailController, readOnly: true, ), const SizedBox(height: AppSpacing.md), // Birth Date _buildDateField( context: context, label: 'Ngày sinh', controller: birthDateController, hasChanges: hasChanges, ), 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), // Company Name _buildTextField( label: 'Tên công ty/Cửa hàng', controller: companyController, ), const SizedBox(height: AppSpacing.md), // Tax ID _buildTextField( label: 'Mã số thuế', controller: taxIdController, ), ], ), ), const SizedBox(height: AppSpacing.md), // Info Note about Read-only Fields Container( margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFEFF6FF), borderRadius: BorderRadius.circular(AppRadius.card), border: Border.all(color: Colors.blue), ), child: Row( children: [ const FaIcon( FontAwesomeIcons.circleInfo, color: AppColors.primaryBlue, size: 16, ), const SizedBox(width: 8), Expanded( child: Text( 'Để thay đổi số điện thoại, email hoặc vai trò, vui lòng liên hệ bộ phận hỗ trợ.', style: TextStyle( fontSize: 12, color: AppColors.primaryBlue.withValues(alpha: 0.9), ), ), ), ], ), ), const SizedBox(height: AppSpacing.lg), // Save Changes Button Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), child: SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => _saveUserInfo( context: context, ref: ref, formKey: formKey, hasChanges: hasChanges, nameController: nameController, birthDateController: birthDateController, selectedGender: selectedGender, companyController: companyController, taxIdController: taxIdController, avatarImage: selectedImage.value, idCardFrontImage: idCardFrontImage.value, idCardBackImage: idCardBackImage.value, certificateImages: certificateImages.value, ), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryBlue, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.button), ), ), child: const Text( 'Lưu thay đổi', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ), const SizedBox(height: AppSpacing.lg), ], ); } /// Build Verification Tab Widget _buildVerificationTab({ required WidgetRef ref, required BuildContext context, required ValueNotifier idCardFrontImage, required ValueNotifier idCardBackImage, required ValueNotifier> certificateImages, required bool isVerified, String? existingIdCardFrontUrl, String? existingIdCardBackUrl, List? existingCertificateUrls, required GlobalKey formKey, required ValueNotifier hasChanges, required TextEditingController nameController, required TextEditingController birthDateController, required ValueNotifier selectedGender, required TextEditingController companyController, required TextEditingController taxIdController, required ValueNotifier selectedImage, }) { return Column( children: [ // Info Note Container( margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isVerified ? const Color(0xFFF0FDF4) // Green for verified : const Color(0xFFEFF6FF), // Blue for not verified borderRadius: BorderRadius.circular(AppRadius.card), border: Border.all( color: isVerified ? const Color(0xFFBBF7D0) : Colors.blue, ), ), child: Row( children: [ FaIcon( isVerified ? FontAwesomeIcons.circleCheck : FontAwesomeIcons.circleInfo, color: isVerified ? AppColors.success : AppColors.primaryBlue, size: 16, ), const SizedBox(width: 8), Expanded( child: Text( isVerified ? 'Tài khoản của bạn đã được xác thực. Các thông tin xác thực không thể chỉnh sửa.' : 'Vui lòng cung cấp ảnh chụp rõ ràng các giấy tờ xác thực để được phê duyệt nhanh chóng.', style: TextStyle( fontSize: 12, color: isVerified ? const Color(0xFF166534) : AppColors.primaryBlue.withValues(alpha: 0.9), fontWeight: FontWeight.w500, ), ), ), ], ), ), const SizedBox(height: AppSpacing.md), // Verification 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.center, children: [ // Section Header const Row( children: [ FaIcon( FontAwesomeIcons.fileCircleCheck, color: AppColors.primaryBlue, size: 20, ), SizedBox(width: 12), Text( 'Thông tin xác thực', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF212121), ), ), ], ), const Divider(height: 32), // ID Card Front Upload const Text( 'Ảnh mặt trước CCCD/CMND', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1E293B), ), ), const SizedBox(height: 8), _buildUploadCard( context: context, icon: FontAwesomeIcons.camera, title: 'Chụp ảnh hoặc chọn file', subtitle: 'JPG, PNG tối đa 5MB', selectedImage: idCardFrontImage, existingImageUrl: existingIdCardFrontUrl, onTap: isVerified ? null // Disable if verified : () => _pickVerificationImage(context, idCardFrontImage), ), const SizedBox(height: AppSpacing.md), // ID Card Back Upload const Text( 'Ảnh mặt sau CCCD/CMND', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1E293B), ), ), const SizedBox(height: 8), _buildUploadCard( context: context, icon: FontAwesomeIcons.camera, title: 'Chụp ảnh hoặc chọn file', subtitle: 'JPG, PNG tối đa 5MB', selectedImage: idCardBackImage, existingImageUrl: existingIdCardBackUrl, onTap: isVerified ? null // Disable if verified : () => _pickVerificationImage(context, idCardBackImage), ), const SizedBox(height: AppSpacing.md), // Certificates Upload (Multiple) const Text( 'Chứng chỉ hành nghề', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1E293B), ), ), const SizedBox(height: 8), _buildMultipleUploadCard( context: context, selectedImages: certificateImages, existingImageUrls: existingCertificateUrls, isVerified: isVerified, ), ], ), ), const SizedBox(height: AppSpacing.lg), // Submit Verification Button (disabled if already verified) if (!isVerified) Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), child: SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => _saveUserInfo( context: context, ref: ref, formKey: formKey, hasChanges: hasChanges, nameController: nameController, birthDateController: birthDateController, selectedGender: selectedGender, companyController: companyController, taxIdController: taxIdController, avatarImage: selectedImage.value, idCardFrontImage: idCardFrontImage.value, idCardBackImage: idCardBackImage.value, certificateImages: certificateImages.value, ), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryBlue, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.button), ), ), child: const Text( 'Gửi xác thực', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ), const SizedBox(height: AppSpacing.lg), ], ); } /// Build upload card for verification files Widget _buildUploadCard({ required BuildContext context, required IconData icon, required String title, required String subtitle, required ValueNotifier selectedImage, String? existingImageUrl, VoidCallback? onTap, }) { final hasLocalImage = selectedImage.value != null; final hasExistingImage = existingImageUrl != null && existingImageUrl.isNotEmpty; final hasAnyImage = hasLocalImage || hasExistingImage; final isDisabled = onTap == null; return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: hasAnyImage ? const Color(0xFFF0FDF4) : isDisabled ? const Color(0xFFF1F5F9) // Gray for disabled : const Color(0xFFF8FAFC), border: Border.all( color: hasAnyImage ? const Color(0xFFBBF7D0) : isDisabled ? const Color(0xFFCBD5E1) : const Color(0xFFE2E8F0), width: 2, style: BorderStyle.solid, ), borderRadius: BorderRadius.circular(AppRadius.card), ), child: hasAnyImage ? Column( children: [ // Image preview ClipRRect( borderRadius: BorderRadius.circular(8), child: hasLocalImage ? Image.file( selectedImage.value!, height: 200, width: double.infinity, fit: BoxFit.cover, ) : Image.network( existingImageUrl!, height: 200, width: double.infinity, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( height: 120, color: AppColors.grey100, child: const Center( child: FaIcon( FontAwesomeIcons.circleExclamation, color: AppColors.grey500, size: 32, ), ), ); }, ), ), const SizedBox(height: 12), // Success indicator Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const FaIcon( FontAwesomeIcons.circleCheck, color: AppColors.success, size: 18, ), const SizedBox(width: 8), Flexible( child: Text( hasLocalImage ? selectedImage.value!.path.split('/').last : 'Đã tải lên', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.success, ), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 4), if (!isDisabled) const Text( 'Nhấn để thay đổi', style: TextStyle( fontSize: 12, color: AppColors.grey500, ), ), ], ) : Column( children: [ FaIcon( icon, color: isDisabled ? AppColors.grey500 : AppColors.grey500, size: 32, ), const SizedBox(height: 8), Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: isDisabled ? AppColors.grey500 : const Color(0xFF1E293B), ), ), const SizedBox(height: 4), Text( subtitle, style: TextStyle( fontSize: 12, color: isDisabled ? AppColors.grey500 : AppColors.grey500, ), ), ], ), ), ); } /// Build multiple upload card for certificates (supports multiple images) Widget _buildMultipleUploadCard({ required BuildContext context, required ValueNotifier> selectedImages, List? existingImageUrls, required bool isVerified, }) { final hasLocalImages = selectedImages.value.isNotEmpty; final hasExistingImages = existingImageUrls != null && existingImageUrls.isNotEmpty; final allImages = []; // Add existing images from API if (hasExistingImages) { for (final url in existingImageUrls) { allImages.add( _buildImagePreview( imageUrl: url, onRemove: isVerified ? null : () { // TODO: Mark for removal in API ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Xóa ảnh hiện có sẽ được cập nhật khi lưu'), duration: Duration(seconds: 2), ), ); }, ), ); } } // Add local images if (hasLocalImages) { for (int i = 0; i < selectedImages.value.length; i++) { final file = selectedImages.value[i]; allImages.add( _buildImagePreview( imageFile: file, onRemove: isVerified ? null : () { final updated = List.from(selectedImages.value); updated.removeAt(i); selectedImages.value = updated; }, ), ); } } return Column( children: [ // Display grid of images if any if (allImages.isNotEmpty) ...[ Wrap( spacing: 8, runSpacing: 8, children: allImages, ), const SizedBox(height: 12), ], // Add button (always show if not verified) if (!isVerified) GestureDetector( onTap: () => _pickMultipleCertificateImages(context, selectedImages), child: Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: const Color(0xFFF8FAFC), border: Border.all( color: const Color(0xFFE2E8F0), width: 2, ), borderRadius: BorderRadius.circular(AppRadius.card), ), child: Column( children: [ const FaIcon( FontAwesomeIcons.folderPlus, color: AppColors.grey500, size: 32, ), const SizedBox(height: 8), Text( allImages.isEmpty ? 'Chọn ảnh chứng chỉ' : 'Thêm ảnh chứng chỉ', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1E293B), ), ), const SizedBox(height: 4), const Text( 'Có thể chọn nhiều ảnh - JPG, PNG tối đa 5MB mỗi ảnh', style: TextStyle( fontSize: 12, color: AppColors.grey500, ), textAlign: TextAlign.center, ), ], ), ), ), // Read-only message if verified if (isVerified && allImages.isEmpty) Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: const Color(0xFFF1F5F9), border: Border.all( color: const Color(0xFFCBD5E1), width: 2, ), borderRadius: BorderRadius.circular(AppRadius.card), ), child: const Column( children: [ FaIcon( FontAwesomeIcons.certificate, color: AppColors.grey500, size: 32, ), SizedBox(height: 8), Text( 'Chưa có chứng chỉ', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.grey500, ), ), ], ), ), ], ); } /// Build image preview with remove button Widget _buildImagePreview({ File? imageFile, String? imageUrl, VoidCallback? onRemove, }) { return Container( width: 100, height: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all( color: const Color(0xFFBBF7D0), width: 2, ), ), child: Stack( children: [ // Image ClipRRect( borderRadius: BorderRadius.circular(6), child: imageFile != null ? Image.file( imageFile, width: 100, height: 100, fit: BoxFit.cover, ) : Image.network( imageUrl!, width: 100, height: 100, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( color: AppColors.grey100, child: const Center( child: FaIcon( FontAwesomeIcons.circleExclamation, color: AppColors.grey500, size: 24, ), ), ); }, ), ), // Remove button if (onRemove != null) Positioned( top: 4, right: 4, child: GestureDetector( onTap: onRemove, child: Container( width: 24, height: 24, decoration: const BoxDecoration( color: AppColors.danger, shape: BoxShape.circle, ), child: const Center( child: FaIcon( FontAwesomeIcons.xmark, size: 12, color: Colors.white, ), ), ), ), ), ], ), ); } /// Build avatar section with name, position, and status Widget _buildAvatarAndStatusSection( BuildContext context, ValueNotifier selectedImage, String initials, String? avatarUrl, String fullName, String position, String? membershipStatus, String? membershipStatusColor, ) { // Map position to Vietnamese labels final positionLabels = { 'contractor': 'Thầu thợ', 'architect': 'Kiến trúc sư', 'distributor': 'Đại lý phân phối', 'broker': 'Môi giới', }; // Map status color to badge colors final statusColors = { 'Success': { 'bg': const Color(0xFFF0FDF4), // Green background 'border': const Color(0xFFBBF7D0), // Green border 'icon': const Color(0xFF16A34A), // Green icon 'text': const Color(0xFF166534), // Green text }, 'Warning': { 'bg': const Color(0xFFFEF3C7), // Yellow background 'border': const Color(0xFFFDE68A), // Yellow border 'icon': const Color(0xFFEAB308), // Yellow icon 'text': const Color(0xFF854D0E), // Yellow text }, 'Danger': { 'bg': const Color(0xFFFEE2E2), // Red background 'border': const Color(0xFFFECACA), // Red border 'icon': const Color(0xFFB91C1C), // Red icon 'text': const Color(0xFFB91C1C), // Red text }, }; final colors = statusColors[membershipStatusColor] ?? statusColors['Danger']!; return Container( margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.lg), 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( children: [ // Avatar with camera button Stack( clipBehavior: Clip.none, children: [ // Avatar Container( width: 96, height: 96, decoration: BoxDecoration( shape: BoxShape.circle, color: AppColors.primaryBlue, border: Border.all(color: Colors.white, width: 4), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2), ), ], 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, ), // Camera button Positioned( bottom: -2, right: -2, 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: 3), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.15), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: const Center( child: FaIcon( FontAwesomeIcons.camera, size: 14, color: Colors.white, ), ), ), ), ), ], ), const SizedBox(height: 16), // Name Text( fullName, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF212121), ), ), const SizedBox(height: 4), // Position Text( positionLabels[position] ?? position, style: const TextStyle( fontSize: 14, color: AppColors.grey500, ), ), const SizedBox(height: 16), // Account Status Badge (from API) if (membershipStatus != null) Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: colors['bg'] as Color, borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ FaIcon( membershipStatusColor == 'Success' ? FontAwesomeIcons.circleCheck : membershipStatusColor == 'Warning' ? FontAwesomeIcons.clock : FontAwesomeIcons.circleExclamation, color: colors['icon'] as Color, size: 12, ), const SizedBox(width: 6), Text( membershipStatus, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: colors['text'] as Color, ), ), ], ), ), ], ), ); } /// Build text field Widget _buildTextField({ required String label, required TextEditingController controller, bool required = false, bool readOnly = 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, readOnly: readOnly, validator: validator, decoration: InputDecoration( hintText: 'Nhập $label', hintStyle: TextStyle( color: AppColors.grey500.withValues(alpha: 0.6), fontSize: 14, ), filled: true, fillColor: readOnly ? const Color(0xFFF1F5F9) : 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, ValueNotifier? hasChanges, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1E293B), ), ), const SizedBox(height: 8), TextField( controller: controller, readOnly: true, style: const TextStyle( fontSize: 14, color: Color(0xFF1E293B), fontWeight: FontWeight.w400, ), onTap: () async { final date = await showDatePicker( context: context, initialDate: DateTime.now(), 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}'; // Mark as changed if (hasChanges != null) { hasChanges.value = true; } } }, 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: 16, ), suffixIcon: const Icon( Icons.calendar_today, size: 20, color: AppColors.grey500, ), 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(), ), ], ); } /// Pick verification image from gallery or camera Future _pickVerificationImage( 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: 1024, maxHeight: 1024, imageQuality: 85, ); if (image != null) { selectedImage.value = File(image.path); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Đã chọn ảnh thành công'), backgroundColor: AppColors.success, duration: Duration(seconds: 2), ), ); } } } } /// Pick multiple certificate images from gallery Future _pickMultipleCertificateImages( BuildContext context, ValueNotifier> selectedImages, ) async { final ImagePicker picker = ImagePicker(); try { // Pick multiple images from gallery final List images = await picker.pickMultiImage( maxWidth: 1024, maxHeight: 1024, imageQuality: 85, ); if (images.isNotEmpty) { // Convert XFile to File, fix orientation, and add to existing list final List fixedFiles = []; for (final xfile in images) { final originalFile = File(xfile.path); final fixedFile = await _fixImageOrientation(originalFile); fixedFiles.add(fixedFile); } final updated = List.from(selectedImages.value); updated.addAll(fixedFiles); selectedImages.value = updated; if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Đã chọn ${images.length} ảnh thành công'), backgroundColor: AppColors.success, duration: const Duration(seconds: 2), ), ); } } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Không thể chọn ảnh. Vui lòng thử lại.'), backgroundColor: AppColors.danger, duration: Duration(seconds: 2), ), ); } } } /// Pick image from gallery or camera /// Fix image orientation based on EXIF data Future _fixImageOrientation(File imageFile) async { try { // Read the image bytes final bytes = await imageFile.readAsBytes(); // Decode the image (this automatically applies EXIF orientation) final image = img.decodeImage(bytes); if (image == null) { return imageFile; } // Encode back to JPEG final fixedBytes = img.encodeJpg(image, quality: 85); // Create a temporary file to save the fixed image final tempDir = await getTemporaryDirectory(); final timestamp = DateTime.now().millisecondsSinceEpoch; final tempFile = File('${tempDir.path}/fixed_image_$timestamp.jpg'); await tempFile.writeAsBytes(fixedBytes); return tempFile; } catch (e) { // If orientation fix fails, return original file debugPrint('Error fixing image orientation: $e'); return imageFile; } } 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: 1024, maxHeight: 1024, imageQuality: 85, ); if (image != null) { // Fix orientation before setting the image final originalFile = File(image.path); final fixedFile = await _fixImageOrientation(originalFile); selectedImage.value = fixedFile; } } } /// Convert File to base64 string Future _fileToBase64(File? file) async { if (file == null) return null; try { final bytes = await file.readAsBytes(); return base64Encode(bytes); } catch (e) { return null; } } /// Unified save function for both personal info and verification Future _saveUserInfo({ required BuildContext context, required WidgetRef ref, required GlobalKey formKey, required ValueNotifier hasChanges, required TextEditingController nameController, required TextEditingController birthDateController, required ValueNotifier selectedGender, required TextEditingController companyController, required TextEditingController taxIdController, File? avatarImage, File? idCardFrontImage, File? idCardBackImage, List? certificateImages, }) async { // Validate form if (!(formKey.currentState?.validate() ?? false)) { return; } try { // Convert images to base64 final avatarBase64 = await _fileToBase64(avatarImage); final idCardFrontBase64 = await _fileToBase64(idCardFrontImage); final idCardBackBase64 = await _fileToBase64(idCardBackImage); // Convert certificate images to base64 list final List certificatesBase64 = []; if (certificateImages != null && certificateImages.isNotEmpty) { for (final file in certificateImages) { final base64 = await _fileToBase64(file); if (base64 != null) { certificatesBase64.add(base64); } } } // Prepare update data final updateData = { 'full_name': nameController.text, 'date_of_birth': birthDateController.text.isNotEmpty ? _formatDateForApi(birthDateController.text) : null, 'gender': selectedGender.value, 'company_name': companyController.text.isNotEmpty ? companyController.text : null, 'tax_code': taxIdController.text.isNotEmpty ? taxIdController.text : null, 'avatar_base64': avatarBase64, 'id_card_front_base64': idCardFrontBase64, 'id_card_back_base64': idCardBackBase64, 'certificates_base64': certificatesBase64, }; // Call API to update user info await ref.read(userInfoProvider.notifier).updateUserInfo(updateData); if (context.mounted) { // Mark as saved hasChanges.value = false; 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), ), ); // context.pop(); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Lỗi khi cập nhật thông tin: $e'), backgroundColor: AppColors.danger, duration: const Duration(seconds: 3), ), ); } } } /// Format date from DD/MM/YYYY to YYYY-MM-DD for API String? _formatDateForApi(String dateText) { if (dateText.isEmpty) return null; try { final parts = dateText.split('/'); if (parts.length == 3) { return '${parts[2]}-${parts[1]}-${parts[0]}'; // YYYY-MM-DD } return null; } catch (e) { return null; } } /// 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'), ), ], ), ); } }