diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 82a80b0..f65a948 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -13,12 +13,12 @@ PODS: - Flutter - google_navigation_flutter (0.0.1): - Flutter - - GoogleNavigation (= 10.0.0) - - GoogleMaps (10.0.0): - - GoogleMaps/Maps (= 10.0.0) - - GoogleMaps/Maps (10.0.0) - - GoogleNavigation (10.0.0): - - GoogleMaps (= 10.0.0) + - GoogleNavigation (= 10.7.0) + - GoogleMaps (10.7.0): + - GoogleMaps/Maps (= 10.7.0) + - GoogleMaps/Maps (10.7.0) + - GoogleNavigation (10.7.0): + - GoogleMaps (= 10.7.0) - image_picker_ios (0.0.1): - Flutter - path_provider_foundation (0.0.1): @@ -74,9 +74,9 @@ SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_appauth: d4abcf54856e5d8ba82ed7646ffc83245d4aa448 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 - google_navigation_flutter: aff5e273b19113b8964780ff4e899f6f2e07f6dc - GoogleMaps: 9ce9c898074e96655acaf1ba5d6f85991ecee7a3 - GoogleNavigation: 963899162709d245f07a65cd68c3115292ee2bdb + google_navigation_flutter: d3daf840117efbfd2d70e0f70c933cffb62b3ad1 + GoogleMaps: 5db81729b4f6defd40820d46b49a350273ec1d28 + GoogleNavigation: ed62063d8f141a8a134703ea8246778ec3d8da01 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d diff --git a/lib/components/mobile_map_with_overlay.dart b/lib/components/mobile_map_with_overlay.dart new file mode 100644 index 0000000..11e0be2 --- /dev/null +++ b/lib/components/mobile_map_with_overlay.dart @@ -0,0 +1,230 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/delivery.dart'; +import '../providers/providers.dart'; +import '../l10n/app_localizations.dart'; +import 'dark_mode_map.dart'; +import 'unified_delivery_list.dart'; + +/// Mobile-optimized map layout with toggleable deliveries list overlay +/// +/// This component provides a full-screen map view for mobile devices with +/// a bottom overlay that can be toggled to show the deliveries list. +class MobileMapWithOverlay extends ConsumerStatefulWidget { + final List deliveries; + final Delivery? selectedDelivery; + final ValueChanged onDeliverySelected; + final Function(Delivery, String) onDeliveryAction; + + const MobileMapWithOverlay({ + super.key, + required this.deliveries, + this.selectedDelivery, + required this.onDeliverySelected, + required this.onDeliveryAction, + }); + + @override + ConsumerState createState() => _MobileMapWithOverlayState(); +} + +class _MobileMapWithOverlayState extends ConsumerState + with SingleTickerProviderStateMixin { + late ScrollController _listScrollController; + late AnimationController _animationController; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + _listScrollController = ScrollController(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _slideAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ); + } + + @override + void dispose() { + _listScrollController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + void _toggleList() { + final isOpen = ref.read(mobileDeliveriesListOpenProvider); + if (isOpen) { + _animationController.reverse(); + } else { + _animationController.forward(); + } + ref.read(mobileDeliveriesListOpenProvider.notifier).toggle(); + } + + int get _completedCount { + return widget.deliveries.where((d) => d.delivered).length; + } + + int get _totalCount { + // Exclude warehouse delivery from total count + return widget.deliveries.where((d) => !d.isWarehouseDelivery).length; + } + + @override + Widget build(BuildContext context) { + final isListOpen = ref.watch(mobileDeliveriesListOpenProvider); + final l10n = AppLocalizations.of(context); + final screenHeight = MediaQuery.of(context).size.height; + final overlayHeight = screenHeight * 0.7; + + return Stack( + children: [ + // Full-screen map + Positioned.fill( + child: DarkModeMapComponent( + deliveries: widget.deliveries, + selectedDelivery: widget.selectedDelivery, + onDeliverySelected: widget.onDeliverySelected, + onAction: (action) { + if (widget.selectedDelivery != null) { + widget.onDeliveryAction(widget.selectedDelivery!, action); + } + }, + ), + ), + + // Dimmed overlay when list is open + if (isListOpen) + Positioned.fill( + child: GestureDetector( + onTap: _toggleList, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: isListOpen ? 0.3 : 0.0, + child: Container( + color: Colors.black, + ), + ), + ), + ), + + // Animated deliveries overlay + AnimatedBuilder( + animation: _slideAnimation, + builder: (context, child) { + return Positioned( + left: 0, + right: 0, + bottom: -overlayHeight * (1 - _slideAnimation.value), + height: overlayHeight, + child: GestureDetector( + onVerticalDragEnd: (details) { + // Swipe down to close + if (details.primaryVelocity != null && details.primaryVelocity! > 500) { + _toggleList(); + } + }, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: Column( + children: [ + // Drag handle + Container( + height: 32, + alignment: Alignment.center, + child: Container( + height: 4, + width: 40, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + // Header with close button + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + l10n.deliveries, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: _toggleList, + tooltip: l10n.close, + ), + ], + ), + ), + // Deliveries list + Expanded( + child: UnifiedDeliveryListView( + deliveries: widget.deliveries, + selectedDelivery: widget.selectedDelivery, + scrollController: _listScrollController, + onDeliverySelected: (delivery) { + widget.onDeliverySelected(delivery); + // Optionally close the overlay after selection + // _toggleList(); + }, + onItemAction: (delivery, action) { + widget.onDeliveryAction(delivery, action); + }, + isCollapsed: false, + ), + ), + ], + ), + ), + ), + ); + }, + ), + + // Floating toggle button (FAB) - only show when list is closed + if (!isListOpen) + Positioned( + bottom: 80, // Above bottom action buttons + right: 16, + child: FloatingActionButton.extended( + onPressed: _toggleList, + icon: const Icon(Icons.list), + label: Text('$_completedCount/$_totalCount'), + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + elevation: 4, + ), + ), + ], + ); + } +} diff --git a/lib/pages/routes_page.dart b/lib/pages/routes_page.dart index b6fc49d..a877012 100644 --- a/lib/pages/routes_page.dart +++ b/lib/pages/routes_page.dart @@ -17,7 +17,9 @@ import '../components/dark_mode_map.dart'; import '../components/loading_dialog.dart'; import '../components/notes_dialog.dart'; import '../components/photo_capture_dialog.dart'; +import '../components/mobile_map_with_overlay.dart'; import '../services/location_permission_service.dart'; +import '../utils/breakpoints.dart'; import 'deliveries_page.dart'; import 'settings_page.dart'; @@ -462,6 +464,69 @@ class _RoutesPageState extends ConsumerState { } return allDeliveriesData.when( data: (allDeliveries) { + final isMobile = context.isMobile; + + // Mobile layout: Show routes list full-screen when no route selected + if (isMobile && _selectedRoute == null) { + return RefreshIndicator( + onRefresh: () async { + // ignore: unused_result + ref.refresh(deliveryRoutesProvider); + // ignore: unused_result + ref.refresh(allDeliveriesProvider); + }, + child: CollapsibleRoutesSidebar( + routes: routes, + selectedRoute: null, + onRouteSelected: _selectRoute, + ), + ); + } + + // Mobile layout: full-screen map with overlay when route is selected + if (isMobile && _selectedRoute != null) { + final routeDeliveries = allDeliveries + .where((d) => d.routeFragmentId == _selectedRoute!.id) + .toList(); + + return RefreshIndicator( + onRefresh: () async { + // ignore: unused_result + ref.refresh(deliveryRoutesProvider); + // ignore: unused_result + ref.refresh(allDeliveriesProvider); + }, + child: Stack( + children: [ + MobileMapWithOverlay( + deliveries: routeDeliveries, + selectedDelivery: _selectedDelivery, + onDeliverySelected: (delivery) { + setState(() { + _selectedDelivery = delivery; + }); + _autoShowNotesIfNeeded(delivery); + }, + onDeliveryAction: (delivery, action) { + _handleDeliveryAction(action, delivery, _selectedRoute!.id); + }, + ), + // Back button to return to routes list + Positioned( + top: 16, + left: 16, + child: FloatingActionButton.small( + onPressed: _backToRoutes, + backgroundColor: Theme.of(context).colorScheme.surface, + child: const Icon(Icons.arrow_back), + ), + ), + ], + ), + ); + } + + // Tablet/Desktop layout: split view with map + sidebar return RefreshIndicator( onRefresh: () async { // ignore: unused_result diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index b4b0947..603a162 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -179,6 +179,19 @@ final collapseStateProvider = NotifierProvider(() { return CollapseStateNotifier(); }); +// Mobile deliveries list toggle state notifier for mobile overlay +class MobileDeliveriesListOpenNotifier extends Notifier { + @override + bool build() => false; // Default: closed + + void toggle() => state = !state; + void setOpen(bool open) => state = open; +} + +final mobileDeliveriesListOpenProvider = NotifierProvider(() { + return MobileDeliveriesListOpenNotifier(); +}); + class _EmptyQuery implements Serializable { @override Map toJson() => {};