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: 'Warehouse data is required', ); } 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: 'Invalid product parameters', ); } 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: 'Invalid product detail parameters', ); } return ProductDetailPage( warehouseId: warehouseId, productId: productId, warehouseName: warehouseName, stageId: stageId, operationType: operationType, ); }, ), ], // ==================== Error Handling ==================== errorBuilder: (context, state) { return Scaffold( appBar: AppBar( title: const Text('Page Not Found'), ), 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( 'Page Not Found', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 8), Text( 'The page "${state.uri.path}" does not exist.', 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('Go to Login'), ), ], ), ), ); }, ); /// 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 _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((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('Error'), ), 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( 'Navigation Error', 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('Go to Warehouses'), ), ], ), ), ); } } /// 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 = { '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, }, ); } }