Files
worker/lib/features/auth/presentation/pages/register_page.dart
Phuoc Nguyen 49a41d24eb update theme
2025-12-02 15:20:54 +07:00

996 lines
34 KiB
Dart

/// 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<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends ConsumerState<RegisterPage> {
// Form key
final _formKey = GlobalKey<FormState>();
// 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<void> _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<String?> _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<void> _pickImage(bool isIdCard) async {
try {
// Show bottom sheet to select source
final source = await showModalBottomSheet<ImageSource>(
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<void> _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)]
: <String?>[];
// Remove null values from certificates list
final validCertificates = certificatesBase64
.whereType<String>()
.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<void>.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) {
// Get color scheme at the start of build method
final colorScheme = Theme.of(context).colorScheme;
// Initialize data on first build
if (!_hasInitialized) {
// Use addPostFrameCallback to avoid calling setState during build
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializeData();
});
}
return Scaffold(
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
onPressed: () => context.pop(),
),
title: Text(
'Đăng ký tài khoản',
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
centerTitle: false,
),
body: _isLoadingData
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: AppSpacing.md),
Text(
'Đang tải dữ liệu...',
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
],
),
)
: SafeArea(
child: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Welcome section
Text(
'Tạo tài khoản mới',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.xs),
Text(
'Điền thông tin để bắt đầu',
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.lg),
// Form card
Container(
decoration: BoxDecoration(
color: colorScheme.surface,
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 *', colorScheme),
TextFormField(
controller: _fullNameController,
focusNode: _fullNameFocus,
textInputAction: TextInputAction.next,
decoration: _buildInputDecoration(
hintText: 'Nhập họ và tên',
prefixIcon: FontAwesomeIcons.user,
colorScheme: colorScheme,
),
validator: (value) => Validators.minLength(
value,
3,
fieldName: 'Họ và tên',
),
),
const SizedBox(height: AppSpacing.md),
// Phone Number
_buildLabel('Số điện thoại *', colorScheme),
PhoneInputField(
controller: _phoneController,
focusNode: _phoneFocus,
validator: Validators.phone,
),
const SizedBox(height: AppSpacing.md),
// Email
_buildLabel('Email *', colorScheme),
TextFormField(
controller: _emailController,
focusNode: _emailFocus,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
decoration: _buildInputDecoration(
hintText: 'Nhập email',
prefixIcon: FontAwesomeIcons.envelope,
colorScheme: colorScheme,
),
validator: Validators.email,
),
const SizedBox(height: AppSpacing.md),
// Password
_buildLabel('Mật khẩu *', colorScheme),
TextFormField(
controller: _passwordController,
focusNode: _passwordFocus,
obscureText: !_passwordVisible,
textInputAction: TextInputAction.done,
decoration: _buildInputDecoration(
hintText: 'Tạo mật khẩu mới',
prefixIcon: FontAwesomeIcons.lock,
colorScheme: colorScheme,
suffixIcon: IconButton(
icon: Icon(
_passwordVisible
? FontAwesomeIcons.eye
: FontAwesomeIcons.eyeSlash,
color: colorScheme.onSurfaceVariant,
),
onPressed: () {
setState(() {
_passwordVisible = !_passwordVisible;
});
},
),
),
validator: (value) =>
Validators.passwordSimple(value, minLength: 6),
),
const SizedBox(height: AppSpacing.xs),
Text(
'Mật khẩu tối thiểu 6 ký tự',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: AppSpacing.md),
// Role Selection (Customer Groups)
_buildLabel('Vai trò *', colorScheme),
_buildCustomerGroupDropdown(colorScheme),
const SizedBox(height: AppSpacing.md),
// Verification Section (conditional)
if (_shouldShowVerification) ...[
_buildVerificationSection(colorScheme),
const SizedBox(height: AppSpacing.md),
],
// Company Name (optional)
_buildLabel('Tên công ty/Cửa hàng', colorScheme),
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,
colorScheme: colorScheme,
),
),
const SizedBox(height: AppSpacing.md),
// City/Province
_buildLabel('Tỉnh/Thành phố *', colorScheme),
_buildCityDropdown(colorScheme),
const SizedBox(height: AppSpacing.md),
// Terms and Conditions
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Checkbox(
value: _termsAccepted,
onChanged: (value) {
setState(() {
_termsAccepted = value ?? false;
});
},
activeColor: colorScheme.primary,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 12.0),
child: GestureDetector(
onTap: () {
setState(() {
_termsAccepted = !_termsAccepted;
});
},
child: Text.rich(
TextSpan(
text: 'Tôi đồng ý với ',
style: const TextStyle(fontSize: 13),
children: [
TextSpan(
text: 'Điều khoản sử dụng',
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
const TextSpan(text: ''),
TextSpan(
text: 'Chính sách bảo mật',
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
),
],
),
const SizedBox(height: AppSpacing.lg),
// Register Button
SizedBox(
height: ButtonSpecs.height,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleRegister,
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
ButtonSpecs.borderRadius,
),
),
),
child: _isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
colorScheme.onPrimary,
),
),
)
: const Text(
'Đăng ký',
style: TextStyle(
fontSize: ButtonSpecs.fontSize,
fontWeight: ButtonSpecs.fontWeight,
),
),
),
),
],
),
),
const SizedBox(height: AppSpacing.lg),
// Login Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Đã có tài khoản? ',
style: TextStyle(fontSize: 13, color: colorScheme.onSurfaceVariant),
),
GestureDetector(
onTap: () => context.pop(),
child: Text(
'Đăng nhập',
style: TextStyle(
fontSize: 13,
color: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: AppSpacing.lg),
],
),
),
),
),
);
}
/// Build label widget
Widget _buildLabel(String text, ColorScheme colorScheme) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: Text(
text,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
);
}
/// Build input decoration
InputDecoration _buildInputDecoration({
required String hintText,
required IconData prefixIcon,
required ColorScheme colorScheme,
Widget? suffixIcon,
}) {
return InputDecoration(
hintText: hintText,
hintStyle: TextStyle(
fontSize: InputFieldSpecs.hintFontSize,
color: colorScheme.onSurfaceVariant,
),
prefixIcon: Icon(
prefixIcon,
color: colorScheme.primary,
size: AppIconSize.md,
),
suffixIcon: suffixIcon,
filled: true,
fillColor: colorScheme.surface,
contentPadding: InputFieldSpecs.contentPadding,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: BorderSide(color: colorScheme.primary, 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(ColorScheme colorScheme) {
final customerGroupsAsync = ref.watch(customerGroupsProvider);
return customerGroupsAsync.when(
data: (groups) {
return DropdownButtonFormField<CustomerGroup>(
initialValue: _selectedRole,
decoration: _buildInputDecoration(
hintText: 'Chọn vai trò',
prefixIcon: FontAwesomeIcons.briefcase,
colorScheme: colorScheme,
),
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(ColorScheme colorScheme) {
final citiesAsync = ref.watch(citiesProvider);
return citiesAsync.when(
data: (cities) {
return DropdownButtonFormField<City>(
initialValue: _selectedCity,
decoration: _buildInputDecoration(
hintText: 'Chọn tỉnh/thành phố',
prefixIcon: Icons.location_city,
colorScheme: colorScheme,
),
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(ColorScheme colorScheme) {
return Container(
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLowest,
border: Border.all(color: colorScheme.outlineVariant, 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: [
Icon(Icons.shield, color: colorScheme.primary, size: 20),
const SizedBox(width: AppSpacing.xs),
Text(
'Thông tin xác thực',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
],
),
const SizedBox(height: AppSpacing.xs),
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: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.md),
// ID Number
_buildLabel('Số CCCD/CMND', colorScheme),
TextFormField(
controller: _idNumberController,
focusNode: _idNumberFocus,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
decoration: _buildInputDecoration(
hintText: 'Nhập số CCCD/CMND',
prefixIcon: Icons.badge,
colorScheme: colorScheme,
),
),
const SizedBox(height: AppSpacing.md),
// Tax Code
_buildLabel('Mã số thuế cá nhân/Công ty', colorScheme),
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,
colorScheme: colorScheme,
),
validator: Validators.taxIdOptional,
),
const SizedBox(height: AppSpacing.md),
// ID Card Upload
_buildLabel('Ảnh mặt trước CCCD/CMND', colorScheme),
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', colorScheme),
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',
),
],
),
);
}
}