Files
worker/lib/features/auth/presentation/pages/login_page.dart
2025-11-07 11:52:06 +07:00

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