493 lines
15 KiB
Dart
493 lines
15 KiB
Dart
/// 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,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|