This commit is contained in:
2025-10-28 00:09:46 +07:00
parent 9ebe7c2919
commit de49f564b1
110 changed files with 15392 additions and 3996 deletions

View File

@@ -0,0 +1,150 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../di/auth_dependency_injection.dart';
import '../widgets/login_form.dart';
/// Login page for user authentication
///
/// Displays login form and handles authentication flow
class LoginPage extends ConsumerStatefulWidget {
const LoginPage({super.key});
@override
ConsumerState<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends ConsumerState<LoginPage> {
@override
void initState() {
super.initState();
// Check authentication status on page load
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkAuthStatus();
});
}
/// Check if user is already authenticated
Future<void> _checkAuthStatus() async {
ref.read(authProvider.notifier).checkAuthStatus();
}
/// Handle login button press
void _handleLogin(String username, String password) {
ref.read(authProvider.notifier).login(username, password);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final authState = ref.watch(authProvider);
final isLoading = authState.isLoading;
final error = authState.error;
// Listen for authentication state changes
ref.listen(authProvider, (previous, next) {
if (next.isAuthenticated) {
// Navigate to warehouses page on successful login
context.go('/warehouses');
}
});
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// App logo/icon
Icon(
Icons.warehouse_outlined,
size: 80,
color: theme.colorScheme.primary,
),
const SizedBox(height: 24),
// App title
Text(
'Warehouse Manager',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
// Subtitle
Text(
'Login to continue',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
// Error message (show before form)
if (error != null)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: theme.colorScheme.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
error,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.error,
),
),
),
],
),
),
if (error != null) const SizedBox(height: 16),
// Login form (includes button)
LoginForm(
onSubmit: _handleLogin,
isLoading: isLoading,
),
const SizedBox(height: 24),
// Additional info or version
Text(
'Version 1.0.0',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.4),
),
textAlign: TextAlign.center,
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,12 @@
/// Barrel file for auth presentation layer exports
///
/// Provides clean imports for presentation layer components
// Pages
export 'pages/login_page.dart';
// Providers
export 'providers/auth_provider.dart';
// Widgets
export 'widgets/login_form.dart';

View File

@@ -0,0 +1,190 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/models/login_request_model.dart';
import '../../domain/entities/user_entity.dart';
import '../../domain/usecases/login_usecase.dart';
/// Authentication state
///
/// Represents the current authentication status and user data
class AuthState extends Equatable {
/// Current authenticated user (null if not authenticated)
final UserEntity? user;
/// Whether user is authenticated
final bool isAuthenticated;
/// Whether an authentication operation is in progress
final bool isLoading;
/// Error message if authentication fails
final String? error;
const AuthState({
this.user,
this.isAuthenticated = false,
this.isLoading = false,
this.error,
});
/// Initial state (not authenticated, not loading)
const AuthState.initial()
: user = null,
isAuthenticated = false,
isLoading = false,
error = null;
/// Loading state
const AuthState.loading()
: user = null,
isAuthenticated = false,
isLoading = true,
error = null;
/// Authenticated state with user data
const AuthState.authenticated(UserEntity user)
: user = user,
isAuthenticated = true,
isLoading = false,
error = null;
/// Error state
const AuthState.error(String message)
: user = null,
isAuthenticated = false,
isLoading = false,
error = message;
/// Create a copy with modified fields
AuthState copyWith({
UserEntity? user,
bool? isAuthenticated,
bool? isLoading,
String? error,
}) {
return AuthState(
user: user ?? this.user,
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
@override
List<Object?> get props => [user, isAuthenticated, isLoading, error];
@override
String toString() {
return 'AuthState(isAuthenticated: $isAuthenticated, isLoading: $isLoading, error: $error, user: $user)';
}
}
/// Auth state notifier that manages authentication state
///
/// Handles login, logout, and authentication status checks
class AuthNotifier extends StateNotifier<AuthState> {
final LoginUseCase loginUseCase;
final LogoutUseCase logoutUseCase;
final CheckAuthStatusUseCase checkAuthStatusUseCase;
final GetCurrentUserUseCase getCurrentUserUseCase;
AuthNotifier({
required this.loginUseCase,
required this.logoutUseCase,
required this.checkAuthStatusUseCase,
required this.getCurrentUserUseCase,
}) : super(const AuthState.initial());
/// Login with username and password
///
/// Updates state to loading, then either authenticated or error
Future<void> login(String username, String password) async {
// Set loading state
state = const AuthState.loading();
// Create login request
final request = LoginRequestModel(
username: username,
password: password,
);
// Call login use case
final result = await loginUseCase(request);
// Handle result
result.fold(
(failure) {
// Login failed - set error state
state = AuthState.error(failure.message);
},
(user) {
// Login successful - set authenticated state
state = AuthState.authenticated(user);
},
);
}
/// Logout current user
///
/// Clears authentication data and returns to initial state
Future<void> logout() async {
// Set loading state
state = state.copyWith(isLoading: true, error: null);
// Call logout use case
final result = await logoutUseCase();
// Handle result
result.fold(
(failure) {
// Logout failed - but still reset to initial state
// (local data should be cleared even if API call fails)
state = const AuthState.initial();
},
(_) {
// Logout successful - reset to initial state
state = const AuthState.initial();
},
);
}
/// Check authentication status on app start
///
/// Loads user data from storage if authenticated
Future<void> checkAuthStatus() async {
// Check if user is authenticated
final isAuthenticated = await checkAuthStatusUseCase();
if (isAuthenticated) {
// Try to load user data
final result = await getCurrentUserUseCase();
result.fold(
(failure) {
// Failed to load user data - reset to initial state
state = const AuthState.initial();
},
(user) {
// User data loaded - set authenticated state
state = AuthState.authenticated(user);
},
);
} else {
// Not authenticated - initial state
state = const AuthState.initial();
}
}
/// Clear error message
void clearError() {
if (state.error != null) {
state = state.copyWith(error: null);
}
}
/// Reset to initial state
void reset() {
state = const AuthState.initial();
}
}

View File

@@ -0,0 +1,202 @@
import 'package:flutter/material.dart';
/// Reusable login form widget with validation
///
/// Handles username and password input with proper validation
class LoginForm extends StatefulWidget {
/// Callback when login button is pressed
final void Function(String username, String password) onSubmit;
/// Whether the form is in loading state
final bool isLoading;
const LoginForm({
super.key,
required this.onSubmit,
this.isLoading = false,
});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController(text: "yesterday305@gmail.com");
final _passwordController = TextEditingController(text: '123456');
bool _obscurePassword = true;
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
void _handleSubmit() {
// Validate form
if (_formKey.currentState?.validate() ?? false) {
// Call submit callback
widget.onSubmit(
_usernameController.text.trim(),
_passwordController.text,
);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Username field
TextFormField(
controller: _usernameController,
enabled: !widget.isLoading,
decoration: InputDecoration(
labelText: 'Username',
hintText: 'Enter your username',
prefixIcon: const Icon(Icons.person_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Username is required';
}
if (value.trim().length < 3) {
return 'Username must be at least 3 characters';
}
return null;
},
),
const SizedBox(height: 16),
// Password field
TextFormField(
controller: _passwordController,
enabled: !widget.isLoading,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _handleSubmit(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
},
),
const SizedBox(height: 24),
// Login button
FilledButton.icon(
onPressed: widget.isLoading ? null : _handleSubmit,
icon: widget.isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.login),
label: Text(widget.isLoading ? 'Logging in...' : 'Login'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
);
}
}
/// Simple text field widget for login forms
class LoginTextField extends StatelessWidget {
final TextEditingController controller;
final String label;
final String hint;
final IconData? prefixIcon;
final bool obscureText;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final String? Function(String?)? validator;
final void Function(String)? onFieldSubmitted;
final bool enabled;
final Widget? suffixIcon;
const LoginTextField({
super.key,
required this.controller,
required this.label,
required this.hint,
this.prefixIcon,
this.obscureText = false,
this.keyboardType,
this.textInputAction,
this.validator,
this.onFieldSubmitted,
this.enabled = true,
this.suffixIcon,
});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
enabled: enabled,
obscureText: obscureText,
keyboardType: keyboardType,
textInputAction: textInputAction,
onFieldSubmitted: onFieldSubmitted,
decoration: InputDecoration(
labelText: label,
hintText: hint,
prefixIcon: prefixIcon != null ? Icon(prefixIcon) : null,
suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: enabled
? Theme.of(context).colorScheme.surface
: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
),
validator: validator,
);
}
}