add auth register
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
/// Matches design from html/register.html
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -12,12 +13,17 @@ 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/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/core/utils/validators.dart';
|
||||
import 'package:worker/features/auth/domain/entities/business_unit.dart';
|
||||
import 'package:worker/features/auth/domain/entities/city.dart';
|
||||
import 'package:worker/features/auth/domain/entities/customer_group.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/cities_provider.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/customer_groups_provider.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/session_provider.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
|
||||
///
|
||||
@@ -65,16 +71,60 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
final _companyFocus = FocusNode();
|
||||
|
||||
// State
|
||||
String? _selectedRole;
|
||||
String? _selectedCity;
|
||||
CustomerGroup? _selectedRole;
|
||||
City? _selectedCity;
|
||||
File? _idCardFile;
|
||||
File? _certificateFile;
|
||||
bool _termsAccepted = false;
|
||||
bool _passwordVisible = false;
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingData = true;
|
||||
bool _hasInitialized = false;
|
||||
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
/// Initialize session and load data
|
||||
/// This should be called from build method or after widget is mounted
|
||||
Future<void> _initializeData() async {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isLoadingData = true;
|
||||
_hasInitialized = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Step 1: Get session (public API user)
|
||||
await ref.read(sessionProvider.notifier).getSession();
|
||||
|
||||
// Step 2: Fetch cities and customer groups in parallel using the session
|
||||
await Future.wait([
|
||||
ref.read(citiesProvider.notifier).fetchCities(),
|
||||
ref.read(customerGroupsProvider.notifier).fetchCustomerGroups(),
|
||||
]);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Lỗi tải dữ liệu: $e'),
|
||||
backgroundColor: AppColors.danger,
|
||||
action: SnackBarAction(
|
||||
label: 'Thử lại',
|
||||
textColor: AppColors.white,
|
||||
onPressed: _initializeData,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingData = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fullNameController.dispose();
|
||||
@@ -95,8 +145,23 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
}
|
||||
|
||||
/// Check if verification section should be shown
|
||||
/// Note: This is based on the old role system
|
||||
/// TODO: Update this logic based on actual customer group requirements
|
||||
bool get _shouldShowVerification {
|
||||
return _selectedRole == 'worker' || _selectedRole == 'dealer';
|
||||
// For now, always hide verification section since we're using customer groups
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Convert file to base64 string
|
||||
Future<String?> _fileToBase64(File? file) async {
|
||||
if (file == null) return null;
|
||||
try {
|
||||
final bytes = await file.readAsBytes();
|
||||
return base64Encode(bytes);
|
||||
} catch (e) {
|
||||
debugPrint('Error converting file to base64: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick image from gallery or camera
|
||||
@@ -238,48 +303,60 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: Implement actual registration API call
|
||||
// Include widget.selectedBusinessUnit?.id in the API request
|
||||
// Example:
|
||||
// final result = await authRepository.register(
|
||||
// fullName: _fullNameController.text.trim(),
|
||||
// phone: _phoneController.text.trim(),
|
||||
// email: _emailController.text.trim(),
|
||||
// password: _passwordController.text,
|
||||
// role: _selectedRole,
|
||||
// businessUnitId: widget.selectedBusinessUnit?.id,
|
||||
// ...
|
||||
// );
|
||||
// Get session state for CSRF token and SID
|
||||
final sessionState = ref.read(sessionProvider);
|
||||
|
||||
// For now, simulate API delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (!sessionState.hasSession) {
|
||||
throw Exception('Session không hợp lệ. Vui lòng thử lại.');
|
||||
}
|
||||
|
||||
// Convert files to base64
|
||||
final idCardFrontBase64 = await _fileToBase64(_idCardFile);
|
||||
final certificatesBase64 = _certificateFile != null
|
||||
? [await _fileToBase64(_certificateFile)]
|
||||
: <String?>[];
|
||||
|
||||
// Remove null values from certificates list
|
||||
final validCertificates = certificatesBase64
|
||||
.whereType<String>()
|
||||
.toList();
|
||||
|
||||
// Call registration API
|
||||
final dataSource = ref.read(authRemoteDataSourceProvider);
|
||||
final response = await dataSource.register(
|
||||
csrfToken: sessionState.csrfToken!,
|
||||
sid: sessionState.sid!,
|
||||
fullName: _fullNameController.text.trim(),
|
||||
phone: _phoneController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
customerGroupCode: _selectedRole?.value ?? _selectedRole?.name ?? '',
|
||||
cityCode: _selectedCity?.code ?? '',
|
||||
companyName: _companyController.text.trim().isEmpty
|
||||
? null
|
||||
: _companyController.text.trim(),
|
||||
taxCode: _taxCodeController.text.trim().isEmpty
|
||||
? null
|
||||
: _taxCodeController.text.trim(),
|
||||
idCardFrontBase64: idCardFrontBase64,
|
||||
idCardBackBase64: null, // Not collecting back side in current UI
|
||||
certificatesBase64: validCertificates,
|
||||
);
|
||||
|
||||
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,
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Đăng ký thành công!',
|
||||
// response['message']?.toString() ?? 'Đăng ký thành công!',
|
||||
),
|
||||
);
|
||||
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();
|
||||
}
|
||||
duration: Duration(seconds: 1),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
|
||||
Future<void>.delayed(const Duration(seconds: 1)).then((_) => context.goLogin());
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
@@ -301,6 +378,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Initialize data on first build
|
||||
if (!_hasInitialized) {
|
||||
// Use addPostFrameCallback to avoid calling setState during build
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initializeData();
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
appBar: AppBar(
|
||||
@@ -320,8 +405,22 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Form(
|
||||
body: _isLoadingData
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
'Đang tải dữ liệu...',
|
||||
style: TextStyle(color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SafeArea(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
@@ -442,29 +541,9 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Role Selection
|
||||
// Role Selection (Customer Groups)
|
||||
_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;
|
||||
},
|
||||
),
|
||||
_buildCustomerGroupDropdown(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Verification Section (conditional)
|
||||
@@ -488,47 +567,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
|
||||
// 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;
|
||||
},
|
||||
),
|
||||
_buildCityDropdown(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Terms and Conditions
|
||||
@@ -648,8 +687,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -712,6 +751,143 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Build customer group dropdown
|
||||
Widget _buildCustomerGroupDropdown() {
|
||||
final customerGroupsAsync = ref.watch(customerGroupsProvider);
|
||||
|
||||
return customerGroupsAsync.when(
|
||||
data: (groups) {
|
||||
return DropdownButtonFormField<CustomerGroup>(
|
||||
value: _selectedRole,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Chọn vai trò',
|
||||
prefixIcon: Icons.work,
|
||||
),
|
||||
items: groups
|
||||
.map(
|
||||
(group) => DropdownMenuItem(
|
||||
value: group,
|
||||
child: Text(group.customerGroupName),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
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) {
|
||||
return 'Vui lòng chọn vai trò';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox(
|
||||
height: 48,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (error, stack) => Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.danger.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
border: Border.all(color: AppColors.danger),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: AppColors.danger, size: 20),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Lỗi tải vai trò',
|
||||
style: const TextStyle(color: AppColors.danger, fontSize: 12),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _initializeData,
|
||||
child: const Text('Thử lại', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build city dropdown
|
||||
Widget _buildCityDropdown() {
|
||||
final citiesAsync = ref.watch(citiesProvider);
|
||||
|
||||
return citiesAsync.when(
|
||||
data: (cities) {
|
||||
return DropdownButtonFormField<City>(
|
||||
value: _selectedCity,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Chọn tỉnh/thành phố',
|
||||
prefixIcon: Icons.location_city,
|
||||
),
|
||||
items: cities
|
||||
.map(
|
||||
(city) => DropdownMenuItem(
|
||||
value: city,
|
||||
child: Text(city.cityName),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedCity = value;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return 'Vui lòng chọn tỉnh/thành phố';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox(
|
||||
height: 48,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (error, stack) => Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.danger.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
border: Border.all(color: AppColors.danger),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: AppColors.danger, size: 20),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Lỗi tải danh sách tỉnh/thành phố',
|
||||
style: const TextStyle(color: AppColors.danger, fontSize: 12),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _initializeData,
|
||||
child: const Text('Thử lại', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build verification section
|
||||
Widget _buildVerificationSection() {
|
||||
return Container(
|
||||
|
||||
Reference in New Issue
Block a user