Files
minhthu/lib/core/router/app_router.dart
2025-10-28 23:56:47 +07:00

415 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/auth/presentation/pages/login_page.dart';
import '../../features/auth/di/auth_dependency_injection.dart';
import '../../features/warehouse/presentation/pages/warehouse_selection_page.dart';
import '../../features/operation/presentation/pages/operation_selection_page.dart';
import '../../features/products/presentation/pages/products_page.dart';
import '../../features/products/presentation/pages/product_detail_page.dart';
import '../../features/warehouse/domain/entities/warehouse_entity.dart';
import '../storage/secure_storage.dart';
/// Application router configuration using GoRouter
///
/// Implements authentication-based redirect logic:
/// - Unauthenticated users are redirected to /login
/// - Authenticated users on /login are redirected to /warehouses
/// - Proper parameter passing between routes
///
/// App Flow: Login → Warehouses → Operations → Products
class AppRouter {
final Ref ref;
final SecureStorage secureStorage;
AppRouter({
required this.ref,
required this.secureStorage,
});
late final GoRouter router = GoRouter(
debugLogDiagnostics: true,
initialLocation: '/login',
refreshListenable: GoRouterRefreshStream(ref),
redirect: _handleRedirect,
routes: [
// ==================== Auth Routes ====================
/// Login Route
/// Path: /login
/// Initial route for unauthenticated users
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginPage(),
),
// ==================== Main App Routes ====================
/// Warehouse Selection Route
/// Path: /warehouses
/// Shows list of available warehouses after login
GoRoute(
path: '/warehouses',
name: 'warehouses',
builder: (context, state) => const WarehouseSelectionPage(),
),
/// Operation Selection Route
/// Path: /operations
/// Takes warehouse data as extra parameter
/// Shows Import/Export operation options for selected warehouse
GoRoute(
path: '/operations',
name: 'operations',
builder: (context, state) {
final warehouse = state.extra as WarehouseEntity?;
if (warehouse == null) {
// If no warehouse data, redirect to warehouses
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go('/warehouses');
});
return const _ErrorScreen(
message: 'Yêu cầu dữ liệu kho',
);
}
return OperationSelectionPage(warehouse: warehouse);
},
),
/// Products List Route
/// Path: /products/:warehouseId/:operationType
/// Query params: name (warehouse name)
/// Shows products for selected warehouse and operation
GoRoute(
path: '/products/:warehouseId/:operationType',
name: 'products',
builder: (context, state) {
// Extract path parameters
final warehouseIdStr = state.pathParameters['warehouseId'];
final operationType = state.pathParameters['operationType'];
// Extract query parameter
final warehouseName = state.uri.queryParameters['name'];
// Parse and validate parameters
final warehouseId = int.tryParse(warehouseIdStr ?? '');
if (warehouseId == null || warehouseName == null || operationType == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go('/warehouses');
});
return const _ErrorScreen(
message: 'Tham số sản phẩm không hợp lệ',
);
}
return ProductsPage(
warehouseId: warehouseId,
warehouseName: warehouseName,
operationType: operationType,
);
},
),
/// Product Detail Route
/// Path: /product-detail/:warehouseId/:productId/:operationType
/// Query params: name (warehouse name), stageId (optional)
/// Shows detailed information for a specific product
/// If stageId is provided, only that stage is shown, otherwise all stages are shown
GoRoute(
path: '/product-detail/:warehouseId/:productId/:operationType',
name: 'product-detail',
builder: (context, state) {
// Extract path parameters
final warehouseIdStr = state.pathParameters['warehouseId'];
final productIdStr = state.pathParameters['productId'];
final operationType = state.pathParameters['operationType'] ?? 'import';
// Extract query parameters
final warehouseName = state.uri.queryParameters['name'];
final stageIdStr = state.uri.queryParameters['stageId'];
// Parse and validate parameters
final warehouseId = int.tryParse(warehouseIdStr ?? '');
final productId = int.tryParse(productIdStr ?? '');
final stageId = stageIdStr != null ? int.tryParse(stageIdStr) : null;
if (warehouseId == null || productId == null || warehouseName == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go('/warehouses');
});
return const _ErrorScreen(
message: 'Tham số chi tiết sản phẩm không hợp lệ',
);
}
return ProductDetailPage(
warehouseId: warehouseId,
productId: productId,
warehouseName: warehouseName,
stageId: stageId,
operationType: operationType,
);
},
),
],
// ==================== Error Handling ====================
errorBuilder: (context, state) {
return Scaffold(
appBar: AppBar(
title: const Text('Không tìm thấy trang'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
'Không tìm thấy trang',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
'Trang "${state.uri.path}" không tồn tại.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/login'),
child: const Text('Về trang đăng nhập'),
),
],
),
),
);
},
);
/// Handle global redirect logic based on authentication status
///
/// Redirect rules:
/// 1. Check authentication status using SecureStorage
/// 2. If not authenticated and not on login page → redirect to /login
/// 3. If authenticated and on login page → redirect to /warehouses
/// 4. Otherwise, allow navigation
Future<String?> _handleRedirect(
BuildContext context,
GoRouterState state,
) async {
try {
// Check if user has access token
final isAuthenticated = await secureStorage.isAuthenticated();
final isOnLoginPage = state.matchedLocation == '/login';
// User is not authenticated
if (!isAuthenticated) {
// Allow access to login page
if (isOnLoginPage) {
return null;
}
// Redirect to login for all other pages
return '/login';
}
// User is authenticated
if (isAuthenticated) {
// Redirect away from login page to warehouses
if (isOnLoginPage) {
return '/warehouses';
}
// Allow access to all other pages
return null;
}
return null;
} catch (e) {
// On error, redirect to login for safety
debugPrint('Error in redirect: $e');
return '/login';
}
}
}
/// Provider for AppRouter
///
/// Creates and provides the GoRouter instance with dependencies
final appRouterProvider = Provider<GoRouter>((ref) {
final secureStorage = SecureStorage();
final appRouter = AppRouter(
ref: ref,
secureStorage: secureStorage,
);
return appRouter.router;
});
/// Helper class to refresh router when auth state changes
///
/// This allows GoRouter to react to authentication state changes
/// and re-evaluate redirect logic
class GoRouterRefreshStream extends ChangeNotifier {
final Ref ref;
GoRouterRefreshStream(this.ref) {
// Listen to auth state changes
// When auth state changes, notify GoRouter to re-evaluate redirects
ref.listen(
authProvider,
(_, __) => notifyListeners(),
);
}
}
/// Error screen widget for route parameter validation errors
class _ErrorScreen extends StatelessWidget {
final String message;
const _ErrorScreen({required this.message});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Lỗi'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
'Lỗi điều hướng',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
message,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/warehouses'),
child: const Text('Về trang kho'),
),
],
),
),
);
}
}
/// Extension methods for easier type-safe navigation
///
/// Usage:
/// ```dart
/// context.goToLogin();
/// context.goToWarehouses();
/// context.goToOperations(warehouse);
/// context.goToProducts(warehouse, 'import');
/// ```
extension AppRouterExtension on BuildContext {
/// Navigate to login page
void goToLogin() => go('/login');
/// Navigate to warehouses list
void goToWarehouses() => go('/warehouses');
/// Navigate to operation selection with warehouse data
void goToOperations(WarehouseEntity warehouse) {
go('/operations', extra: warehouse);
}
/// Navigate to products list with required parameters
///
/// [warehouse] - Selected warehouse entity
/// [operationType] - Either 'import' or 'export'
void pushToProducts({
required WarehouseEntity warehouse,
required String operationType,
}) {
push('/products/${warehouse.id}/$operationType?name=${Uri.encodeQueryComponent(warehouse.name)}');
}
/// Navigate to product detail page
///
/// [warehouseId] - ID of the warehouse
/// [productId] - ID of the product to view
/// [warehouseName] - Name of the warehouse (for display)
/// [operationType] - Either 'import' or 'export'
/// [stageId] - Optional ID of specific stage to show (if null, show all stages)
void goToProductDetail({
required int warehouseId,
required int productId,
required String warehouseName,
required String operationType,
int? stageId,
}) {
final queryParams = <String, String>{
'name': warehouseName,
if (stageId != null) 'stageId': stageId.toString(),
};
final queryString = queryParams.entries
.map((e) => '${e.key}=${Uri.encodeQueryComponent(e.value)}')
.join('&');
push('/product-detail/$warehouseId/$productId/$operationType?$queryString');
}
/// Pop current route
void goBack() => pop();
}
/// Extension for named route navigation
extension AppRouterNamedExtension on BuildContext {
/// Navigate to login page using named route
void goToLoginNamed() => goNamed('login');
/// Navigate to warehouses using named route
void goToWarehousesNamed() => goNamed('warehouses');
/// Navigate to operations using named route with warehouse
void goToOperationsNamed(WarehouseEntity warehouse) {
goNamed('operations', extra: warehouse);
}
/// Navigate to products using named route with parameters
void goToProductsNamed({
required WarehouseEntity warehouse,
required String operationType,
}) {
goNamed(
'products',
pathParameters: {
'warehouseId': warehouse.id.toString(),
'operationType': operationType,
},
queryParameters: {
'name': warehouse.name,
},
);
}
}