415 lines
13 KiB
Dart
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: '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<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 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,
|
|
},
|
|
);
|
|
}
|
|
}
|