Files
worker/lib/features/auth/presentation/pages/register_page.dart
2025-11-07 11:52:06 +07:00

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: ''),
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',
),
],
),
);
}
}