/// 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:font_awesome_flutter/font_awesome_flutter.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 createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState { // Form key for validation final _formKey = GlobalKey(); // Controllers final _phoneController = TextEditingController(text: "0986788766"); final _passwordController = TextEditingController(text: "123456"); // Focus nodes final _phoneFocusNode = FocusNode(); final _passwordFocusNode = FocusNode(); // Remember me checkbox state bool _rememberMe = true; @override void dispose() { _phoneController.dispose(); _passwordController.dispose(); _phoneFocusNode.dispose(); _passwordFocusNode.dispose(); super.dispose(); } /// Handle login button press Future _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, rememberMe: _rememberMe, ); // Check if login was successful final authState = ref.read(authProvider) ..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 business unit selection (registration flow) void _navigateToRegister() { // Navigate to business unit selection page first context.pushNamed( RouteNames.businessUnitSelection, extra: {'isRegistrationFlow': true}, ); } /// Navigate to forgot password page void _navigateToForgotPassword() { context.pushNamed(RouteNames.forgotPassword); } /// Show support dialog void _showSupport() { showDialog( 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); final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colorScheme.surfaceContainerLowest, 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(colorScheme), const SizedBox(height: AppSpacing.xl), // Login Form Card _buildLoginForm(authState, isPasswordVisible, colorScheme), const SizedBox(height: AppSpacing.lg), // Register Link _buildRegisterLink(colorScheme), const SizedBox(height: AppSpacing.xl), // Support Link _buildSupportLink(colorScheme), ], ), ), ), ), ); } /// 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: Colors.white, fontSize: 32.0, fontWeight: FontWeight.w700, letterSpacing: 1.5, ), ), SizedBox(height: 4.0), Text( 'Worker App', style: TextStyle( color: Colors.white, fontSize: 12.0, letterSpacing: 0.5, ), ), ], ), ), ); } /// Build welcome message Widget _buildWelcomeMessage(ColorScheme colorScheme) { return Column( children: [ Text( 'Xin chào!', style: TextStyle( fontSize: 32.0, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), Text( 'Đăng nhập để tiếp tục', style: TextStyle(fontSize: 16.0, color: colorScheme.onSurfaceVariant), ), ], ); } /// Build login form card Widget _buildLoginForm( AsyncValue authState, bool isPasswordVisible, ColorScheme colorScheme, ) { final isLoading = authState.isLoading; return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: colorScheme.surface, 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: TextStyle( fontSize: InputFieldSpecs.fontSize, color: colorScheme.onSurface, ), decoration: InputDecoration( labelText: 'Mật khẩu', labelStyle: TextStyle( fontSize: InputFieldSpecs.labelFontSize, color: colorScheme.onSurfaceVariant, ), hintText: 'Nhập mật khẩu', hintStyle: TextStyle( fontSize: InputFieldSpecs.hintFontSize, color: colorScheme.onSurfaceVariant, ), prefixIcon: Icon( FontAwesomeIcons.lock, color: colorScheme.primary, size: AppIconSize.md, ), suffixIcon: IconButton( icon: Icon( isPasswordVisible ? FontAwesomeIcons.eye : FontAwesomeIcons.eyeSlash, color: colorScheme.onSurfaceVariant, size: AppIconSize.md, ), onPressed: () { ref.read(passwordVisibilityProvider.notifier).toggle(); }, ), filled: true, fillColor: colorScheme.surface, contentPadding: InputFieldSpecs.contentPadding, border: OutlineInputBorder( borderRadius: BorderRadius.circular( InputFieldSpecs.borderRadius, ), borderSide: BorderSide( color: colorScheme.surfaceContainerHighest, width: 1.0, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular( InputFieldSpecs.borderRadius, ), borderSide: BorderSide( color: colorScheme.surfaceContainerHighest, width: 1.0, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular( InputFieldSpecs.borderRadius, ), borderSide: BorderSide( color: colorScheme.primary, 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.sm), // Remember Me Checkbox & Forgot Password Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Remember Me Checkbox Row( children: [ Checkbox( value: _rememberMe, onChanged: isLoading ? null : (value) { setState(() { _rememberMe = value ?? false; }); }, activeColor: colorScheme.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4.0), ), ), GestureDetector( onTap: isLoading ? null : () { setState(() { _rememberMe = !_rememberMe; }); }, child: Text( 'Ghi nhớ đăng nhập', style: TextStyle( fontSize: 14.0, color: colorScheme.onSurfaceVariant, ), ), ), ], ), // Forgot Password Link GestureDetector( onTap: isLoading ? null : _navigateToForgotPassword, child: Text( 'Quên mật khẩu?', style: TextStyle( fontSize: 14.0, color: isLoading ? colorScheme.onSurfaceVariant : colorScheme.primary, fontWeight: FontWeight.w500, ), ), ), ], ), const SizedBox(height: AppSpacing.md), // Login Button SizedBox( height: ButtonSpecs.height, child: ElevatedButton( onPressed: isLoading ? null : _handleLogin, style: ElevatedButton.styleFrom( backgroundColor: colorScheme.primary, foregroundColor: colorScheme.onPrimary, disabledBackgroundColor: colorScheme.surfaceContainerHighest, disabledForegroundColor: colorScheme.onSurfaceVariant, elevation: ButtonSpecs.elevation, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius), ), ), child: isLoading ? SizedBox( height: 20.0, width: 20.0, child: CircularProgressIndicator( strokeWidth: 2.0, valueColor: AlwaysStoppedAnimation( colorScheme.onPrimary, ), ), ) : const Text( 'Đăng nhập', style: TextStyle( fontSize: ButtonSpecs.fontSize, fontWeight: ButtonSpecs.fontWeight, ), ), ), ), ], ), ); } /// Build register link Widget _buildRegisterLink(ColorScheme colorScheme) { return Center( child: RichText( text: TextSpan( text: 'Chưa có tài khoản? ', style: TextStyle(fontSize: 14.0, color: colorScheme.onSurfaceVariant), children: [ WidgetSpan( child: GestureDetector( onTap: _navigateToRegister, child: Text( 'Đăng ký ngay', style: TextStyle( fontSize: 14.0, color: colorScheme.primary, fontWeight: FontWeight.w500, decoration: TextDecoration.none, ), ), ), ), ], ), ), ); } /// Build support link Widget _buildSupportLink(ColorScheme colorScheme) { return Center( child: TextButton.icon( onPressed: _showSupport, icon: Icon( FontAwesomeIcons.headset, size: AppIconSize.sm, color: colorScheme.primary, ), label: Text( 'Hỗ trợ khách hàng', style: TextStyle( fontSize: 14.0, color: colorScheme.primary, fontWeight: FontWeight.w500, ), ), ), ); } }