import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../l10n/app_localizations.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/unified_delivery_list.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; final l10n = AppLocalizations.of(context)!; // 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(l10n.error(error.toString())), ), ); } // 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(l10n.error(error.toString())), ), ), ), ); } Future _handleDeliveryAction( BuildContext context, Delivery delivery, String action, String? token, ) async { // Prevent any actions on warehouse delivery except map navigation if (delivery.isWarehouseDelivery && action != 'map') { return; } if (token == null) { final l10n = AppLocalizations.of(context)!; ToastHelper.showError(context, l10n.authenticationRequired); 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: (_) { final l10n = AppLocalizations.of(context)!; // ignore: unused_result ref.refresh(deliveriesProvider(widget.routeFragmentId)); // ignore: unused_result ref.refresh(deliveryRoutesProvider); ToastHelper.showSuccess(context, l10n.deliverySuccessful); }, onError: (error) { final l10n = AppLocalizations.of(context)!; ToastHelper.showError(context, l10n.error(error.message)); }, ); break; case 'uncomplete': final result = await authClient.executeCommand( endpoint: 'markDeliveryAsUncompleted', command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id), ); result.when( success: (_) { final l10n = AppLocalizations.of(context)!; // ignore: unused_result ref.refresh(deliveriesProvider(widget.routeFragmentId)); // ignore: unused_result ref.refresh(deliveryRoutesProvider); ToastHelper.showSuccess(context, 'Delivery marked as uncompleted'); }, onError: (error) { final l10n = AppLocalizations.of(context)!; ToastHelper.showError(context, l10n.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) { final l10n = AppLocalizations.of(context)!; ToastHelper.showError(context, l10n.authenticationRequired); 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: Text(AppLocalizations.of(context)!.cancel), ), ElevatedButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: Text(AppLocalizations.of(context)!.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'); } } } }