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 '../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; final String routeName; const DeliveriesPage({ super.key, required this.routeFragmentId, required this.routeName, }); @override ConsumerState createState() => _DeliveriesPageState(); } class _DeliveriesPageState extends ConsumerState { late PageController _pageController; int _currentSegment = 0; Delivery? _selectedDelivery; @override void initState() { super.initState(); _pageController = PageController(); } @override void dispose() { _pageController.dispose(); super.dispose(); } @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( appBar: AppBar( title: Text(widget.routeName), elevation: 0, ), body: deliveriesData.when( data: (deliveries) { final todoDeliveries = deliveries .where((d) => !d.delivered && !d.isSkipped) .toList(); final completedDeliveries = deliveries .where((d) => d.delivered) .toList(); 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( segments: const [ ButtonSegment( value: 0, label: Text('To Do'), ), ButtonSegment( value: 1, label: Text('Delivered'), ), ], selected: {_currentSegment}, onSelectionChanged: (Set 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(), ), 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( segments: const [ ButtonSegment( value: 0, label: Text('To Do'), ), ButtonSegment( value: 1, label: Text('Delivered'), ), ], selected: {_currentSegment}, onSelectionChanged: (Set 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(), ), error: (error, stackTrace) => Center( child: Text('Error: $error'), ), ), ); } Future _handleDeliveryAction( BuildContext context, Delivery delivery, String action, String? token, ) async { if (token == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Authentication required')), ); return; } final authClient = CqrsApiClient( config: ApiClientConfig( baseUrl: ApiClientConfig.production.baseUrl, defaultHeaders: {'Authorization': 'Bearer $token'}, ), ); switch (action) { case 'complete': final result = await authClient.executeCommand( endpoint: 'completeDelivery', command: CompleteDeliveryCommand( deliveryId: delivery.id, deliveredAt: DateTime.now().toIso8601String(), ), ); result.when( success: (_) { // ignore: unused_result ref.refresh(deliveriesProvider(widget.routeFragmentId)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Delivery marked as completed')), ); }, onError: (error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: ${error.message}')), ); }, ); break; case 'uncomplete': final result = await authClient.executeCommand( endpoint: 'markDeliveryAsUncompleted', command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id), ); result.when( success: (_) { // ignore: unused_result ref.refresh(deliveriesProvider(widget.routeFragmentId)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Delivery marked as uncompleted')), ); }, onError: (error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: ${error.message}')), ); }, ); break; case 'call': final contact = delivery.orders.isNotEmpty && delivery.orders.first.contact != null ? delivery.orders.first.contact : null; if (contact?.phoneNumber != null) { final Uri phoneUri = Uri(scheme: 'tel', path: contact!.phoneNumber); if (await canLaunchUrl(phoneUri)) { await launchUrl(phoneUri); } } break; case 'map': // Navigation is now handled in-app by the DeliveryMap component // Just ensure the delivery is selected break; } } } class DeliveryListView extends StatelessWidget { final List deliveries; final Delivery? selectedDelivery; final ValueChanged onDeliverySelected; final Function(Delivery, String) onAction; const DeliveryListView({ super.key, required this.deliveries, this.selectedDelivery, required this.onDeliverySelected, required this.onAction, }); @override Widget build(BuildContext context) { if (deliveries.isEmpty) { return const Center( child: Text('No deliveries'), ); } return RefreshIndicator( onRefresh: () async { // Trigger refresh via provider }, child: ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: deliveries.length, itemBuilder: (context, index) { final delivery = deliveries[index]; return DeliveryListItem( delivery: delivery, isSelected: selectedDelivery?.id == delivery.id, onTap: () => onDeliverySelected(delivery), onCall: () => onAction(delivery, 'call'), animationIndex: index, ); }, ), ); } } class DeliveryCard extends StatelessWidget { final Delivery delivery; final bool isSelected; final VoidCallback onTap; final Function(Delivery, String) onAction; const DeliveryCard({ super.key, required this.delivery, this.isSelected = false, required this.onTap, required this.onAction, }); @override Widget build(BuildContext context) { final contact = delivery.orders.isNotEmpty && delivery.orders.first.contact != null ? delivery.orders.first.contact : null; final order = delivery.orders.isNotEmpty ? delivery.orders.first : null; return Card( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), color: isSelected ? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3) : null, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( delivery.name, style: Theme.of(context).textTheme.titleMedium, maxLines: 2, overflow: TextOverflow.ellipsis, ), if (contact != null) Text( contact.fullName, style: Theme.of(context).textTheme.bodySmall, ), ], ), ), if (delivery.delivered) Chip( label: const Text('Delivered'), backgroundColor: Theme.of(context).colorScheme.primaryContainer, ) else if (order?.isNewCustomer ?? false) Chip( label: const Text('New Customer'), backgroundColor: const Color(0xFFFFFBEB), ), ], ), const SizedBox(height: 12), if (delivery.deliveryAddress != null) Text( delivery.deliveryAddress!.formattedAddress, style: Theme.of(context).textTheme.bodySmall, maxLines: 2, overflow: TextOverflow.ellipsis, ), if (order != null) ...[ const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (order.totalItems != null) Text( '${order.totalItems} items', style: Theme.of(context).textTheme.bodySmall, ), Text( '${order.totalAmount} MAD', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), ], ), ], const SizedBox(height: 12), Wrap( spacing: 8, children: [ if (contact?.phoneNumber != null) OutlinedButton.icon( onPressed: () => onAction(delivery, 'call'), icon: const Icon(Icons.phone), label: const Text('Call'), ), if (delivery.deliveryAddress != null) OutlinedButton.icon( onPressed: () { onTap(); // Select the delivery onAction(delivery, 'map'); }, icon: const Icon(Icons.map), label: const Text('Navigate'), ), OutlinedButton.icon( onPressed: () => _showDeliveryActions(context), icon: const Icon(Icons.more_vert), label: const Text('More'), ), ], ), ], ), ), ), ); } void _showDeliveryActions(BuildContext context) { showModalBottomSheet( context: context, builder: (context) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ if (!delivery.delivered) ListTile( leading: const Icon(Icons.check_circle), title: const Text('Mark as Completed'), onTap: () { Navigator.pop(context); onAction(delivery, 'complete'); }, ) else ListTile( leading: const Icon(Icons.undo), title: const Text('Mark as Uncompleted'), onTap: () { Navigator.pop(context); onAction(delivery, 'uncomplete'); }, ), ListTile( leading: const Icon(Icons.camera_alt), title: const Text('Upload Photo'), onTap: () { Navigator.pop(context); // TODO: Implement photo upload }, ), ListTile( leading: const Icon(Icons.description), title: const Text('View Details'), onTap: () { Navigator.pop(context); // TODO: Navigate to delivery details }, ), ], ), ), ); } }