Implements complete refactor of Ionic Angular logistics app to Flutter/Dart with: - Svrnty dark mode console theme (Material Design 3) - Responsive layouts (mobile, tablet, desktop) following FRONTEND standards - CQRS API integration with Result<T> error handling - OAuth2/OIDC authentication support (mocked for initial testing) - Delivery route and delivery management features - Multi-language support (EN/FR) with i18n - Native integrations (camera, phone calls, maps) - Strict typing throughout codebase - Mock data for UI testing without backend Follows all FRONTEND style guides, design patterns, and conventions. App is running in dark mode and fully responsive across all device sizes. Co-Authored-By: Claude <noreply@anthropic.com>
184 lines
5.8 KiB
Dart
184 lines
5.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../models/delivery_route.dart';
|
|
import '../providers/providers.dart';
|
|
import '../utils/breakpoints.dart';
|
|
import '../utils/responsive.dart';
|
|
import 'deliveries_page.dart';
|
|
import 'settings_page.dart';
|
|
|
|
class RoutesPage extends ConsumerWidget {
|
|
const RoutesPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final routesData = ref.watch(deliveryRoutesProvider);
|
|
final userProfile = ref.watch(userProfileProvider);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Delivery Routes'),
|
|
elevation: 0,
|
|
actions: [
|
|
userProfile.when(
|
|
data: (profile) => PopupMenuButton<String>(
|
|
onSelected: (value) {
|
|
if (value == 'settings') {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (context) => const SettingsPage(),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
itemBuilder: (BuildContext context) => [
|
|
PopupMenuItem(
|
|
value: 'profile',
|
|
child: Text(profile?.fullName ?? 'User'),
|
|
enabled: false,
|
|
),
|
|
const PopupMenuDivider(),
|
|
const PopupMenuItem(
|
|
value: 'settings',
|
|
child: Text('Settings'),
|
|
),
|
|
],
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: Center(
|
|
child: Text(
|
|
profile?.fullName ?? 'User',
|
|
style: Theme.of(context).textTheme.titleSmall,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
loading: () => const Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
),
|
|
error: (error, stackTrace) => const SizedBox(),
|
|
),
|
|
],
|
|
),
|
|
body: routesData.when(
|
|
data: (routes) {
|
|
if (routes.isEmpty) {
|
|
return const Center(
|
|
child: Text('No routes available'),
|
|
);
|
|
}
|
|
return RefreshIndicator(
|
|
onRefresh: () async {
|
|
// ignore: unused_result
|
|
ref.refresh(deliveryRoutesProvider);
|
|
},
|
|
child: context.isDesktop
|
|
? _buildDesktopGrid(context, routes)
|
|
: _buildMobileList(context, routes),
|
|
);
|
|
},
|
|
loading: () => const Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
error: (error, stackTrace) => Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text('Error: $error'),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: () => ref.refresh(deliveryRoutesProvider),
|
|
child: const Text('Retry'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMobileList(BuildContext context, List<DeliveryRoute> routes) {
|
|
final spacing = ResponsiveSpacing.md(context);
|
|
return ListView.builder(
|
|
padding: EdgeInsets.all(ResponsiveSpacing.md(context)),
|
|
itemCount: routes.length,
|
|
itemBuilder: (context, index) {
|
|
final route = routes[index];
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: spacing),
|
|
child: _buildRouteCard(context, route),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildDesktopGrid(BuildContext context, List<DeliveryRoute> routes) {
|
|
final spacing = ResponsiveSpacing.lg(context);
|
|
final columns = context.isTablet ? 2 : 3;
|
|
return GridView.builder(
|
|
padding: EdgeInsets.all(spacing),
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: columns,
|
|
crossAxisSpacing: spacing,
|
|
mainAxisSpacing: spacing,
|
|
childAspectRatio: 1.2,
|
|
),
|
|
itemCount: routes.length,
|
|
itemBuilder: (context, index) {
|
|
final route = routes[index];
|
|
return _buildRouteCard(context, route);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildRouteCard(BuildContext context, DeliveryRoute route) {
|
|
return Card(
|
|
child: InkWell(
|
|
onTap: () {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (context) => DeliveriesPage(
|
|
routeFragmentId: route.routeFragmentId,
|
|
routeName: route.name,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
child: Padding(
|
|
padding: EdgeInsets.all(ResponsiveSpacing.md(context)),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
route.name,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: ResponsiveSpacing.sm(context)),
|
|
Text(
|
|
'${route.completedDeliveries}/${route.totalDeliveries} completed',
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
SizedBox(height: ResponsiveSpacing.md(context)),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: LinearProgressIndicator(
|
|
value: route.progress,
|
|
minHeight: 8,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|