Implement UI/UX enhancements with collapsible routes sidebar and glassmorphic route cards

Adds new components (CollapsibleRoutesSidebar, GlassmorphicRouteCard) and
internationalization support. Updates deliveries and routes pages with improved
navigation and visual presentation using Material Design 3 principles.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jean-Philippe Brule
2025-11-15 17:55:18 -05:00
parent 6e6d279d77
commit 5714fd8443
8 changed files with 1463 additions and 129 deletions
+188 -71
View File
@@ -2,15 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:url_launcher/url_launcher.dart';
import '../models/delivery.dart';
import '../models/delivery_route.dart';
import '../providers/providers.dart';
import '../api/client.dart';
import '../api/openapi_config.dart';
import '../models/delivery_commands.dart';
import '../utils/breakpoints.dart';
import '../utils/responsive.dart';
import '../components/map_sidebar_layout.dart';
import '../components/dark_mode_map.dart';
import '../components/delivery_list_item.dart';
import '../components/collapsible_routes_sidebar.dart'
show CollapsibleRoutesSidebar;
class DeliveriesPage extends ConsumerStatefulWidget {
final int routeFragmentId;
@@ -46,6 +48,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
@override
Widget build(BuildContext context) {
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
final routesData = ref.watch(deliveryRoutesProvider);
final token = ref.watch(authTokenProvider).valueOrNull;
return Scaffold(
@@ -62,79 +65,193 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
.where((d) => d.delivered)
.toList();
return MapSidebarLayout(
mapWidget: DarkModeMapComponent(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
return routesData.when(
data: (routes) {
DeliveryRoute? currentRoute;
try {
currentRoute = routes.firstWhere(
(r) => r.id == widget.routeFragmentId,
);
} catch (_) {
currentRoute = routes.isNotEmpty ? routes.first : null;
}
return Row(
children: [
if (context.isDesktop && routes.isNotEmpty)
CollapsibleRoutesSidebar(
routes: routes,
selectedRoute: currentRoute,
onRouteSelected: (route) {
if (route.id != widget.routeFragmentId) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => DeliveriesPage(
routeFragmentId: route.id,
routeName: route.name,
),
),
);
}
},
),
Expanded(
child: MapSidebarLayout(
mapWidget: DarkModeMapComponent(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
),
sidebarWidget: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SegmentedButton<int>(
segments: const [
ButtonSegment(
value: 0,
label: Text('To Do'),
),
ButtonSegment(
value: 1,
label: Text('Delivered'),
),
],
selected: <int>{_currentSegment},
onSelectionChanged: (Set<int> newSelection) {
setState(() {
_currentSegment = newSelection.first;
_pageController.animateToPage(
_currentSegment,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
},
),
),
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentSegment = index;
});
},
children: [
DeliveryListView(
deliveries: todoDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
DeliveryListView(
deliveries: completedDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
],
),
),
],
),
),
),
],
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
sidebarWidget: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SegmentedButton<int>(
segments: const [
ButtonSegment(
value: 0,
label: Text('To Do'),
),
ButtonSegment(
value: 1,
label: Text('Delivered'),
),
],
selected: <int>{_currentSegment},
onSelectionChanged: (Set<int> newSelection) {
setState(() {
_currentSegment = newSelection.first;
_pageController.animateToPage(
_currentSegment,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
},
error: (error, stackTrace) => MapSidebarLayout(
mapWidget: DarkModeMapComponent(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
),
sidebarWidget: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SegmentedButton<int>(
segments: const [
ButtonSegment(
value: 0,
label: Text('To Do'),
),
ButtonSegment(
value: 1,
label: Text('Delivered'),
),
],
selected: <int>{_currentSegment},
onSelectionChanged: (Set<int> newSelection) {
setState(() {
_currentSegment = newSelection.first;
_pageController.animateToPage(
_currentSegment,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
},
),
),
),
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentSegment = index;
});
},
children: [
DeliveryListView(
deliveries: todoDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
DeliveryListView(
deliveries: completedDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
],
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentSegment = index;
});
},
children: [
DeliveryListView(
deliveries: todoDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
DeliveryListView(
deliveries: completedDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
],
),
),
),
],
],
),
),
);
},
+77 -58
View File
@@ -3,17 +3,29 @@ 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 '../components/premium_route_card.dart';
import '../components/collapsible_routes_sidebar.dart';
import '../components/dark_mode_map.dart';
import 'deliveries_page.dart';
import 'settings_page.dart';
class RoutesPage extends ConsumerWidget {
const RoutesPage({super.key});
void _navigateToDeliveries(BuildContext context, DeliveryRoute route) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DeliveriesPage(
routeFragmentId: route.id,
routeName: route.name,
),
),
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final routesData = ref.watch(deliveryRoutesProvider);
final allDeliveriesData = ref.watch(allDeliveriesProvider);
final userProfile = ref.watch(userProfileProvider);
return Scaffold(
@@ -73,14 +85,70 @@ class RoutesPage extends ConsumerWidget {
child: Text('No routes available'),
);
}
return RefreshIndicator(
onRefresh: () async {
// ignore: unused_result
ref.refresh(deliveryRoutesProvider);
return allDeliveriesData.when(
data: (allDeliveries) {
return RefreshIndicator(
onRefresh: () async {
// ignore: unused_result
ref.refresh(deliveryRoutesProvider);
// ignore: unused_result
ref.refresh(allDeliveriesProvider);
},
child: context.isDesktop
? Row(
children: [
Expanded(
child: DarkModeMapComponent(
deliveries: allDeliveries,
selectedDelivery: null,
onDeliverySelected: null,
),
),
CollapsibleRoutesSidebar(
routes: routes,
selectedRoute: null,
onRouteSelected: (route) {
_navigateToDeliveries(context, route);
},
),
],
)
: Column(
children: [
Expanded(
child: DarkModeMapComponent(
deliveries: allDeliveries,
selectedDelivery: null,
onDeliverySelected: null,
),
),
CollapsibleRoutesSidebar(
routes: routes,
selectedRoute: null,
onRouteSelected: (route) {
_navigateToDeliveries(context, route);
},
),
],
),
);
},
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 loading deliveries: $error'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.refresh(allDeliveriesProvider),
child: const Text('Retry'),
),
],
),
),
);
},
loading: () => const Center(
@@ -103,53 +171,4 @@ class RoutesPage extends ConsumerWidget {
);
}
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 PremiumRouteCard(
route: route,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DeliveriesPage(
routeFragmentId: route.id,
routeName: route.name,
),
),
);
},
);
}
}