/// Registration Page /// /// User registration form with role-based verification requirements. /// Matches design from html/register.html library; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/utils/validators.dart'; import 'package:worker/features/auth/domain/entities/business_unit.dart'; import 'package:worker/features/auth/domain/entities/city.dart'; import 'package:worker/features/auth/domain/entities/customer_group.dart'; import 'package:worker/features/auth/presentation/providers/cities_provider.dart'; import 'package:worker/features/auth/presentation/providers/customer_groups_provider.dart'; import 'package:worker/features/auth/presentation/providers/session_provider.dart'; import 'package:worker/features/auth/presentation/widgets/phone_input_field.dart'; import 'package:worker/features/auth/presentation/widgets/file_upload_card.dart'; /// Registration Page /// /// Features: /// - Full name, phone, email, password fields /// - Role selection (dealer/worker/broker/other) /// - Conditional verification section for workers/dealers /// - File upload for ID card and certificate /// - Company name and city selection /// - Terms and conditions checkbox /// /// Navigation: /// - From: Business unit selection page /// - To: OTP verification (broker/other) or pending approval (worker/dealer) class RegisterPage extends ConsumerStatefulWidget { /// Selected business unit from previous screen final BusinessUnit? selectedBusinessUnit; const RegisterPage({super.key, this.selectedBusinessUnit}); @override ConsumerState createState() => _RegisterPageState(); } class _RegisterPageState extends ConsumerState { // Form key final _formKey = GlobalKey(); // Text controllers final _fullNameController = TextEditingController(); final _phoneController = TextEditingController(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _idNumberController = TextEditingController(); final _taxCodeController = TextEditingController(); final _companyController = TextEditingController(); // Focus nodes final _fullNameFocus = FocusNode(); final _phoneFocus = FocusNode(); final _emailFocus = FocusNode(); final _passwordFocus = FocusNode(); final _idNumberFocus = FocusNode(); final _taxCodeFocus = FocusNode(); final _companyFocus = FocusNode(); // State CustomerGroup? _selectedRole; City? _selectedCity; File? _idCardFile; File? _certificateFile; bool _termsAccepted = false; bool _passwordVisible = false; bool _isLoading = false; bool _isLoadingData = true; bool _hasInitialized = false; final _imagePicker = ImagePicker(); /// Initialize session and load data /// This should be called from build method or after widget is mounted Future _initializeData() async { if (!mounted) return; setState(() { _isLoadingData = true; _hasInitialized = true; }); try { // Step 1: Get session (public API user) await ref.read(sessionProvider.notifier).getSession(); // Step 2: Fetch cities and customer groups in parallel using the session await Future.wait([ ref.read(citiesProvider.notifier).fetchCities(), ref.read(customerGroupsProvider.notifier).fetchCustomerGroups(), ]); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Lỗi tải dữ liệu: $e'), backgroundColor: AppColors.danger, action: SnackBarAction( label: 'Thử lại', textColor: AppColors.white, onPressed: _initializeData, ), ), ); } } finally { if (mounted) { setState(() { _isLoadingData = false; }); } } } @override void dispose() { _fullNameController.dispose(); _phoneController.dispose(); _emailController.dispose(); _passwordController.dispose(); _idNumberController.dispose(); _taxCodeController.dispose(); _companyController.dispose(); _fullNameFocus.dispose(); _phoneFocus.dispose(); _emailFocus.dispose(); _passwordFocus.dispose(); _idNumberFocus.dispose(); _taxCodeFocus.dispose(); _companyFocus.dispose(); super.dispose(); } /// Check if verification section should be shown /// Note: This is based on the old role system /// TODO: Update this logic based on actual customer group requirements bool get _shouldShowVerification { // For now, always hide verification section since we're using customer groups return false; } /// 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) { debugPrint('Error converting file to base64: $e'); return null; } } /// Pick image from gallery or camera Future _pickImage(bool isIdCard) async { try { // Show bottom sheet to select source final source = await showModalBottomSheet( context: context, builder: (context) => SafeArea( child: Wrap( children: [ ListTile( leading: const FaIcon(FontAwesomeIcons.camera, size: 20), title: const Text('Chụp ảnh'), onTap: () => Navigator.pop(context, ImageSource.camera), ), ListTile( leading: const FaIcon(FontAwesomeIcons.images, size: 20), title: const Text('Chọn từ thư viện'), onTap: () => Navigator.pop(context, ImageSource.gallery), ), ], ), ), ); if (source == null) return; final pickedFile = await _imagePicker.pickImage( source: source, maxWidth: 1920, maxHeight: 1080, imageQuality: 85, ); if (pickedFile == null) return; final file = File(pickedFile.path); // Validate file size (max 5MB) final fileSize = await file.length(); if (fileSize > 5 * 1024 * 1024) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('File không được vượt quá 5MB'), backgroundColor: AppColors.danger, ), ); } return; } setState(() { if (isIdCard) { _idCardFile = file; } else { _certificateFile = file; } }); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Lỗi chọn ảnh: $e'), backgroundColor: AppColors.danger, ), ); } } } /// Remove selected image void _removeImage(bool isIdCard) { setState(() { if (isIdCard) { _idCardFile = null; } else { _certificateFile = null; } }); } /// Validate form and submit Future _handleRegister() async { // Validate form if (!_formKey.currentState!.validate()) { return; } // Check terms acceptance if (!_termsAccepted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Vui lòng đồng ý với Điều khoản sử dụng và Chính sách bảo mật', ), backgroundColor: AppColors.warning, ), ); return; } // Validate verification requirements for workers/dealers if (_shouldShowVerification) { if (_idNumberController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Vui lòng nhập số CCCD/CMND'), backgroundColor: AppColors.warning, ), ); return; } if (_idCardFile == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Vui lòng tải lên ảnh CCCD/CMND'), backgroundColor: AppColors.warning, ), ); return; } if (_certificateFile == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Vui lòng tải lên ảnh chứng chỉ hành nghề hoặc GPKD'), backgroundColor: AppColors.warning, ), ); return; } } setState(() { _isLoading = true; }); try { // Get session state for CSRF token and SID final sessionState = ref.read(sessionProvider); if (!sessionState.hasSession) { throw Exception('Session không hợp lệ. Vui lòng thử lại.'); } // Convert files to base64 final idCardFrontBase64 = await _fileToBase64(_idCardFile); final certificatesBase64 = _certificateFile != null ? [await _fileToBase64(_certificateFile)] : []; // Remove null values from certificates list final validCertificates = certificatesBase64 .whereType() .toList(); // Call registration API final dataSource = ref.read(authRemoteDataSourceProvider); final response = await dataSource.register( csrfToken: sessionState.csrfToken!, sid: sessionState.sid!, fullName: _fullNameController.text.trim(), phone: _phoneController.text.trim(), email: _emailController.text.trim(), customerGroupCode: _selectedRole?.value ?? _selectedRole?.name ?? '', cityCode: _selectedCity?.code ?? '', companyName: _companyController.text.trim().isEmpty ? null : _companyController.text.trim(), taxCode: _taxCodeController.text.trim().isEmpty ? null : _taxCodeController.text.trim(), idCardFrontBase64: idCardFrontBase64, idCardBackBase64: null, // Not collecting back side in current UI certificatesBase64: validCertificates, ); if (mounted) { // Show success message ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Đăng ký thành công!', // response['message']?.toString() ?? 'Đăng ký thành công!', ), duration: Duration(seconds: 1), backgroundColor: AppColors.success, ), ); Future.delayed(const Duration(seconds: 1)).then((_) => context.goLogin()); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Đăng ký thất bại: $e'), backgroundColor: AppColors.danger, ), ); } } finally { if (mounted) { setState(() { _isLoading = false; }); } } } @override Widget build(BuildContext context) { // Initialize data on first build if (!_hasInitialized) { // Use addPostFrameCallback to avoid calling setState during build WidgetsBinding.instance.addPostFrameCallback((_) { _initializeData(); }); } return Scaffold( backgroundColor: const Color(0xFFF4F6F8), appBar: AppBar( backgroundColor: AppColors.white, elevation: AppBarSpecs.elevation, leading: IconButton( icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), onPressed: () => context.pop(), ), title: const Text( 'Đăng ký tài khoản', style: TextStyle( color: Colors.black, fontSize: 18, fontWeight: FontWeight.w600, ), ), centerTitle: false, ), body: _isLoadingData ? const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: AppSpacing.md), Text( 'Đang tải dữ liệu...', style: TextStyle(color: AppColors.grey500), ), ], ), ) : SafeArea( child: Form( key: _formKey, child: SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.md), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Welcome section const Text( 'Tạo tài khoản mới', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: AppColors.grey900, ), textAlign: TextAlign.center, ), const SizedBox(height: AppSpacing.xs), const Text( 'Điền thông tin để bắt đầu', style: TextStyle(fontSize: 14, color: AppColors.grey500), textAlign: TextAlign.center, ), const SizedBox(height: AppSpacing.lg), // Form card Container( decoration: BoxDecoration( color: AppColors.white, borderRadius: BorderRadius.circular(AppRadius.card), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), padding: const EdgeInsets.all(AppSpacing.md), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Full Name _buildLabel('Họ và tên *'), TextFormField( controller: _fullNameController, focusNode: _fullNameFocus, textInputAction: TextInputAction.next, decoration: _buildInputDecoration( hintText: 'Nhập họ và tên', prefixIcon: FontAwesomeIcons.user, ), validator: (value) => Validators.minLength( value, 3, fieldName: 'Họ và tên', ), ), const SizedBox(height: AppSpacing.md), // Phone Number _buildLabel('Số điện thoại *'), PhoneInputField( controller: _phoneController, focusNode: _phoneFocus, validator: Validators.phone, ), const SizedBox(height: AppSpacing.md), // Email _buildLabel('Email *'), TextFormField( controller: _emailController, focusNode: _emailFocus, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, decoration: _buildInputDecoration( hintText: 'Nhập email', prefixIcon: FontAwesomeIcons.envelope, ), validator: Validators.email, ), const SizedBox(height: AppSpacing.md), // Password _buildLabel('Mật khẩu *'), TextFormField( controller: _passwordController, focusNode: _passwordFocus, obscureText: !_passwordVisible, textInputAction: TextInputAction.done, decoration: _buildInputDecoration( hintText: 'Tạo mật khẩu mới', prefixIcon: FontAwesomeIcons.lock, suffixIcon: IconButton( icon: Icon( _passwordVisible ? FontAwesomeIcons.eye : FontAwesomeIcons.eyeSlash, color: AppColors.grey500, ), onPressed: () { setState(() { _passwordVisible = !_passwordVisible; }); }, ), ), validator: (value) => Validators.passwordSimple(value, minLength: 6), ), const SizedBox(height: AppSpacing.xs), const Text( 'Mật khẩu tối thiểu 6 ký tự', style: TextStyle( fontSize: 12, color: AppColors.grey500, ), ), const SizedBox(height: AppSpacing.md), // Role Selection (Customer Groups) _buildLabel('Vai trò *'), _buildCustomerGroupDropdown(), const SizedBox(height: AppSpacing.md), // Verification Section (conditional) if (_shouldShowVerification) ...[ _buildVerificationSection(), const SizedBox(height: AppSpacing.md), ], // Company Name (optional) _buildLabel('Tên công ty/Cửa hàng'), TextFormField( controller: _companyController, focusNode: _companyFocus, textInputAction: TextInputAction.next, decoration: _buildInputDecoration( hintText: 'Nhập tên công ty (không bắt buộc)', prefixIcon: FontAwesomeIcons.building, ), ), const SizedBox(height: AppSpacing.md), // City/Province _buildLabel('Tỉnh/Thành phố *'), _buildCityDropdown(), const SizedBox(height: AppSpacing.md), // Terms and Conditions Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Checkbox( value: _termsAccepted, onChanged: (value) { setState(() { _termsAccepted = value ?? false; }); }, activeColor: AppColors.primaryBlue, ), Expanded( child: Padding( padding: const EdgeInsets.only(top: 12.0), child: GestureDetector( onTap: () { setState(() { _termsAccepted = !_termsAccepted; }); }, child: const Text.rich( TextSpan( text: 'Tôi đồng ý với ', style: TextStyle(fontSize: 13), children: [ TextSpan( text: 'Điều khoản sử dụng', style: TextStyle( color: AppColors.primaryBlue, fontWeight: FontWeight.w500, ), ), TextSpan(text: ' và '), TextSpan( text: 'Chính sách bảo mật', style: TextStyle( color: AppColors.primaryBlue, fontWeight: FontWeight.w500, ), ), ], ), ), ), ), ), ], ), const SizedBox(height: AppSpacing.lg), // Register Button SizedBox( height: ButtonSpecs.height, child: ElevatedButton( onPressed: _isLoading ? null : _handleRegister, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryBlue, foregroundColor: AppColors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( ButtonSpecs.borderRadius, ), ), ), child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( AppColors.white, ), ), ) : const Text( 'Đăng ký', style: TextStyle( fontSize: ButtonSpecs.fontSize, fontWeight: ButtonSpecs.fontWeight, ), ), ), ), ], ), ), const SizedBox(height: AppSpacing.lg), // Login Link Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Đã có tài khoản? ', style: TextStyle(fontSize: 13, color: AppColors.grey500), ), GestureDetector( onTap: () => context.pop(), child: const Text( 'Đăng nhập', style: TextStyle( fontSize: 13, color: AppColors.primaryBlue, fontWeight: FontWeight.w500, ), ), ), ], ), const SizedBox(height: AppSpacing.lg), ], ), ), ), ), ); } /// Build label widget Widget _buildLabel(String text) { return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.xs), child: Text( text, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.grey900, ), ), ); } /// Build input decoration InputDecoration _buildInputDecoration({ required String hintText, required IconData prefixIcon, Widget? suffixIcon, }) { return InputDecoration( hintText: hintText, hintStyle: const TextStyle( fontSize: InputFieldSpecs.hintFontSize, color: AppColors.grey500, ), prefixIcon: Icon( prefixIcon, color: AppColors.primaryBlue, size: AppIconSize.md, ), suffixIcon: suffixIcon, filled: true, fillColor: AppColors.white, contentPadding: InputFieldSpecs.contentPadding, border: OutlineInputBorder( borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), borderSide: const BorderSide(color: AppColors.grey100, width: 1.0), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), borderSide: const BorderSide(color: AppColors.grey100, width: 1.0), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2.0), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), borderSide: const BorderSide(color: AppColors.danger, width: 1.0), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), borderSide: const BorderSide(color: AppColors.danger, width: 2.0), ), ); } /// Build customer group dropdown Widget _buildCustomerGroupDropdown() { final customerGroupsAsync = ref.watch(customerGroupsProvider); return customerGroupsAsync.when( data: (groups) { return DropdownButtonFormField( initialValue: _selectedRole, decoration: _buildInputDecoration( hintText: 'Chọn vai trò', prefixIcon: FontAwesomeIcons.briefcase, ), items: groups .map( (group) => DropdownMenuItem( value: group, child: Text(group.customerGroupName), ), ) .toList(), onChanged: (value) { setState(() { _selectedRole = value; // Clear verification fields when role changes if (!_shouldShowVerification) { _idNumberController.clear(); _taxCodeController.clear(); _idCardFile = null; _certificateFile = null; } }); }, validator: (value) { if (value == null) { return 'Vui lòng chọn vai trò'; } return null; }, ); }, loading: () => const SizedBox( height: 48, child: Center(child: CircularProgressIndicator()), ), error: (error, stack) => Container( height: 48, padding: const EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: AppColors.danger.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), border: Border.all(color: AppColors.danger), ), child: Row( children: [ const Icon(Icons.error_outline, color: AppColors.danger, size: 20), const SizedBox(width: AppSpacing.xs), Expanded( child: Text( 'Lỗi tải vai trò', style: const TextStyle(color: AppColors.danger, fontSize: 12), ), ), TextButton( onPressed: _initializeData, child: const Text('Thử lại', style: TextStyle(fontSize: 12)), ), ], ), ), ); } /// Build city dropdown Widget _buildCityDropdown() { final citiesAsync = ref.watch(citiesProvider); return citiesAsync.when( data: (cities) { return DropdownButtonFormField( initialValue: _selectedCity, decoration: _buildInputDecoration( hintText: 'Chọn tỉnh/thành phố', prefixIcon: Icons.location_city, ), items: cities .map( (city) => DropdownMenuItem( value: city, child: Text(city.cityName), ), ) .toList(), onChanged: (value) { setState(() { _selectedCity = value; }); }, validator: (value) { if (value == null) { return 'Vui lòng chọn tỉnh/thành phố'; } return null; }, ); }, loading: () => const SizedBox( height: 48, child: Center(child: CircularProgressIndicator()), ), error: (error, stack) => Container( height: 48, padding: const EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: AppColors.danger.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), border: Border.all(color: AppColors.danger), ), child: Row( children: [ const Icon(Icons.error_outline, color: AppColors.danger, size: 20), const SizedBox(width: AppSpacing.xs), Expanded( child: Text( 'Lỗi tải danh sách tỉnh/thành phố', style: const TextStyle(color: AppColors.danger, fontSize: 12), ), ), TextButton( onPressed: _initializeData, child: const Text('Thử lại', style: TextStyle(fontSize: 12)), ), ], ), ), ); } /// Build verification section Widget _buildVerificationSection() { return Container( decoration: BoxDecoration( color: const Color(0xFFF8FAFC), border: Border.all(color: const Color(0xFFE2E8F0), width: 2), borderRadius: BorderRadius.circular(AppRadius.lg), ), padding: const EdgeInsets.all(AppSpacing.md), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.shield, color: AppColors.primaryBlue, size: 20), const SizedBox(width: AppSpacing.xs), const Text( 'Thông tin xác thực', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.primaryBlue, ), ), ], ), const SizedBox(height: AppSpacing.xs), const Text( 'Thông tin này sẽ được dùng để xác minh tư cách chuyên môn của bạn', style: TextStyle(fontSize: 12, color: AppColors.grey500), textAlign: TextAlign.center, ), const SizedBox(height: AppSpacing.md), // ID Number _buildLabel('Số CCCD/CMND'), TextFormField( controller: _idNumberController, focusNode: _idNumberFocus, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, decoration: _buildInputDecoration( hintText: 'Nhập số CCCD/CMND', prefixIcon: Icons.badge, ), ), const SizedBox(height: AppSpacing.md), // Tax Code _buildLabel('Mã số thuế cá nhân/Công ty'), TextFormField( controller: _taxCodeController, focusNode: _taxCodeFocus, keyboardType: TextInputType.number, textInputAction: TextInputAction.done, decoration: _buildInputDecoration( hintText: 'Nhập mã số thuế (không bắt buộc)', prefixIcon: Icons.receipt_long, ), validator: Validators.taxIdOptional, ), const SizedBox(height: AppSpacing.md), // ID Card Upload _buildLabel('Ảnh mặt trước CCCD/CMND'), FileUploadCard( file: _idCardFile, onTap: () => _pickImage(true), onRemove: () => _removeImage(true), icon: Icons.camera_alt, title: 'Chụp ảnh hoặc chọn file', subtitle: 'JPG, PNG tối đa 5MB', ), const SizedBox(height: AppSpacing.md), // Certificate Upload _buildLabel('Ảnh chứng chỉ hành nghề hoặc GPKD'), FileUploadCard( file: _certificateFile, onTap: () => _pickImage(false), onRemove: () => _removeImage(false), icon: Icons.file_present, title: 'Chụp ảnh hoặc chọn file', subtitle: 'JPG, PNG tối đa 5MB', ), ], ), ); } }