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

306 lines
9.2 KiB
Dart

/// 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;
}