add auth, format
This commit is contained in:
492
lib/features/auth/presentation/pages/login_page.dart
Normal file
492
lib/features/auth/presentation/pages/login_page.dart
Normal file
@@ -0,0 +1,492 @@
|
||||
/// Login Page
|
||||
///
|
||||
/// Main authentication page for the Worker app.
|
||||
/// Allows users to login with phone number and password.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.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/presentation/providers/auth_provider.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/password_visibility_provider.dart';
|
||||
import 'package:worker/features/auth/presentation/widgets/phone_input_field.dart';
|
||||
|
||||
/// Login Page
|
||||
///
|
||||
/// Provides phone and password authentication.
|
||||
/// On successful login, navigates to home page.
|
||||
/// Links to registration page for new users.
|
||||
///
|
||||
/// Features:
|
||||
/// - Phone number input with Vietnamese format validation
|
||||
/// - Password input with visibility toggle
|
||||
/// - Form validation
|
||||
/// - Loading states
|
||||
/// - Error handling with snackbar
|
||||
/// - Link to registration
|
||||
/// - Customer support link
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
// Form key for validation
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Controllers
|
||||
final _phoneController = TextEditingController(text: "0988111111");
|
||||
final _passwordController = TextEditingController(text: "123456");
|
||||
|
||||
// Focus nodes
|
||||
final _phoneFocusNode = FocusNode();
|
||||
final _passwordFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneController.dispose();
|
||||
_passwordController.dispose();
|
||||
_phoneFocusNode.dispose();
|
||||
_passwordFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Handle login button press
|
||||
Future<void> _handleLogin() async {
|
||||
// Validate form
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unfocus keyboard
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
try {
|
||||
// Call login method
|
||||
await ref
|
||||
.read(authProvider.notifier)
|
||||
.login(
|
||||
phoneNumber: _phoneController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
);
|
||||
|
||||
// Check if login was successful
|
||||
final authState = ref.read(authProvider);
|
||||
authState.when(
|
||||
data: (user) {
|
||||
if (user != null && mounted) {
|
||||
// Navigate to home on success
|
||||
context.goHome();
|
||||
}
|
||||
},
|
||||
loading: () {},
|
||||
error: (error, stack) {
|
||||
// Show error snackbar
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.toString()),
|
||||
backgroundColor: AppColors.danger,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Show error snackbar
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Đăng nhập thất bại: ${e.toString()}'),
|
||||
backgroundColor: AppColors.danger,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigate to register page
|
||||
void _navigateToRegister() {
|
||||
// TODO: Navigate to register page when route is set up
|
||||
// context.go('/register');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Chức năng đăng ký đang được phát triển'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show support dialog
|
||||
void _showSupport() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Hỗ trợ khách hàng'),
|
||||
content: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Hotline: 1900 xxxx'),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text('Email: support@eurotile.vn'),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text('Giờ làm việc: 8:00 - 17:00 (T2-T6)'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Đóng'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Watch auth state for loading indicator
|
||||
final authState = ref.watch(authProvider);
|
||||
final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Logo Section
|
||||
_buildLogo(),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Welcome Message
|
||||
_buildWelcomeMessage(),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Login Form Card
|
||||
_buildLoginForm(authState, isPasswordVisible),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Register Link
|
||||
_buildRegisterLink(),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Support Link
|
||||
_buildSupportLink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build logo section
|
||||
Widget _buildLogo() {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 20.0),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.primaryBlue, AppColors.lightBlue],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
child: const Column(
|
||||
children: [
|
||||
Text(
|
||||
'EUROTILE',
|
||||
style: TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.0),
|
||||
Text(
|
||||
'Worker App',
|
||||
style: TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 12.0,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build welcome message
|
||||
Widget _buildWelcomeMessage() {
|
||||
return const Column(
|
||||
children: [
|
||||
Text(
|
||||
'Xin chào!',
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'Đăng nhập để tiếp tục',
|
||||
style: TextStyle(fontSize: 16.0, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Build login form card
|
||||
Widget _buildLoginForm(
|
||||
AsyncValue<dynamic> authState,
|
||||
bool isPasswordVisible,
|
||||
) {
|
||||
final isLoading = authState.isLoading;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10.0,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Phone Input
|
||||
PhoneInputField(
|
||||
controller: _phoneController,
|
||||
focusNode: _phoneFocusNode,
|
||||
validator: Validators.phone,
|
||||
enabled: !isLoading,
|
||||
onFieldSubmitted: (_) {
|
||||
// Move focus to password field
|
||||
FocusScope.of(context).requestFocus(_passwordFocusNode);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Password Input
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocusNode,
|
||||
enabled: !isLoading,
|
||||
obscureText: !isPasswordVisible,
|
||||
textInputAction: TextInputAction.done,
|
||||
style: const TextStyle(
|
||||
fontSize: InputFieldSpecs.fontSize,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Mật khẩu',
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.labelFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
hintText: 'Nhập mật khẩu',
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.hintFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.lock,
|
||||
color: AppColors.primaryBlue,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
isPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
color: AppColors.grey500,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
onPressed: () {
|
||||
ref.read(passwordVisibilityProvider.notifier).toggle();
|
||||
},
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
errorStyle: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
Validators.passwordSimple(value, minLength: 6),
|
||||
onFieldSubmitted: (_) {
|
||||
if (!isLoading) {
|
||||
_handleLogin();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Login Button
|
||||
SizedBox(
|
||||
height: ButtonSpecs.height,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : _handleLogin,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
disabledBackgroundColor: AppColors.grey100,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
elevation: ButtonSpecs.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
AppColors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Đăng nhập',
|
||||
style: TextStyle(
|
||||
fontSize: ButtonSpecs.fontSize,
|
||||
fontWeight: ButtonSpecs.fontWeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build register link
|
||||
Widget _buildRegisterLink() {
|
||||
return Center(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: 'Chưa có tài khoản? ',
|
||||
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: GestureDetector(
|
||||
onTap: _navigateToRegister,
|
||||
child: const Text(
|
||||
'Đăng ký ngay',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.primaryBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build support link
|
||||
Widget _buildSupportLink() {
|
||||
return Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: _showSupport,
|
||||
icon: const Icon(
|
||||
Icons.headset_mic,
|
||||
size: AppIconSize.sm,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
label: const Text(
|
||||
'Hỗ trợ khách hàng',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.primaryBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
790
lib/features/auth/presentation/pages/register_page.dart
Normal file
790
lib/features/auth/presentation/pages/register_page.dart
Normal file
@@ -0,0 +1,790 @@
|
||||
/// 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',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user