import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:image_picker/image_picker.dart'; import 'package:http/http.dart' as http; import '../models/delivery.dart'; import '../providers/providers.dart'; import '../api/client.dart'; import '../api/openapi_config.dart'; import '../models/delivery_commands.dart'; import '../components/map_sidebar_layout.dart'; import '../components/dark_mode_map.dart'; import '../components/delivery_list_item.dart'; import '../utils/toast_helper.dart'; class DeliveriesPage extends ConsumerStatefulWidget { final int routeFragmentId; final String routeName; final VoidCallback? onBack; final bool showAsEmbedded; final Delivery? selectedDelivery; final ValueChanged? onDeliverySelected; const DeliveriesPage({ super.key, required this.routeFragmentId, required this.routeName, this.onBack, this.showAsEmbedded = false, this.selectedDelivery, this.onDeliverySelected, }); @override ConsumerState createState() => _DeliveriesPageState(); } class _DeliveriesPageState extends ConsumerState { late ScrollController _listScrollController; Delivery? _selectedDelivery; int? _lastRouteFragmentId; @override void initState() { super.initState(); _listScrollController = ScrollController(); _selectedDelivery = widget.selectedDelivery; } @override void didUpdateWidget(DeliveriesPage oldWidget) { super.didUpdateWidget(oldWidget); if (widget.selectedDelivery != oldWidget.selectedDelivery) { setState(() { _selectedDelivery = widget.selectedDelivery; }); } } @override void dispose() { _listScrollController.dispose(); super.dispose(); } Future _autoScrollToFirstPending(List deliveries) async { final firstPendingIndex = deliveries.indexWhere((d) => !d.delivered && !d.isSkipped); if (_listScrollController.hasClients && firstPendingIndex != -1) { await Future.delayed(const Duration(milliseconds: 200)); // Scroll to position first pending delivery at top of list // Each item is approximately 70 pixels tall final scrollOffset = firstPendingIndex * 70.0; _listScrollController.animateTo( scrollOffset.clamp(0, _listScrollController.position.maxScrollExtent), duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, ); } } @override Widget build(BuildContext context) { final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId)); final tokenAsync = ref.watch(authTokenProvider); final token = tokenAsync.hasValue ? tokenAsync.value : null; // When embedded in sidebar, show only the delivery list with back button // This is a responsive sidebar that collapses like routes if (widget.showAsEmbedded) { final isExpanded = ref.watch(collapseStateProvider); return deliveriesData.when( data: (deliveries) { // Auto-scroll to first pending delivery when page loads or route changes if (_lastRouteFragmentId != widget.routeFragmentId) { _lastRouteFragmentId = widget.routeFragmentId; WidgetsBinding.instance.addPostFrameCallback((_) { _autoScrollToFirstPending(deliveries); }); } // Responsive sidebar that changes width when collapsed (300px expanded, 80px collapsed) return AnimatedContainer( duration: const Duration(milliseconds: 300), width: isExpanded ? 300 : 80, child: Column( children: [ // Header with back button Container( padding: EdgeInsets.symmetric( horizontal: isExpanded ? 12 : 0, vertical: 8, ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, border: Border( bottom: BorderSide( color: Theme.of(context).dividerColor, width: 1, ), ), ), child: Row( mainAxisAlignment: isExpanded ? MainAxisAlignment.start : MainAxisAlignment.center, children: [ if (isExpanded) ...[ IconButton( icon: const Icon(Icons.arrow_back), onPressed: widget.onBack, tooltip: 'Back to routes', ), const SizedBox(width: 8), Expanded( child: Text( widget.routeName, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], IconButton( icon: Icon(isExpanded ? Icons.menu_open : Icons.menu), onPressed: () { ref.read(collapseStateProvider.notifier).toggle(); }, tooltip: isExpanded ? 'Collapse' : 'Expand', ), ], ), ), // Delivery list Expanded( child: UnifiedDeliveryListView( deliveries: deliveries, selectedDelivery: _selectedDelivery, scrollController: _listScrollController, onDeliverySelected: (delivery) { setState(() { _selectedDelivery = delivery; }); // Notify parent about delivery selection widget.onDeliverySelected?.call(delivery); }, onItemAction: (delivery, action) { _handleDeliveryAction(context, delivery, action, token); _autoScrollToFirstPending(deliveries); }, isCollapsed: !isExpanded, ), ), ], ), ); }, loading: () => const Center( child: CircularProgressIndicator(), ), error: (error, stackTrace) => Center( child: Text('Error: $error'), ), ); } // When not embedded, show full page with map final routesData = ref.watch(deliveryRoutesProvider); return Scaffold( appBar: AppBar( title: Text(widget.routeName), elevation: 0, ), body: SafeArea( child: deliveriesData.when( data: (deliveries) { // Auto-scroll to first pending delivery when page loads or route changes if (_lastRouteFragmentId != widget.routeFragmentId) { _lastRouteFragmentId = widget.routeFragmentId; WidgetsBinding.instance.addPostFrameCallback((_) { _autoScrollToFirstPending(deliveries); }); } return routesData.when( data: (routes) { return MapSidebarLayout( mapWidget: DarkModeMapComponent( deliveries: deliveries, selectedDelivery: _selectedDelivery, onDeliverySelected: (delivery) { setState(() { _selectedDelivery = delivery; }); }, onAction: (action) => _selectedDelivery != null ? _handleDeliveryAction(context, _selectedDelivery!, action, token) : null, ), sidebarBuilder: (isCollapsed) => UnifiedDeliveryListView( deliveries: deliveries, selectedDelivery: _selectedDelivery, scrollController: _listScrollController, onDeliverySelected: (delivery) { setState(() { _selectedDelivery = delivery; }); }, onItemAction: (delivery, action) { _handleDeliveryAction(context, delivery, action, token); _autoScrollToFirstPending(deliveries); }, isCollapsed: isCollapsed, ), ); }, loading: () => const Center( child: CircularProgressIndicator(), ), error: (error, stackTrace) => MapSidebarLayout( mapWidget: DarkModeMapComponent( deliveries: deliveries, selectedDelivery: _selectedDelivery, onDeliverySelected: (delivery) { setState(() { _selectedDelivery = delivery; }); }, onAction: (action) => _selectedDelivery != null ? _handleDeliveryAction(context, _selectedDelivery!, action, token) : null, ), sidebarBuilder: (isCollapsed) => UnifiedDeliveryListView( deliveries: deliveries, selectedDelivery: _selectedDelivery, scrollController: _listScrollController, onDeliverySelected: (delivery) { setState(() { _selectedDelivery = delivery; }); }, onItemAction: (delivery, action) { _handleDeliveryAction(context, delivery, action, token); _autoScrollToFirstPending(deliveries); }, isCollapsed: isCollapsed, ), ), ); }, 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) { ToastHelper.showError(context, '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, ), ); result.when( success: (_) { // ignore: unused_result ref.refresh(deliveriesProvider(widget.routeFragmentId)); ToastHelper.showSuccess(context, 'Delivery marked as completed'); }, onError: (error) { ToastHelper.showError(context, '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)); ToastHelper.showSuccess(context, 'Delivery marked as uncompleted'); }, onError: (error) { ToastHelper.showError(context, 'Error: ${error.message}'); }, ); break; case 'photo': await _handlePhotoCapture(context, delivery, token); 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; } } Future _handlePhotoCapture( BuildContext context, Delivery delivery, String? token, ) async { if (token == null) { ToastHelper.showError(context, 'Authentication required'); return; } final ImagePicker picker = ImagePicker(); XFile? pickedFile; try { pickedFile = await picker.pickImage( source: ImageSource.camera, imageQuality: 85, ); } catch (e) { if (context.mounted) { ToastHelper.showError(context, 'Camera error: $e'); } return; } if (pickedFile == null) { return; } if (!context.mounted) return; final bool? confirmed = await showDialog( context: context, builder: (BuildContext dialogContext) { return AlertDialog( title: const Text('Confirm Photo'), content: Column( mainAxisSize: MainAxisSize.min, children: [ Image.file( File(pickedFile!.path), height: 300, fit: BoxFit.contain, ), const SizedBox(height: 16), Text( 'Upload this photo for ${delivery.name}?', textAlign: TextAlign.center, ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('Upload'), ), ], ); }, ); if (confirmed != true) { return; } if (!context.mounted) return; showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { return const Center( child: Card( child: Padding( padding: EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Uploading photo...'), ], ), ), ), ); }, ); try { final Uri uploadUrl = Uri.parse( '${ApiClientConfig.production.baseUrl}/api/delivery/uploadDeliveryPicture?deliveryId=${delivery.id}', ); final http.MultipartRequest request = http.MultipartRequest('POST', uploadUrl); request.headers['Authorization'] = 'Bearer $token'; request.files.add(await http.MultipartFile.fromPath('file', pickedFile.path)); final http.StreamedResponse streamedResponse = await request.send(); final http.Response response = await http.Response.fromStream(streamedResponse); if (context.mounted) { Navigator.of(context).pop(); } if (response.statusCode >= 200 && response.statusCode < 300) { if (context.mounted) { ToastHelper.showSuccess(context, 'Photo uploaded successfully'); } // ignore: unused_result ref.refresh(deliveriesProvider(widget.routeFragmentId)); } else { if (context.mounted) { ToastHelper.showError(context, 'Upload failed: ${response.statusCode}'); } } } catch (e) { if (context.mounted) { Navigator.of(context).pop(); ToastHelper.showError(context, 'Upload error: $e'); } } } } class UnifiedDeliveryListView extends StatelessWidget { final List deliveries; final Delivery? selectedDelivery; final ScrollController scrollController; final ValueChanged onDeliverySelected; final Function(Delivery, String) onItemAction; final bool isCollapsed; const UnifiedDeliveryListView({ super.key, required this.deliveries, this.selectedDelivery, required this.scrollController, required this.onDeliverySelected, required this.onItemAction, this.isCollapsed = false, }); @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( controller: scrollController, padding: const EdgeInsets.only(top: 4, bottom: 8), physics: const AlwaysScrollableScrollPhysics(), itemCount: deliveries.length, // Show all deliveries with scrolling itemBuilder: (context, index) { final delivery = deliveries[index]; return DeliveryListItem( delivery: delivery, isSelected: selectedDelivery?.id == delivery.id, onTap: () => onDeliverySelected(delivery), onCall: () => onItemAction(delivery, 'call'), onAction: (action) => onItemAction(delivery, action), animationIndex: index, isCollapsed: isCollapsed, ); }, ), ); } } 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.withValues(alpha: 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 }, ), ], ), ), ); } }