add auth, format

This commit is contained in:
Phuoc Nguyen
2025-11-07 11:52:06 +07:00
parent 24a8508fce
commit 3803bd26e0
173 changed files with 8505 additions and 7116 deletions

View 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,
),
),
),
);
}
}

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

View File

@@ -0,0 +1,279 @@
/// Authentication State Provider
///
/// Manages authentication state for the Worker application.
/// Handles login, logout, and user session management.
///
/// Uses Riverpod 3.0 with code generation for type-safe state management.
library;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart';
import 'package:worker/features/auth/data/models/auth_session_model.dart';
import 'package:worker/features/auth/domain/entities/user.dart';
part 'auth_provider.g.dart';
/// Provide FlutterSecureStorage instance
@riverpod
FlutterSecureStorage secureStorage(Ref ref) {
return const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
}
/// Provide AuthLocalDataSource instance
@riverpod
AuthLocalDataSource authLocalDataSource(Ref ref) {
final secureStorage = ref.watch(secureStorageProvider);
return AuthLocalDataSource(secureStorage);
}
/// Authentication state result
///
/// Represents the result of authentication operations.
/// Contains either the authenticated user or null if logged out.
typedef AuthState = AsyncValue<User?>;
/// Authentication Provider
///
/// Main provider for authentication state management.
/// Provides login and logout functionality with async state handling.
///
/// Usage in widgets:
/// ```dart
/// final authState = ref.watch(authProvider);
/// authState.when(
/// data: (user) => user != null ? HomeScreen() : LoginScreen(),
/// loading: () => LoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
@riverpod
class Auth extends _$Auth {
/// Get auth local data source
AuthLocalDataSource get _localDataSource =>
ref.read(authLocalDataSourceProvider);
/// Initialize with saved session if available
@override
Future<User?> build() async {
// Check for saved session in secure storage
final session = await _localDataSource.getSession();
if (session != null) {
// User has saved session, create User entity
final now = DateTime.now();
return User(
userId: 'user_saved', // TODO: Get from API
phoneNumber: '', // TODO: Get from saved user data
fullName: session.fullName,
email: '', // TODO: Get from saved user data
role: UserRole.customer,
status: UserStatus.active,
loyaltyTier: LoyaltyTier.gold,
totalPoints: 0,
companyInfo: null,
cccd: null,
attachments: [],
address: null,
avatarUrl: null,
referralCode: null,
referredBy: null,
erpnextCustomerId: null,
createdAt: session.createdAt,
updatedAt: now,
lastLoginAt: now,
);
}
return null;
}
/// Login with phone number and password
///
/// Simulates ERPNext API authentication with mock response.
/// Stores session data (SID, CSRF token) in Hive.
///
/// Parameters:
/// - [phoneNumber]: User's phone number (Vietnamese format)
/// - [password]: User's password
///
/// Returns: Authenticated User object on success
///
/// Throws: Exception on authentication failure
Future<void> login({
required String phoneNumber,
required String password,
}) async {
// Set loading state
state = const AsyncValue.loading();
// Simulate API call delay
state = await AsyncValue.guard(() async {
await Future<void>.delayed(const Duration(seconds: 2));
// Mock validation
if (phoneNumber.isEmpty || password.isEmpty) {
throw Exception('Số điện thoại và mật khẩu không được để trống');
}
if (password.length < 6) {
throw Exception('Mật khẩu phải có ít nhất 6 ký tự');
}
// Simulate API response matching ERPNext format
final mockApiResponse = AuthSessionResponse(
sessionExpired: 1,
message: const LoginMessage(
success: true,
message: 'Login successful',
sid: 'df7fd4e7ef1041aa3422b0ee861315ba8c28d4fe008a7d7e0e7e0e01',
csrfToken: '6b6e37563854e951c36a7af4177956bb15ca469ca4f498b742648d70',
apps: [
AppInfo(
appTitle: 'App nhân viên kinh doanh',
appEndpoint: '/ecommerce/app-sales',
appLogo:
'https://assets.digitalbiz.com.vn/DBIZ_Internal/Logo/logo_app_sales.png',
),
],
),
homePage: '/apps',
fullName: 'Tân Duy Nguyễn',
);
// Save session data to Hive
final sessionData = SessionData.fromAuthResponse(mockApiResponse);
await _localDataSource.saveSession(sessionData);
// Create and return User entity
final now = DateTime.now();
return User(
userId: 'user_${phoneNumber.replaceAll('+84', '')}',
phoneNumber: phoneNumber,
fullName: mockApiResponse.fullName,
email: 'user@eurotile.vn',
role: UserRole.customer,
status: UserStatus.active,
loyaltyTier: LoyaltyTier.gold,
totalPoints: 1500,
companyInfo: const CompanyInfo(
name: 'Công ty TNHH XYZ',
taxId: '0123456789',
businessType: 'Xây dựng',
),
cccd: '001234567890',
attachments: [],
address: '123 Đường ABC, Quận 1, TP.HCM',
avatarUrl: null,
referralCode: 'REF${phoneNumber.replaceAll('+84', '').substring(0, 6)}',
referredBy: null,
erpnextCustomerId: null,
createdAt: now.subtract(const Duration(days: 30)),
updatedAt: now,
lastLoginAt: now,
);
});
}
/// Logout current user
///
/// Clears authentication state and removes saved session from Hive.
Future<void> logout() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// Clear saved session from Hive
await _localDataSource.clearSession();
// TODO: Call logout API to invalidate token on server
await Future<void>.delayed(const Duration(milliseconds: 500));
// Return null to indicate logged out
return null;
});
}
/// Get current authenticated user
///
/// Returns the current user if logged in, null otherwise.
User? get currentUser => state.value;
/// Check if user is authenticated
///
/// Returns true if there is a logged-in user.
bool get isAuthenticated => currentUser != null;
/// Check if authentication is in progress
///
/// Returns true during login/logout operations.
bool get isLoading => state.isLoading;
/// Get authentication error if any
///
/// Returns error message or null if no error.
Object? get error => state.error;
}
/// Convenience provider for checking if user is authenticated
///
/// Usage:
/// ```dart
/// final isLoggedIn = ref.watch(isAuthenticatedProvider);
/// if (isLoggedIn) {
/// // Show home screen
/// }
/// ```
@riverpod
bool isAuthenticated(Ref ref) {
final authState = ref.watch(authProvider);
return authState.value != null;
}
/// Convenience provider for getting current user
///
/// Usage:
/// ```dart
/// final user = ref.watch(currentUserProvider);
/// if (user != null) {
/// Text('Welcome ${user.fullName}');
/// }
/// ```
@riverpod
User? currentUser(Ref ref) {
final authState = ref.watch(authProvider);
return authState.value;
}
/// Convenience provider for user's loyalty tier
///
/// Returns the current user's loyalty tier or null if not logged in.
///
/// Usage:
/// ```dart
/// final tier = ref.watch(userLoyaltyTierProvider);
/// if (tier != null) {
/// Text('Tier: ${tier.displayName}');
/// }
/// ```
@riverpod
LoyaltyTier? userLoyaltyTier(Ref ref) {
final user = ref.watch(currentUserProvider);
return user?.loyaltyTier;
}
/// Convenience provider for user's total points
///
/// Returns the current user's total loyalty points or 0 if not logged in.
///
/// Usage:
/// ```dart
/// final points = ref.watch(userTotalPointsProvider);
/// Text('Points: $points');
/// ```
@riverpod
int userTotalPoints(Ref ref) {
final user = ref.watch(currentUserProvider);
return user?.totalPoints ?? 0;
}

View File

@@ -0,0 +1,500 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'auth_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provide FlutterSecureStorage instance
@ProviderFor(secureStorage)
const secureStorageProvider = SecureStorageProvider._();
/// Provide FlutterSecureStorage instance
final class SecureStorageProvider
extends
$FunctionalProvider<
FlutterSecureStorage,
FlutterSecureStorage,
FlutterSecureStorage
>
with $Provider<FlutterSecureStorage> {
/// Provide FlutterSecureStorage instance
const SecureStorageProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'secureStorageProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$secureStorageHash();
@$internal
@override
$ProviderElement<FlutterSecureStorage> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
FlutterSecureStorage create(Ref ref) {
return secureStorage(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(FlutterSecureStorage value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<FlutterSecureStorage>(value),
);
}
}
String _$secureStorageHash() => r'c3d90388f6d1bb7c95a29ceeda2e56c57deb1ecb';
/// Provide AuthLocalDataSource instance
@ProviderFor(authLocalDataSource)
const authLocalDataSourceProvider = AuthLocalDataSourceProvider._();
/// Provide AuthLocalDataSource instance
final class AuthLocalDataSourceProvider
extends
$FunctionalProvider<
AuthLocalDataSource,
AuthLocalDataSource,
AuthLocalDataSource
>
with $Provider<AuthLocalDataSource> {
/// Provide AuthLocalDataSource instance
const AuthLocalDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'authLocalDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$authLocalDataSourceHash();
@$internal
@override
$ProviderElement<AuthLocalDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
AuthLocalDataSource create(Ref ref) {
return authLocalDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AuthLocalDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AuthLocalDataSource>(value),
);
}
}
String _$authLocalDataSourceHash() =>
r'f104de00a8ab431f6736387fb499c2b6e0ab4924';
/// Authentication Provider
///
/// Main provider for authentication state management.
/// Provides login and logout functionality with async state handling.
///
/// Usage in widgets:
/// ```dart
/// final authState = ref.watch(authProvider);
/// authState.when(
/// data: (user) => user != null ? HomeScreen() : LoginScreen(),
/// loading: () => LoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
@ProviderFor(Auth)
const authProvider = AuthProvider._();
/// Authentication Provider
///
/// Main provider for authentication state management.
/// Provides login and logout functionality with async state handling.
///
/// Usage in widgets:
/// ```dart
/// final authState = ref.watch(authProvider);
/// authState.when(
/// data: (user) => user != null ? HomeScreen() : LoginScreen(),
/// loading: () => LoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
final class AuthProvider extends $AsyncNotifierProvider<Auth, User?> {
/// Authentication Provider
///
/// Main provider for authentication state management.
/// Provides login and logout functionality with async state handling.
///
/// Usage in widgets:
/// ```dart
/// final authState = ref.watch(authProvider);
/// authState.when(
/// data: (user) => user != null ? HomeScreen() : LoginScreen(),
/// loading: () => LoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
const AuthProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'authProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$authHash();
@$internal
@override
Auth create() => Auth();
}
String _$authHash() => r'6f410d1abe6c53a6cbfa52fde7ea7a2d22a7f78d';
/// Authentication Provider
///
/// Main provider for authentication state management.
/// Provides login and logout functionality with async state handling.
///
/// Usage in widgets:
/// ```dart
/// final authState = ref.watch(authProvider);
/// authState.when(
/// data: (user) => user != null ? HomeScreen() : LoginScreen(),
/// loading: () => LoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
abstract class _$Auth extends $AsyncNotifier<User?> {
FutureOr<User?> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AsyncValue<User?>, User?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<User?>, User?>,
AsyncValue<User?>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Convenience provider for checking if user is authenticated
///
/// Usage:
/// ```dart
/// final isLoggedIn = ref.watch(isAuthenticatedProvider);
/// if (isLoggedIn) {
/// // Show home screen
/// }
/// ```
@ProviderFor(isAuthenticated)
const isAuthenticatedProvider = IsAuthenticatedProvider._();
/// Convenience provider for checking if user is authenticated
///
/// Usage:
/// ```dart
/// final isLoggedIn = ref.watch(isAuthenticatedProvider);
/// if (isLoggedIn) {
/// // Show home screen
/// }
/// ```
final class IsAuthenticatedProvider
extends $FunctionalProvider<bool, bool, bool>
with $Provider<bool> {
/// Convenience provider for checking if user is authenticated
///
/// Usage:
/// ```dart
/// final isLoggedIn = ref.watch(isAuthenticatedProvider);
/// if (isLoggedIn) {
/// // Show home screen
/// }
/// ```
const IsAuthenticatedProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'isAuthenticatedProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$isAuthenticatedHash();
@$internal
@override
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
bool create(Ref ref) {
return isAuthenticated(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$isAuthenticatedHash() => r'dc783f052ad2ddb7fa18c58e5dc6d212e6c32a96';
/// Convenience provider for getting current user
///
/// Usage:
/// ```dart
/// final user = ref.watch(currentUserProvider);
/// if (user != null) {
/// Text('Welcome ${user.fullName}');
/// }
/// ```
@ProviderFor(currentUser)
const currentUserProvider = CurrentUserProvider._();
/// Convenience provider for getting current user
///
/// Usage:
/// ```dart
/// final user = ref.watch(currentUserProvider);
/// if (user != null) {
/// Text('Welcome ${user.fullName}');
/// }
/// ```
final class CurrentUserProvider extends $FunctionalProvider<User?, User?, User?>
with $Provider<User?> {
/// Convenience provider for getting current user
///
/// Usage:
/// ```dart
/// final user = ref.watch(currentUserProvider);
/// if (user != null) {
/// Text('Welcome ${user.fullName}');
/// }
/// ```
const CurrentUserProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'currentUserProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$currentUserHash();
@$internal
@override
$ProviderElement<User?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
User? create(Ref ref) {
return currentUser(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(User? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<User?>(value),
);
}
}
String _$currentUserHash() => r'f3c1da551f4a4c2bf158782ea37a4749a718128a';
/// Convenience provider for user's loyalty tier
///
/// Returns the current user's loyalty tier or null if not logged in.
///
/// Usage:
/// ```dart
/// final tier = ref.watch(userLoyaltyTierProvider);
/// if (tier != null) {
/// Text('Tier: ${tier.displayName}');
/// }
/// ```
@ProviderFor(userLoyaltyTier)
const userLoyaltyTierProvider = UserLoyaltyTierProvider._();
/// Convenience provider for user's loyalty tier
///
/// Returns the current user's loyalty tier or null if not logged in.
///
/// Usage:
/// ```dart
/// final tier = ref.watch(userLoyaltyTierProvider);
/// if (tier != null) {
/// Text('Tier: ${tier.displayName}');
/// }
/// ```
final class UserLoyaltyTierProvider
extends $FunctionalProvider<LoyaltyTier?, LoyaltyTier?, LoyaltyTier?>
with $Provider<LoyaltyTier?> {
/// Convenience provider for user's loyalty tier
///
/// Returns the current user's loyalty tier or null if not logged in.
///
/// Usage:
/// ```dart
/// final tier = ref.watch(userLoyaltyTierProvider);
/// if (tier != null) {
/// Text('Tier: ${tier.displayName}');
/// }
/// ```
const UserLoyaltyTierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'userLoyaltyTierProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$userLoyaltyTierHash();
@$internal
@override
$ProviderElement<LoyaltyTier?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
LoyaltyTier? create(Ref ref) {
return userLoyaltyTier(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(LoyaltyTier? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<LoyaltyTier?>(value),
);
}
}
String _$userLoyaltyTierHash() => r'f1a157486b8bdd2cf64bc2201207f2ac71ea6a69';
/// Convenience provider for user's total points
///
/// Returns the current user's total loyalty points or 0 if not logged in.
///
/// Usage:
/// ```dart
/// final points = ref.watch(userTotalPointsProvider);
/// Text('Points: $points');
/// ```
@ProviderFor(userTotalPoints)
const userTotalPointsProvider = UserTotalPointsProvider._();
/// Convenience provider for user's total points
///
/// Returns the current user's total loyalty points or 0 if not logged in.
///
/// Usage:
/// ```dart
/// final points = ref.watch(userTotalPointsProvider);
/// Text('Points: $points');
/// ```
final class UserTotalPointsProvider extends $FunctionalProvider<int, int, int>
with $Provider<int> {
/// Convenience provider for user's total points
///
/// Returns the current user's total loyalty points or 0 if not logged in.
///
/// Usage:
/// ```dart
/// final points = ref.watch(userTotalPointsProvider);
/// Text('Points: $points');
/// ```
const UserTotalPointsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'userTotalPointsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$userTotalPointsHash();
@$internal
@override
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
int create(Ref ref) {
return userTotalPoints(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
}
String _$userTotalPointsHash() => r'9ccebb48a8641c3c0624b1649303b436e82602bd';

View File

@@ -0,0 +1,112 @@
/// Password Visibility Provider
///
/// Simple state provider for toggling password visibility in login/register forms.
///
/// Uses Riverpod 3.0 with code generation for type-safe state management.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'password_visibility_provider.g.dart';
/// Password Visibility State Provider
///
/// Manages the visibility state of password input fields.
/// Default state is false (password hidden).
///
/// Usage in login/register pages:
/// ```dart
/// class LoginPage extends ConsumerWidget {
/// @override
/// Widget build(BuildContext context, WidgetRef ref) {
/// final isPasswordVisible = ref.watch(passwordVisibilityProvider);
///
/// return TextField(
/// obscureText: !isPasswordVisible,
/// decoration: InputDecoration(
/// suffixIcon: IconButton(
/// icon: Icon(
/// isPasswordVisible ? Icons.visibility : Icons.visibility_off,
/// ),
/// onPressed: () {
/// ref.read(passwordVisibilityProvider.notifier).toggle();
/// },
/// ),
/// ),
/// );
/// }
/// }
/// ```
@riverpod
class PasswordVisibility extends _$PasswordVisibility {
/// Initialize with password hidden (false)
@override
bool build() => false;
/// Toggle password visibility
///
/// Switches between showing and hiding the password.
void toggle() {
state = !state;
}
/// Show password
///
/// Sets visibility to true (password visible).
void show() {
state = true;
}
/// Hide password
///
/// Sets visibility to false (password hidden).
void hide() {
state = false;
}
}
/// Confirm Password Visibility State Provider
///
/// Separate provider for confirm password field in registration forms.
/// This allows independent control of password and confirm password visibility.
///
/// Usage in registration page:
/// ```dart
/// final isConfirmPasswordVisible = ref.watch(confirmPasswordVisibilityProvider);
///
/// TextField(
/// obscureText: !isConfirmPasswordVisible,
/// decoration: InputDecoration(
/// labelText: 'Xác nhận mật khẩu',
/// suffixIcon: IconButton(
/// icon: Icon(
/// isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
/// ),
/// onPressed: () {
/// ref.read(confirmPasswordVisibilityProvider.notifier).toggle();
/// },
/// ),
/// ),
/// );
/// ```
@riverpod
class ConfirmPasswordVisibility extends _$ConfirmPasswordVisibility {
/// Initialize with password hidden (false)
@override
bool build() => false;
/// Toggle confirm password visibility
void toggle() {
state = !state;
}
/// Show confirm password
void show() {
state = true;
}
/// Hide confirm password
void hide() {
state = false;
}
}

View File

@@ -0,0 +1,329 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'password_visibility_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Password Visibility State Provider
///
/// Manages the visibility state of password input fields.
/// Default state is false (password hidden).
///
/// Usage in login/register pages:
/// ```dart
/// class LoginPage extends ConsumerWidget {
/// @override
/// Widget build(BuildContext context, WidgetRef ref) {
/// final isPasswordVisible = ref.watch(passwordVisibilityProvider);
///
/// return TextField(
/// obscureText: !isPasswordVisible,
/// decoration: InputDecoration(
/// suffixIcon: IconButton(
/// icon: Icon(
/// isPasswordVisible ? Icons.visibility : Icons.visibility_off,
/// ),
/// onPressed: () {
/// ref.read(passwordVisibilityProvider.notifier).toggle();
/// },
/// ),
/// ),
/// );
/// }
/// }
/// ```
@ProviderFor(PasswordVisibility)
const passwordVisibilityProvider = PasswordVisibilityProvider._();
/// Password Visibility State Provider
///
/// Manages the visibility state of password input fields.
/// Default state is false (password hidden).
///
/// Usage in login/register pages:
/// ```dart
/// class LoginPage extends ConsumerWidget {
/// @override
/// Widget build(BuildContext context, WidgetRef ref) {
/// final isPasswordVisible = ref.watch(passwordVisibilityProvider);
///
/// return TextField(
/// obscureText: !isPasswordVisible,
/// decoration: InputDecoration(
/// suffixIcon: IconButton(
/// icon: Icon(
/// isPasswordVisible ? Icons.visibility : Icons.visibility_off,
/// ),
/// onPressed: () {
/// ref.read(passwordVisibilityProvider.notifier).toggle();
/// },
/// ),
/// ),
/// );
/// }
/// }
/// ```
final class PasswordVisibilityProvider
extends $NotifierProvider<PasswordVisibility, bool> {
/// Password Visibility State Provider
///
/// Manages the visibility state of password input fields.
/// Default state is false (password hidden).
///
/// Usage in login/register pages:
/// ```dart
/// class LoginPage extends ConsumerWidget {
/// @override
/// Widget build(BuildContext context, WidgetRef ref) {
/// final isPasswordVisible = ref.watch(passwordVisibilityProvider);
///
/// return TextField(
/// obscureText: !isPasswordVisible,
/// decoration: InputDecoration(
/// suffixIcon: IconButton(
/// icon: Icon(
/// isPasswordVisible ? Icons.visibility : Icons.visibility_off,
/// ),
/// onPressed: () {
/// ref.read(passwordVisibilityProvider.notifier).toggle();
/// },
/// ),
/// ),
/// );
/// }
/// }
/// ```
const PasswordVisibilityProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'passwordVisibilityProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$passwordVisibilityHash();
@$internal
@override
PasswordVisibility create() => PasswordVisibility();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$passwordVisibilityHash() =>
r'25b6fa914e42dd83c8443aecbeb1d608cccd00ab';
/// Password Visibility State Provider
///
/// Manages the visibility state of password input fields.
/// Default state is false (password hidden).
///
/// Usage in login/register pages:
/// ```dart
/// class LoginPage extends ConsumerWidget {
/// @override
/// Widget build(BuildContext context, WidgetRef ref) {
/// final isPasswordVisible = ref.watch(passwordVisibilityProvider);
///
/// return TextField(
/// obscureText: !isPasswordVisible,
/// decoration: InputDecoration(
/// suffixIcon: IconButton(
/// icon: Icon(
/// isPasswordVisible ? Icons.visibility : Icons.visibility_off,
/// ),
/// onPressed: () {
/// ref.read(passwordVisibilityProvider.notifier).toggle();
/// },
/// ),
/// ),
/// );
/// }
/// }
/// ```
abstract class _$PasswordVisibility extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Confirm Password Visibility State Provider
///
/// Separate provider for confirm password field in registration forms.
/// This allows independent control of password and confirm password visibility.
///
/// Usage in registration page:
/// ```dart
/// final isConfirmPasswordVisible = ref.watch(confirmPasswordVisibilityProvider);
///
/// TextField(
/// obscureText: !isConfirmPasswordVisible,
/// decoration: InputDecoration(
/// labelText: 'Xác nhận mật khẩu',
/// suffixIcon: IconButton(
/// icon: Icon(
/// isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
/// ),
/// onPressed: () {
/// ref.read(confirmPasswordVisibilityProvider.notifier).toggle();
/// },
/// ),
/// ),
/// );
/// ```
@ProviderFor(ConfirmPasswordVisibility)
const confirmPasswordVisibilityProvider = ConfirmPasswordVisibilityProvider._();
/// Confirm Password Visibility State Provider
///
/// Separate provider for confirm password field in registration forms.
/// This allows independent control of password and confirm password visibility.
///
/// Usage in registration page:
/// ```dart
/// final isConfirmPasswordVisible = ref.watch(confirmPasswordVisibilityProvider);
///
/// TextField(
/// obscureText: !isConfirmPasswordVisible,
/// decoration: InputDecoration(
/// labelText: 'Xác nhận mật khẩu',
/// suffixIcon: IconButton(
/// icon: Icon(
/// isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
/// ),
/// onPressed: () {
/// ref.read(confirmPasswordVisibilityProvider.notifier).toggle();
/// },
/// ),
/// ),
/// );
/// ```
final class ConfirmPasswordVisibilityProvider
extends $NotifierProvider<ConfirmPasswordVisibility, bool> {
/// Confirm Password Visibility State Provider
///
/// Separate provider for confirm password field in registration forms.
/// This allows independent control of password and confirm password visibility.
///
/// Usage in registration page:
/// ```dart
/// final isConfirmPasswordVisible = ref.watch(confirmPasswordVisibilityProvider);
///
/// TextField(
/// obscureText: !isConfirmPasswordVisible,
/// decoration: InputDecoration(
/// labelText: 'Xác nhận mật khẩu',
/// suffixIcon: IconButton(
/// icon: Icon(
/// isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
/// ),
/// onPressed: () {
/// ref.read(confirmPasswordVisibilityProvider.notifier).toggle();
/// },
/// ),
/// ),
/// );
/// ```
const ConfirmPasswordVisibilityProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'confirmPasswordVisibilityProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$confirmPasswordVisibilityHash();
@$internal
@override
ConfirmPasswordVisibility create() => ConfirmPasswordVisibility();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$confirmPasswordVisibilityHash() =>
r'8408bba9db1e8deba425f98015a4e2fa76d75eb8';
/// Confirm Password Visibility State Provider
///
/// Separate provider for confirm password field in registration forms.
/// This allows independent control of password and confirm password visibility.
///
/// Usage in registration page:
/// ```dart
/// final isConfirmPasswordVisible = ref.watch(confirmPasswordVisibilityProvider);
///
/// TextField(
/// obscureText: !isConfirmPasswordVisible,
/// decoration: InputDecoration(
/// labelText: 'Xác nhận mật khẩu',
/// suffixIcon: IconButton(
/// icon: Icon(
/// isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
/// ),
/// onPressed: () {
/// ref.read(confirmPasswordVisibilityProvider.notifier).toggle();
/// },
/// ),
/// ),
/// );
/// ```
abstract class _$ConfirmPasswordVisibility extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -0,0 +1,305 @@
/// Registration State Provider
///
/// Manages registration state for the Worker application.
/// Handles user registration with role-based validation and verification.
///
/// Uses Riverpod 3.0 with code generation for type-safe state management.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/auth/domain/entities/user.dart';
part 'register_provider.g.dart';
/// Registration Form Data
///
/// Contains all data needed for user registration.
/// Optional fields are used based on selected role.
class RegistrationData {
/// Required: Full name of the user
final String fullName;
/// Required: Phone number (Vietnamese format)
final String phoneNumber;
/// Required: Email address
final String email;
/// Required: Password (minimum 6 characters)
final String password;
/// Required: User role
final UserRole role;
/// Optional: CCCD/ID card number (required for dealer/worker roles)
final String? cccd;
/// Optional: Tax code (personal or company)
final String? taxCode;
/// Optional: Company/store name
final String? companyName;
/// Required: Province/city
final String? city;
/// Optional: Attachment file paths (ID card, certificate, license)
final List<String>? attachments;
const RegistrationData({
required this.fullName,
required this.phoneNumber,
required this.email,
required this.password,
required this.role,
this.cccd,
this.taxCode,
this.companyName,
this.city,
this.attachments,
});
/// Copy with method for immutability
RegistrationData copyWith({
String? fullName,
String? phoneNumber,
String? email,
String? password,
UserRole? role,
String? cccd,
String? taxCode,
String? companyName,
String? city,
List<String>? attachments,
}) {
return RegistrationData(
fullName: fullName ?? this.fullName,
phoneNumber: phoneNumber ?? this.phoneNumber,
email: email ?? this.email,
password: password ?? this.password,
role: role ?? this.role,
cccd: cccd ?? this.cccd,
taxCode: taxCode ?? this.taxCode,
companyName: companyName ?? this.companyName,
city: city ?? this.city,
attachments: attachments ?? this.attachments,
);
}
}
/// Registration State Provider
///
/// Main provider for user registration state management.
/// Handles registration process with role-based validation.
///
/// Usage in widgets:
/// ```dart
/// final registerState = ref.watch(registerProvider);
/// registerState.when(
/// data: (user) => SuccessScreen(user),
/// loading: () => LoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
@riverpod
class Register extends _$Register {
/// Initialize with no registration result
@override
Future<User?> build() async {
// No initial registration
return null;
}
/// Register a new user
///
/// Performs user registration with role-based validation.
/// For dealer/worker roles, requires additional verification documents.
///
/// Parameters:
/// - [data]: Registration form data containing all required fields
///
/// Returns: Newly created User object on success
///
/// Throws: Exception on validation failure or registration error
///
/// Error messages (Vietnamese):
/// - "Vui lòng điền đầy đủ thông tin bắt buộc"
/// - "Số điện thoại không hợp lệ"
/// - "Email không hợp lệ"
/// - "Mật khẩu phải có ít nhất 6 ký tự"
/// - "Vui lòng nhập số CCCD/CMND" (for dealer/worker)
/// - "Vui lòng tải lên ảnh CCCD/CMND" (for dealer/worker)
/// - "Vui lòng tải lên ảnh chứng chỉ hành nghề hoặc GPKD" (for dealer/worker)
/// - "Số điện thoại đã được đăng ký"
/// - "Email đã được đăng ký"
Future<void> register(RegistrationData data) async {
// Set loading state
state = const AsyncValue.loading();
// Perform registration with error handling
state = await AsyncValue.guard(() async {
// Validate required fields
if (data.fullName.isEmpty ||
data.phoneNumber.isEmpty ||
data.email.isEmpty ||
data.password.isEmpty ||
data.city == null ||
data.city!.isEmpty) {
throw Exception('Vui lòng điền đầy đủ thông tin bắt buộc');
}
// Validate phone number (Vietnamese format: 10 digits starting with 0)
final phoneRegex = RegExp(r'^0[0-9]{9}$');
if (!phoneRegex.hasMatch(data.phoneNumber)) {
throw Exception('Số điện thoại không hợp lệ');
}
// Validate email format
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(data.email)) {
throw Exception('Email không hợp lệ');
}
// Validate password length
if (data.password.length < 6) {
throw Exception('Mật khẩu phải có ít nhất 6 ký tự');
}
// Role-based validation for dealer/worker (requires verification)
if (data.role == UserRole.customer) {
// For dealer/worker roles, CCCD and attachments are required
if (data.cccd == null || data.cccd!.isEmpty) {
throw Exception('Vui lòng nhập số CCCD/CMND');
}
// Validate CCCD format (9 or 12 digits)
final cccdRegex = RegExp(r'^[0-9]{9}$|^[0-9]{12}$');
if (!cccdRegex.hasMatch(data.cccd!)) {
throw Exception('Số CCCD/CMND không hợp lệ (phải có 9 hoặc 12 số)');
}
// Validate attachments
if (data.attachments == null || data.attachments!.isEmpty) {
throw Exception('Vui lòng tải lên ảnh CCCD/CMND');
}
if (data.attachments!.length < 2) {
throw Exception('Vui lòng tải lên ảnh chứng chỉ hành nghề hoặc GPKD');
}
}
// Simulate API call delay (2 seconds)
await Future<void>.delayed(const Duration(seconds: 2));
// TODO: In production, call the registration API here
// final response = await ref.read(authRepositoryProvider).register(data);
// Mock: Simulate registration success
final now = DateTime.now();
// Determine initial status based on role
// Dealer/Worker require admin approval (pending status)
// Other roles are immediately active
final initialStatus = data.role == UserRole.customer
? UserStatus.pending
: UserStatus.active;
// Create new user entity
final newUser = User(
userId: 'user_${DateTime.now().millisecondsSinceEpoch}',
phoneNumber: data.phoneNumber,
fullName: data.fullName,
email: data.email,
role: data.role,
status: initialStatus,
loyaltyTier: LoyaltyTier.gold, // Default tier for new users
totalPoints: 0, // New users start with 0 points
companyInfo: data.companyName != null || data.taxCode != null
? CompanyInfo(
name: data.companyName,
taxId: data.taxCode,
businessType: _getBusinessType(data.role),
)
: null,
cccd: data.cccd,
attachments: data.attachments ?? [],
address: data.city,
avatarUrl: null,
referralCode: 'REF${data.phoneNumber.substring(0, 6)}',
referredBy: null,
erpnextCustomerId: null,
createdAt: now,
updatedAt: now,
lastLoginAt: null, // Not logged in yet
);
return newUser;
});
}
/// Reset registration state
///
/// Clears the registration result. Useful when navigating away
/// from success screen or starting a new registration.
Future<void> reset() async {
state = const AsyncValue.data(null);
}
/// Get business type based on user role
String _getBusinessType(UserRole role) {
switch (role) {
case UserRole.customer:
return 'Đại lý/Thầu thợ/Kiến trúc sư';
case UserRole.sales:
return 'Nhân viên kinh doanh';
case UserRole.admin:
return 'Quản trị viên';
case UserRole.accountant:
return 'Kế toán';
case UserRole.designer:
return 'Thiết kế';
}
}
/// Check if registration is in progress
bool get isLoading => state.isLoading;
/// Get registration error if any
Object? get error => state.error;
/// Get registered user if successful
User? get registeredUser => state.value;
/// Check if registration was successful
bool get isSuccess => state.hasValue && state.value != null;
}
/// Convenience provider for checking if registration is in progress
///
/// Usage:
/// ```dart
/// final isRegistering = ref.watch(isRegisteringProvider);
/// if (isRegistering) {
/// // Show loading indicator
/// }
/// ```
@riverpod
bool isRegistering(Ref ref) {
final registerState = ref.watch(registerProvider);
return registerState.isLoading;
}
/// Convenience provider for checking if registration was successful
///
/// Usage:
/// ```dart
/// final success = ref.watch(registrationSuccessProvider);
/// if (success) {
/// // Navigate to pending approval or OTP screen
/// }
/// ```
@riverpod
bool registrationSuccess(Ref ref) {
final registerState = ref.watch(registerProvider);
return registerState.hasValue && registerState.value != null;
}

View File

@@ -0,0 +1,251 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'register_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Registration State Provider
///
/// Main provider for user registration state management.
/// Handles registration process with role-based validation.
///
/// Usage in widgets:
/// ```dart
/// final registerState = ref.watch(registerProvider);
/// registerState.when(
/// data: (user) => SuccessScreen(user),
/// loading: () => LoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
@ProviderFor(Register)
const registerProvider = RegisterProvider._();
/// Registration State Provider
///
/// Main provider for user registration state management.
/// Handles registration process with role-based validation.
///
/// Usage in widgets:
/// ```dart
/// final registerState = ref.watch(registerProvider);
/// registerState.when(
/// data: (user) => SuccessScreen(user),
/// loading: () => LoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
final class RegisterProvider extends $AsyncNotifierProvider<Register, User?> {
/// Registration State Provider
///
/// Main provider for user registration state management.
/// Handles registration process with role-based validation.
///
/// Usage in widgets:
/// ```dart
/// final registerState = ref.watch(registerProvider);
/// registerState.when(
/// data: (user) => SuccessScreen(user),
/// loading: () => LoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
const RegisterProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'registerProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$registerHash();
@$internal
@override
Register create() => Register();
}
String _$registerHash() => r'a073b5c5958b74c63a3cddfec7f6f018e14a5088';
/// Registration State Provider
///
/// Main provider for user registration state management.
/// Handles registration process with role-based validation.
///
/// Usage in widgets:
/// ```dart
/// final registerState = ref.watch(registerProvider);
/// registerState.when(
/// data: (user) => SuccessScreen(user),
/// loading: () => LoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
abstract class _$Register extends $AsyncNotifier<User?> {
FutureOr<User?> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AsyncValue<User?>, User?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<User?>, User?>,
AsyncValue<User?>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Convenience provider for checking if registration is in progress
///
/// Usage:
/// ```dart
/// final isRegistering = ref.watch(isRegisteringProvider);
/// if (isRegistering) {
/// // Show loading indicator
/// }
/// ```
@ProviderFor(isRegistering)
const isRegisteringProvider = IsRegisteringProvider._();
/// Convenience provider for checking if registration is in progress
///
/// Usage:
/// ```dart
/// final isRegistering = ref.watch(isRegisteringProvider);
/// if (isRegistering) {
/// // Show loading indicator
/// }
/// ```
final class IsRegisteringProvider extends $FunctionalProvider<bool, bool, bool>
with $Provider<bool> {
/// Convenience provider for checking if registration is in progress
///
/// Usage:
/// ```dart
/// final isRegistering = ref.watch(isRegisteringProvider);
/// if (isRegistering) {
/// // Show loading indicator
/// }
/// ```
const IsRegisteringProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'isRegisteringProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$isRegisteringHash();
@$internal
@override
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
bool create(Ref ref) {
return isRegistering(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$isRegisteringHash() => r'2108b87b37451de9aaf799f9b8b380924bed2c87';
/// Convenience provider for checking if registration was successful
///
/// Usage:
/// ```dart
/// final success = ref.watch(registrationSuccessProvider);
/// if (success) {
/// // Navigate to pending approval or OTP screen
/// }
/// ```
@ProviderFor(registrationSuccess)
const registrationSuccessProvider = RegistrationSuccessProvider._();
/// Convenience provider for checking if registration was successful
///
/// Usage:
/// ```dart
/// final success = ref.watch(registrationSuccessProvider);
/// if (success) {
/// // Navigate to pending approval or OTP screen
/// }
/// ```
final class RegistrationSuccessProvider
extends $FunctionalProvider<bool, bool, bool>
with $Provider<bool> {
/// Convenience provider for checking if registration was successful
///
/// Usage:
/// ```dart
/// final success = ref.watch(registrationSuccessProvider);
/// if (success) {
/// // Navigate to pending approval or OTP screen
/// }
/// ```
const RegistrationSuccessProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'registrationSuccessProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$registrationSuccessHash();
@$internal
@override
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
bool create(Ref ref) {
return registrationSuccess(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$registrationSuccessHash() =>
r'6435b9ca4bf4c287497a39077a5d4558e0515ddc';

View File

@@ -0,0 +1,175 @@
/// Selected Role State Provider
///
/// Manages the selected user role during registration.
/// Simple state provider for role selection in the registration form.
///
/// Uses Riverpod 3.0 with code generation for type-safe state management.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/auth/domain/entities/user.dart';
part 'selected_role_provider.g.dart';
/// Selected Role Provider
///
/// Manages the currently selected user role in the registration form.
/// Provides methods to select and clear role selection.
///
/// This provider is used to:
/// - Track which role the user has selected
/// - Conditionally show/hide verification fields based on role
/// - Validate required documents for dealer/worker roles
///
/// Usage in widgets:
/// ```dart
/// // Watch the selected role
/// final selectedRole = ref.watch(selectedRoleProvider);
///
/// // Select a role
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
///
/// // Clear selection
/// ref.read(selectedRoleProvider.notifier).clearRole();
///
/// // Show verification section conditionally
/// if (selectedRole == UserRole.customer) {
/// VerificationSection(),
/// }
/// ```
@riverpod
class SelectedRole extends _$SelectedRole {
/// Initialize with no role selected
@override
UserRole? build() {
return null;
}
/// Select a user role
///
/// Updates the state with the newly selected role.
/// This triggers UI updates that depend on role selection.
///
/// Parameters:
/// - [role]: The user role to select
///
/// Example:
/// ```dart
/// // User selects "Đại lý hệ thống" (dealer)
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
/// // This will show verification fields
/// ```
void selectRole(UserRole role) {
state = role;
}
/// Clear the role selection
///
/// Resets the state to null (no role selected).
/// Useful when resetting the form or canceling registration.
///
/// Example:
/// ```dart
/// // User clicks "Cancel" or goes back
/// ref.read(selectedRoleProvider.notifier).clearRole();
/// // This will hide verification fields
/// ```
void clearRole() {
state = null;
}
/// Check if a role is currently selected
///
/// Returns true if any role has been selected, false otherwise.
bool get hasSelection => state != null;
/// Check if verification is required for current role
///
/// Returns true if the selected role requires verification documents
/// (CCCD, certificates, etc.). Currently only customer role requires this.
///
/// This is used to conditionally show the verification section:
/// ```dart
/// if (ref.read(selectedRoleProvider.notifier).requiresVerification) {
/// // Show CCCD input, file uploads, etc.
/// }
/// ```
bool get requiresVerification => state == UserRole.customer;
/// Get the display name for the current role (Vietnamese)
///
/// Returns a user-friendly Vietnamese name for the selected role,
/// or null if no role is selected.
///
/// Example:
/// ```dart
/// final displayName = ref.read(selectedRoleProvider.notifier).displayName;
/// // Returns: "Đại lý hệ thống" for customer role
/// ```
String? get displayName {
if (state == null) return null;
switch (state!) {
case UserRole.customer:
return 'Đại lý/Thầu thợ/Kiến trúc sư';
case UserRole.sales:
return 'Nhân viên kinh doanh';
case UserRole.admin:
return 'Quản trị viên';
case UserRole.accountant:
return 'Kế toán';
case UserRole.designer:
return 'Thiết kế';
}
}
}
/// Convenience provider for checking if verification is required
///
/// Returns true if the currently selected role requires verification
/// documents (CCCD, certificates, etc.).
///
/// Usage:
/// ```dart
/// final needsVerification = ref.watch(requiresVerificationProvider);
/// if (needsVerification) {
/// // Show verification section with file uploads
/// }
/// ```
@riverpod
bool requiresVerification(Ref ref) {
final selectedRole = ref.watch(selectedRoleProvider);
return selectedRole == UserRole.customer;
}
/// Convenience provider for getting role display name
///
/// Returns a user-friendly Vietnamese name for the selected role,
/// or null if no role is selected.
///
/// Usage:
/// ```dart
/// final roleName = ref.watch(roleDisplayNameProvider);
/// if (roleName != null) {
/// Text('Bạn đang đăng ký với vai trò: $roleName');
/// }
/// ```
@riverpod
String? roleDisplayName(Ref ref) {
final selectedRole = ref.watch(selectedRoleProvider);
if (selectedRole == null) return null;
switch (selectedRole) {
case UserRole.customer:
return 'Đại lý/Thầu thợ/Kiến trúc sư';
case UserRole.sales:
return 'Nhân viên kinh doanh';
case UserRole.admin:
return 'Quản trị viên';
case UserRole.accountant:
return 'Kế toán';
case UserRole.designer:
return 'Thiết kế';
}
}

View File

@@ -0,0 +1,327 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'selected_role_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Selected Role Provider
///
/// Manages the currently selected user role in the registration form.
/// Provides methods to select and clear role selection.
///
/// This provider is used to:
/// - Track which role the user has selected
/// - Conditionally show/hide verification fields based on role
/// - Validate required documents for dealer/worker roles
///
/// Usage in widgets:
/// ```dart
/// // Watch the selected role
/// final selectedRole = ref.watch(selectedRoleProvider);
///
/// // Select a role
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
///
/// // Clear selection
/// ref.read(selectedRoleProvider.notifier).clearRole();
///
/// // Show verification section conditionally
/// if (selectedRole == UserRole.customer) {
/// VerificationSection(),
/// }
/// ```
@ProviderFor(SelectedRole)
const selectedRoleProvider = SelectedRoleProvider._();
/// Selected Role Provider
///
/// Manages the currently selected user role in the registration form.
/// Provides methods to select and clear role selection.
///
/// This provider is used to:
/// - Track which role the user has selected
/// - Conditionally show/hide verification fields based on role
/// - Validate required documents for dealer/worker roles
///
/// Usage in widgets:
/// ```dart
/// // Watch the selected role
/// final selectedRole = ref.watch(selectedRoleProvider);
///
/// // Select a role
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
///
/// // Clear selection
/// ref.read(selectedRoleProvider.notifier).clearRole();
///
/// // Show verification section conditionally
/// if (selectedRole == UserRole.customer) {
/// VerificationSection(),
/// }
/// ```
final class SelectedRoleProvider
extends $NotifierProvider<SelectedRole, UserRole?> {
/// Selected Role Provider
///
/// Manages the currently selected user role in the registration form.
/// Provides methods to select and clear role selection.
///
/// This provider is used to:
/// - Track which role the user has selected
/// - Conditionally show/hide verification fields based on role
/// - Validate required documents for dealer/worker roles
///
/// Usage in widgets:
/// ```dart
/// // Watch the selected role
/// final selectedRole = ref.watch(selectedRoleProvider);
///
/// // Select a role
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
///
/// // Clear selection
/// ref.read(selectedRoleProvider.notifier).clearRole();
///
/// // Show verification section conditionally
/// if (selectedRole == UserRole.customer) {
/// VerificationSection(),
/// }
/// ```
const SelectedRoleProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'selectedRoleProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$selectedRoleHash();
@$internal
@override
SelectedRole create() => SelectedRole();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(UserRole? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<UserRole?>(value),
);
}
}
String _$selectedRoleHash() => r'098c7fdaec4694d14a48c049556960eb6ed2dc06';
/// Selected Role Provider
///
/// Manages the currently selected user role in the registration form.
/// Provides methods to select and clear role selection.
///
/// This provider is used to:
/// - Track which role the user has selected
/// - Conditionally show/hide verification fields based on role
/// - Validate required documents for dealer/worker roles
///
/// Usage in widgets:
/// ```dart
/// // Watch the selected role
/// final selectedRole = ref.watch(selectedRoleProvider);
///
/// // Select a role
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
///
/// // Clear selection
/// ref.read(selectedRoleProvider.notifier).clearRole();
///
/// // Show verification section conditionally
/// if (selectedRole == UserRole.customer) {
/// VerificationSection(),
/// }
/// ```
abstract class _$SelectedRole extends $Notifier<UserRole?> {
UserRole? build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<UserRole?, UserRole?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<UserRole?, UserRole?>,
UserRole?,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Convenience provider for checking if verification is required
///
/// Returns true if the currently selected role requires verification
/// documents (CCCD, certificates, etc.).
///
/// Usage:
/// ```dart
/// final needsVerification = ref.watch(requiresVerificationProvider);
/// if (needsVerification) {
/// // Show verification section with file uploads
/// }
/// ```
@ProviderFor(requiresVerification)
const requiresVerificationProvider = RequiresVerificationProvider._();
/// Convenience provider for checking if verification is required
///
/// Returns true if the currently selected role requires verification
/// documents (CCCD, certificates, etc.).
///
/// Usage:
/// ```dart
/// final needsVerification = ref.watch(requiresVerificationProvider);
/// if (needsVerification) {
/// // Show verification section with file uploads
/// }
/// ```
final class RequiresVerificationProvider
extends $FunctionalProvider<bool, bool, bool>
with $Provider<bool> {
/// Convenience provider for checking if verification is required
///
/// Returns true if the currently selected role requires verification
/// documents (CCCD, certificates, etc.).
///
/// Usage:
/// ```dart
/// final needsVerification = ref.watch(requiresVerificationProvider);
/// if (needsVerification) {
/// // Show verification section with file uploads
/// }
/// ```
const RequiresVerificationProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'requiresVerificationProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$requiresVerificationHash();
@$internal
@override
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
bool create(Ref ref) {
return requiresVerification(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$requiresVerificationHash() =>
r'400b4242bca2defd14e46361d2b77dd94a4e3e5e';
/// Convenience provider for getting role display name
///
/// Returns a user-friendly Vietnamese name for the selected role,
/// or null if no role is selected.
///
/// Usage:
/// ```dart
/// final roleName = ref.watch(roleDisplayNameProvider);
/// if (roleName != null) {
/// Text('Bạn đang đăng ký với vai trò: $roleName');
/// }
/// ```
@ProviderFor(roleDisplayName)
const roleDisplayNameProvider = RoleDisplayNameProvider._();
/// Convenience provider for getting role display name
///
/// Returns a user-friendly Vietnamese name for the selected role,
/// or null if no role is selected.
///
/// Usage:
/// ```dart
/// final roleName = ref.watch(roleDisplayNameProvider);
/// if (roleName != null) {
/// Text('Bạn đang đăng ký với vai trò: $roleName');
/// }
/// ```
final class RoleDisplayNameProvider
extends $FunctionalProvider<String?, String?, String?>
with $Provider<String?> {
/// Convenience provider for getting role display name
///
/// Returns a user-friendly Vietnamese name for the selected role,
/// or null if no role is selected.
///
/// Usage:
/// ```dart
/// final roleName = ref.watch(roleDisplayNameProvider);
/// if (roleName != null) {
/// Text('Bạn đang đăng ký với vai trò: $roleName');
/// }
/// ```
const RoleDisplayNameProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'roleDisplayNameProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$roleDisplayNameHash();
@$internal
@override
$ProviderElement<String?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
String? create(Ref ref) {
return roleDisplayName(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(String? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<String?>(value),
);
}
}
String _$roleDisplayNameHash() => r'6cb4bfd9e76fb2f3ed52d4a249e5a2477bc6f39e';

View File

@@ -0,0 +1,216 @@
/// File Upload Card Widget
///
/// Reusable widget for uploading image files with preview.
library;
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// File Upload Card
///
/// A reusable widget for uploading files with preview functionality.
/// Features:
/// - Dashed border upload area
/// - Camera/file icon
/// - Title and subtitle
/// - Image preview after selection
/// - Remove button
///
/// Usage:
/// ```dart
/// FileUploadCard(
/// file: selectedFile,
/// onTap: () => pickImage(),
/// onRemove: () => removeImage(),
/// icon: Icons.camera_alt,
/// title: 'Chụp ảnh hoặc chọn file',
/// subtitle: 'JPG, PNG tối đa 5MB',
/// )
/// ```
class FileUploadCard extends StatelessWidget {
/// Creates a file upload card
const FileUploadCard({
super.key,
required this.file,
required this.onTap,
required this.onRemove,
required this.icon,
required this.title,
required this.subtitle,
});
/// Selected file (null if not selected)
final File? file;
/// Callback when upload area is tapped
final VoidCallback onTap;
/// Callback to remove selected file
final VoidCallback onRemove;
/// Icon to display in upload area
final IconData icon;
/// Title text
final String title;
/// Subtitle text
final String subtitle;
/// Format file size in bytes to human-readable string
String _formatFileSize(int bytes) {
if (bytes == 0) return '0 B';
const suffixes = ['B', 'KB', 'MB', 'GB'];
final i = (bytes.bitLength - 1) ~/ 10;
final size = bytes / (1 << (i * 10));
return '${size.toStringAsFixed(2)} ${suffixes[i]}';
}
/// Get file name from path
String _getFileName(String path) {
return path.split('/').last;
}
@override
Widget build(BuildContext context) {
if (file != null) {
// Show preview with remove option
return _buildPreview(context);
} else {
// Show upload area
return _buildUploadArea(context);
}
}
/// Build upload area
Widget _buildUploadArea(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Container(
decoration: BoxDecoration(
color: AppColors.white,
border: Border.all(
color: const Color(0xFFCBD5E1),
width: 2,
strokeAlign: BorderSide.strokeAlignInside,
),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
children: [
// Icon
Icon(icon, size: 32, color: AppColors.grey500),
const SizedBox(height: AppSpacing.sm),
// Title
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: AppSpacing.xs),
// Subtitle
Text(
subtitle,
style: const TextStyle(fontSize: 12, color: AppColors.grey500),
),
],
),
),
);
}
/// Build preview with remove button
Widget _buildPreview(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.white,
border: Border.all(color: AppColors.grey100, width: 1),
borderRadius: BorderRadius.circular(AppRadius.md),
),
padding: const EdgeInsets.all(AppSpacing.sm),
child: Row(
children: [
// Thumbnail
ClipRRect(
borderRadius: BorderRadius.circular(AppRadius.sm),
child: Image.file(
file!,
width: 50,
height: 50,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 50,
height: 50,
color: AppColors.grey100,
child: const Icon(
Icons.broken_image,
color: AppColors.grey500,
size: 24,
),
);
},
),
),
const SizedBox(width: AppSpacing.sm),
// File info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_getFileName(file!.path),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
FutureBuilder<int>(
future: file!.length(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
_formatFileSize(snapshot.data!),
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
);
}
return const SizedBox.shrink();
},
),
],
),
),
const SizedBox(width: AppSpacing.xs),
// Remove button
IconButton(
icon: const Icon(Icons.close, color: AppColors.danger, size: 20),
onPressed: onRemove,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
],
),
);
}
}

View File

@@ -0,0 +1,133 @@
/// Phone Input Field Widget
///
/// Custom text field for Vietnamese phone number input.
/// Supports formats: 0xxx xxx xxx, +84xxx xxx xxx, 84xxx xxx xxx
library;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Phone Input Field
///
/// A custom text field widget specifically designed for Vietnamese phone number input.
/// Features:
/// - Phone icon prefix
/// - Numeric keyboard
/// - Phone number formatting
/// - Vietnamese phone validation support
///
/// Usage:
/// ```dart
/// PhoneInputField(
/// controller: phoneController,
/// validator: Validators.phone,
/// onChanged: (value) {
/// // Handle phone number change
/// },
/// )
/// ```
class PhoneInputField extends StatelessWidget {
/// Creates a phone input field
const PhoneInputField({
super.key,
required this.controller,
this.focusNode,
this.validator,
this.onChanged,
this.onFieldSubmitted,
this.enabled = true,
this.autofocus = false,
});
/// Text editing controller
final TextEditingController controller;
/// Focus node for keyboard focus management
final FocusNode? focusNode;
/// Form field validator
final String? Function(String?)? validator;
/// Callback when text changes
final void Function(String)? onChanged;
/// Callback when field is submitted
final void Function(String)? onFieldSubmitted;
/// Whether the field is enabled
final bool enabled;
/// Whether the field should auto-focus
final bool autofocus;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
focusNode: focusNode,
autofocus: autofocus,
enabled: enabled,
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
inputFormatters: [
// Allow digits, spaces, +, and parentheses
FilteringTextInputFormatter.allow(RegExp(r'[0-9+\s()]')),
// Limit to reasonable phone length
LengthLimitingTextInputFormatter(15),
],
style: const TextStyle(
fontSize: InputFieldSpecs.fontSize,
color: AppColors.grey900,
),
decoration: InputDecoration(
labelText: 'Số điện thoại',
labelStyle: const TextStyle(
fontSize: InputFieldSpecs.labelFontSize,
color: AppColors.grey500,
),
hintText: 'Nhập số điện thoại',
hintStyle: const TextStyle(
fontSize: InputFieldSpecs.hintFontSize,
color: AppColors.grey500,
),
prefixIcon: const Icon(
Icons.phone,
color: AppColors.primaryBlue,
size: AppIconSize.md,
),
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: validator,
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
);
}
}

View File

@@ -0,0 +1,115 @@
/// Role Dropdown Widget
///
/// Dropdown for selecting user role during registration.
library;
import 'package:flutter/material.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Role Dropdown
///
/// A custom dropdown widget for selecting user role.
/// Roles:
/// - dealer: Đại lý hệ thống
/// - worker: Kiến trúc sư/ Thầu thợ
/// - broker: Khách lẻ
/// - other: Khác
///
/// Usage:
/// ```dart
/// RoleDropdown(
/// value: selectedRole,
/// onChanged: (value) {
/// setState(() {
/// selectedRole = value;
/// });
/// },
/// validator: (value) {
/// if (value == null || value.isEmpty) {
/// return 'Vui lòng chọn vai trò';
/// }
/// return null;
/// },
/// )
/// ```
class RoleDropdown extends StatelessWidget {
/// Creates a role dropdown
const RoleDropdown({
super.key,
required this.value,
required this.onChanged,
this.validator,
});
/// Selected role value
final String? value;
/// Callback when role changes
final void Function(String?) onChanged;
/// Form field validator
final String? Function(String?)? validator;
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<String>(
value: value,
decoration: InputDecoration(
hintText: 'Chọn vai trò của bạn',
hintStyle: const TextStyle(
fontSize: InputFieldSpecs.hintFontSize,
color: AppColors.grey500,
),
prefixIcon: const Icon(
Icons.work,
color: AppColors.primaryBlue,
size: AppIconSize.md,
),
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),
),
),
items: const [
DropdownMenuItem(value: 'dealer', child: Text('Đại lý hệ thống')),
DropdownMenuItem(
value: 'worker',
child: Text('Kiến trúc sư/ Thầu thợ'),
),
DropdownMenuItem(value: 'broker', child: Text('Khách lẻ')),
DropdownMenuItem(value: 'other', child: Text('Khác')),
],
onChanged: onChanged,
validator: validator,
icon: const Icon(Icons.arrow_drop_down, color: AppColors.grey500),
dropdownColor: AppColors.white,
style: const TextStyle(
fontSize: InputFieldSpecs.fontSize,
color: AppColors.grey900,
),
);
}
}