791 lines
28 KiB
Dart
791 lines
28 KiB
Dart
/// Registration Page
|
|
///
|
|
/// User registration form with role-based verification requirements.
|
|
/// Matches design from html/register.html
|
|
library;
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.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/theme/colors.dart';
|
|
import 'package:worker/core/utils/validators.dart';
|
|
import 'package:worker/features/auth/presentation/widgets/phone_input_field.dart';
|
|
import 'package:worker/features/auth/presentation/widgets/file_upload_card.dart';
|
|
import 'package:worker/features/auth/presentation/widgets/role_dropdown.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: Login page
|
|
/// - To: OTP verification (broker/other) or pending approval (worker/dealer)
|
|
class RegisterPage extends ConsumerStatefulWidget {
|
|
const RegisterPage({super.key});
|
|
|
|
@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
|
|
String? _selectedRole;
|
|
String? _selectedCity;
|
|
File? _idCardFile;
|
|
File? _certificateFile;
|
|
bool _termsAccepted = false;
|
|
bool _passwordVisible = false;
|
|
bool _isLoading = false;
|
|
|
|
final _imagePicker = ImagePicker();
|
|
|
|
@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
|
|
bool get _shouldShowVerification {
|
|
return _selectedRole == 'worker' || _selectedRole == 'dealer';
|
|
}
|
|
|
|
/// 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 Icon(Icons.camera_alt),
|
|
title: const Text('Chụp ảnh'),
|
|
onTap: () => Navigator.pop(context, ImageSource.camera),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.photo_library),
|
|
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 {
|
|
// TODO: Implement actual registration API call
|
|
// For now, simulate API delay
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
|
|
if (mounted) {
|
|
// Navigate based on role
|
|
if (_shouldShowVerification) {
|
|
// For workers/dealers with verification, show pending page
|
|
// TODO: Navigate to pending approval page
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Đăng ký thành công! Tài khoản đang chờ xét duyệt.',
|
|
),
|
|
backgroundColor: AppColors.success,
|
|
),
|
|
);
|
|
context.pop();
|
|
} else {
|
|
// For other roles, navigate to OTP verification
|
|
// TODO: Navigate to OTP verification page
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Đăng ký thành công! Vui lòng xác thực OTP.'),
|
|
backgroundColor: AppColors.success,
|
|
),
|
|
);
|
|
// context.push('/otp-verification');
|
|
context.pop();
|
|
}
|
|
}
|
|
} 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) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF4F6F8),
|
|
appBar: AppBar(
|
|
backgroundColor: AppColors.white,
|
|
elevation: AppBarSpecs.elevation,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
|
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: 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.withOpacity(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: Icons.person,
|
|
),
|
|
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: Icons.email,
|
|
),
|
|
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: Icons.lock,
|
|
suffixIcon: IconButton(
|
|
icon: Icon(
|
|
_passwordVisible
|
|
? Icons.visibility
|
|
: Icons.visibility_off,
|
|
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
|
|
_buildLabel('Vai trò *'),
|
|
RoleDropdown(
|
|
value: _selectedRole,
|
|
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 || value.isEmpty) {
|
|
return 'Vui lòng chọn vai trò';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
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: Icons.business,
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// City/Province
|
|
_buildLabel('Tỉnh/Thành phố *'),
|
|
DropdownButtonFormField<String>(
|
|
value: _selectedCity,
|
|
decoration: _buildInputDecoration(
|
|
hintText: 'Chọn tỉnh/thành phố',
|
|
prefixIcon: Icons.location_city,
|
|
),
|
|
items: const [
|
|
DropdownMenuItem(
|
|
value: 'hanoi',
|
|
child: Text('Hà Nội'),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'hcm',
|
|
child: Text('TP. Hồ Chí Minh'),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'danang',
|
|
child: Text('Đà Nẵng'),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'haiphong',
|
|
child: Text('Hải Phòng'),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'cantho',
|
|
child: Text('Cần Thơ'),
|
|
),
|
|
DropdownMenuItem(value: 'other', child: Text('Khác')),
|
|
],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_selectedCity = value;
|
|
});
|
|
},
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Vui lòng chọn tỉnh/thành phố';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
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<Color>(
|
|
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 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',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|