add start/business unit
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
/// Business Unit Selection Page
|
||||
///
|
||||
/// Allows users to select a business unit during registration or after login
|
||||
/// when they have access to multiple units.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.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/features/auth/domain/entities/business_unit.dart';
|
||||
|
||||
/// Business Unit Selection Page
|
||||
///
|
||||
/// Flow:
|
||||
/// 1. During registration: User selects unit before going to register page
|
||||
/// 2. After login: User selects unit if they have multiple units
|
||||
class BusinessUnitSelectionPage extends StatefulWidget {
|
||||
const BusinessUnitSelectionPage({
|
||||
super.key,
|
||||
this.businessUnits,
|
||||
this.isRegistrationFlow = false,
|
||||
this.onUnitSelected,
|
||||
});
|
||||
|
||||
/// List of available business units
|
||||
final List<BusinessUnit>? businessUnits;
|
||||
|
||||
/// Whether this is part of registration flow
|
||||
final bool isRegistrationFlow;
|
||||
|
||||
/// Callback when business unit is selected (for registration flow)
|
||||
final void Function(BusinessUnit)? onUnitSelected;
|
||||
|
||||
@override
|
||||
State<BusinessUnitSelectionPage> createState() =>
|
||||
_BusinessUnitSelectionPageState();
|
||||
}
|
||||
|
||||
class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
BusinessUnit? _selectedUnit;
|
||||
late List<BusinessUnit> _availableUnits;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Use provided units or mock data for registration flow
|
||||
_availableUnits = widget.businessUnits ?? _getMockBusinessUnits();
|
||||
}
|
||||
|
||||
/// Mock business units for registration flow
|
||||
/// TODO: Replace with actual API data when backend is ready
|
||||
List<BusinessUnit> _getMockBusinessUnits() {
|
||||
return [
|
||||
const BusinessUnit(
|
||||
id: '1',
|
||||
code: 'VIKD',
|
||||
name: 'VIKD',
|
||||
description: 'Đơn vị kinh doanh VIKD',
|
||||
),
|
||||
const BusinessUnit(
|
||||
id: '2',
|
||||
code: 'HSKD',
|
||||
name: 'HSKD',
|
||||
description: 'Đơn vị kinh doanh HSKD',
|
||||
),
|
||||
const BusinessUnit(
|
||||
id: '3',
|
||||
code: 'LPKD',
|
||||
name: 'LPKD',
|
||||
description: 'Đơn vị kinh doanh LPKD',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _handleContinue() {
|
||||
if (_selectedUnit == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Vui lòng chọn đơn vị kinh doanh'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.isRegistrationFlow) {
|
||||
// Registration flow: pass selected unit to register page
|
||||
widget.onUnitSelected?.call(_selectedUnit!);
|
||||
context.pushNamed(
|
||||
RouteNames.register,
|
||||
extra: {'businessUnit': _selectedUnit},
|
||||
);
|
||||
} else {
|
||||
// Login flow: save selected unit and navigate to home
|
||||
// TODO: Save selected unit to local storage/state
|
||||
context.goNamed(RouteNames.home);
|
||||
}
|
||||
}
|
||||
|
||||
void _showInfoDialog() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Đơn vị kinh doanh'),
|
||||
content: const Text(
|
||||
'Chọn đơn vị kinh doanh mà bạn muốn truy cập. '
|
||||
'Bạn có thể thay đổi đơn vị sau khi đăng nhập.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Đóng'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.white,
|
||||
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(
|
||||
'Đơn vị kinh doanh',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline, color: Colors.black),
|
||||
onPressed: _showInfoDialog,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Logo Section
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppColors.primaryBlue, AppColors.lightBlue],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'DBIZ',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Worker App',
|
||||
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Welcome Message
|
||||
const Text(
|
||||
'Chọn đơn vị kinh doanh để tiếp tục',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: AppColors.grey500, fontSize: 14),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Business Unit Selection List
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm),
|
||||
child: Text(
|
||||
'Đơn vị kinh doanh',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
// Business Unit List Tiles
|
||||
...(_availableUnits.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final unit = entry.value;
|
||||
final isSelected = _selectedUnit?.id == unit.id;
|
||||
final isFirst = index == 0;
|
||||
final isLast = index == _availableUnits.length - 1;
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: isLast ? 0 : AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue
|
||||
: AppColors.grey100,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: isFirst
|
||||
? const Radius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
)
|
||||
: Radius.zero,
|
||||
bottom: isLast
|
||||
? const Radius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
)
|
||||
: Radius.zero,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryBlue.withValues(
|
||||
alpha: 0.1,
|
||||
),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedUnit = unit;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: isFirst
|
||||
? const Radius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
)
|
||||
: Radius.zero,
|
||||
bottom: isLast
|
||||
? const Radius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
)
|
||||
: Radius.zero,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icon
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue.withValues(
|
||||
alpha: 0.1,
|
||||
)
|
||||
: AppColors.grey50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.business,
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue
|
||||
: AppColors.grey500,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
// Unit Name
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
unit.name,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue
|
||||
: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
if (unit.description != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
unit.description!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
// Radio indicator
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue
|
||||
: AppColors.grey500,
|
||||
width: 2,
|
||||
),
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue
|
||||
: Colors.transparent,
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(
|
||||
Icons.circle,
|
||||
size: 10,
|
||||
color: AppColors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList()),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Continue Button
|
||||
SizedBox(
|
||||
height: ButtonSpecs.height,
|
||||
child: ElevatedButton(
|
||||
onPressed: _handleContinue,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
ButtonSpecs.borderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Tiếp tục',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -115,15 +115,12 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigate to register page
|
||||
/// Navigate to business unit selection (registration flow)
|
||||
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,
|
||||
),
|
||||
// Navigate to business unit selection page first
|
||||
context.pushNamed(
|
||||
RouteNames.businessUnitSelection,
|
||||
extra: {'isRegistrationFlow': true},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ 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/domain/entities/business_unit.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';
|
||||
@@ -29,10 +30,13 @@ import 'package:worker/features/auth/presentation/widgets/role_dropdown.dart';
|
||||
/// - Terms and conditions checkbox
|
||||
///
|
||||
/// Navigation:
|
||||
/// - From: Login page
|
||||
/// - From: Business unit selection page
|
||||
/// - To: OTP verification (broker/other) or pending approval (worker/dealer)
|
||||
class RegisterPage extends ConsumerStatefulWidget {
|
||||
const RegisterPage({super.key});
|
||||
/// Selected business unit from previous screen
|
||||
final BusinessUnit? selectedBusinessUnit;
|
||||
|
||||
const RegisterPage({super.key, this.selectedBusinessUnit});
|
||||
|
||||
@override
|
||||
ConsumerState<RegisterPage> createState() => _RegisterPageState();
|
||||
@@ -235,6 +239,18 @@ 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,
|
||||
// ...
|
||||
// );
|
||||
|
||||
// For now, simulate API delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user