add start/business unit

This commit is contained in:
Phuoc Nguyen
2025-11-07 13:56:51 +07:00
parent 3803bd26e0
commit ce7396f729
9 changed files with 687 additions and 12 deletions

View File

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

View File

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

View File

@@ -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));