425 lines
13 KiB
Dart
425 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: 'Warehouse data is required',
|
|
);
|
|
}
|
|
|
|
return OperationSelectionPage(warehouse: warehouse);
|
|
},
|
|
),
|
|
|
|
/// Products List Route
|
|
/// Path: /products
|
|
/// Takes warehouse, warehouseName, and operationType as extra parameter
|
|
/// Shows products for selected warehouse and operation
|
|
GoRoute(
|
|
path: '/products',
|
|
name: 'products',
|
|
builder: (context, state) {
|
|
final params = state.extra as Map<String, dynamic>?;
|
|
|
|
if (params == null) {
|
|
// If no params, redirect to warehouses
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
context.go('/warehouses');
|
|
});
|
|
return const _ErrorScreen(
|
|
message: 'Product parameters are required',
|
|
);
|
|
}
|
|
|
|
// Extract required parameters
|
|
final warehouse = params['warehouse'] as WarehouseEntity?;
|
|
final warehouseName = params['warehouseName'] as String?;
|
|
final operationType = params['operationType'] as String?;
|
|
|
|
// Validate parameters
|
|
if (warehouse == null || warehouseName == null || operationType == null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
context.go('/warehouses');
|
|
});
|
|
return const _ErrorScreen(
|
|
message: 'Invalid product parameters',
|
|
);
|
|
}
|
|
|
|
return ProductsPage(
|
|
warehouseId: warehouse.id,
|
|
warehouseName: warehouseName,
|
|
operationType: operationType,
|
|
);
|
|
},
|
|
),
|
|
|
|
/// Product Detail Route
|
|
/// Path: /product-detail
|
|
/// Takes warehouseId, productId, and warehouseName as extra parameter
|
|
/// Shows detailed information for a specific product
|
|
GoRoute(
|
|
path: '/product-detail',
|
|
name: 'product-detail',
|
|
builder: (context, state) {
|
|
final params = state.extra as Map<String, dynamic>?;
|
|
|
|
if (params == null) {
|
|
// If no params, redirect to warehouses
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
context.go('/warehouses');
|
|
});
|
|
return const _ErrorScreen(
|
|
message: 'Product detail parameters are required',
|
|
);
|
|
}
|
|
|
|
// Extract required parameters
|
|
final warehouseId = params['warehouseId'] as int?;
|
|
final productId = params['productId'] as int?;
|
|
final warehouseName = params['warehouseName'] as String?;
|
|
|
|
// Validate parameters
|
|
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,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
|
|
// ==================== 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<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('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 goToProducts({
|
|
required WarehouseEntity warehouse,
|
|
required String operationType,
|
|
}) {
|
|
go(
|
|
'/products',
|
|
extra: {
|
|
'warehouse': warehouse,
|
|
'warehouseName': warehouse.name,
|
|
'operationType': operationType,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Navigate to product detail page
|
|
///
|
|
/// [warehouseId] - ID of the warehouse
|
|
/// [productId] - ID of the product to view
|
|
/// [warehouseName] - Name of the warehouse (for display)
|
|
void goToProductDetail({
|
|
required int warehouseId,
|
|
required int productId,
|
|
required String warehouseName,
|
|
}) {
|
|
push(
|
|
'/product-detail',
|
|
extra: {
|
|
'warehouseId': warehouseId,
|
|
'productId': productId,
|
|
'warehouseName': warehouseName,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 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',
|
|
extra: {
|
|
'warehouse': warehouse,
|
|
'warehouseName': warehouse.name,
|
|
'operationType': operationType,
|
|
},
|
|
);
|
|
}
|
|
}
|