import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../l10n/app_localizations.dart'; import 'package:image_picker/image_picker.dart'; import 'package:http/http.dart' as http; import '../models/delivery.dart'; import '../models/delivery_route.dart'; import '../models/delivery_commands.dart'; import '../providers/providers.dart'; import '../api/client.dart'; import '../utils/toast_helper.dart'; import '../api/openapi_config.dart'; import '../utils/breakpoints.dart'; import '../utils/http_client_factory.dart'; import '../components/collapsible_routes_sidebar.dart'; import '../components/dark_mode_map.dart'; import '../services/location_permission_service.dart'; import 'deliveries_page.dart'; import 'settings_page.dart'; class RoutesPage extends ConsumerStatefulWidget { const RoutesPage({super.key}); @override ConsumerState createState() => _RoutesPageState(); } class _RoutesPageState extends ConsumerState { late LocationPermissionService _permissionService; DeliveryRoute? _selectedRoute; Delivery? _selectedDelivery; @override void initState() { super.initState(); _permissionService = LocationPermissionService(); _requestLocationPermissionOnce(); } Future _requestLocationPermissionOnce() async { try { final hasPermission = await _permissionService.hasLocationPermission(); if (!hasPermission && mounted) { final result = await _permissionService.requestLocationPermission(); result.when( granted: () { debugPrint('Location permission granted'); }, denied: () { debugPrint('Location permission denied'); }, permanentlyDenied: () { debugPrint('Location permission permanently denied'); }, error: (message) { debugPrint('Location permission error: $message'); }, ); } } catch (e) { debugPrint('Error requesting location permission: $e'); } } void _selectRoute(DeliveryRoute route) { setState(() { _selectedRoute = route; }); } void _backToRoutes() { setState(() { _selectedRoute = null; _selectedDelivery = null; }); } Future _handleDeliveryAction( String action, Delivery delivery, int routeFragmentId, ) async { final authService = ref.read(authServiceProvider); // Ensure we have a valid token (automatically refreshes if needed) final token = await authService.ensureValidToken(); if (token == null) { if (mounted) { final l10n = AppLocalizations.of(context)!; ToastHelper.showError(context, l10n.authenticationRequired); } return; } // Create API client with auth service for automatic token refresh final authClient = CqrsApiClient( config: ApiClientConfig.development, authService: authService, ); switch (action) { case 'complete': if (mounted) { 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('Completing delivery...'), ], ), ), ), ); }, ); } final result = await authClient.executeCommand( endpoint: 'completeDelivery', command: CompleteDeliveryCommand( deliveryId: delivery.id, ), ); result.when( success: (_) async { if (mounted) { Navigator.of(context).pop(); } if (mounted) { // Invalidate both providers to force refresh ref.invalidate(deliveriesProvider(routeFragmentId)); ref.invalidate(allDeliveriesProvider); // Wait for providers to refresh await Future.delayed(const Duration(milliseconds: 500)); if (mounted) { // Get refreshed deliveries final allDeliveries = await ref.read(allDeliveriesProvider.future); final routeDeliveries = allDeliveries .where((d) => d.routeFragmentId == routeFragmentId) .toList(); // Find the next incomplete delivery in the route final nextDelivery = routeDeliveries.firstWhere( (d) => !d.delivered && !d.isSkipped, orElse: () => routeDeliveries.firstWhere( (d) => d.id == delivery.id, orElse: () => delivery, ), ); setState(() { _selectedDelivery = nextDelivery; }); // Auto-show notes for the next delivery if needed _autoShowNotesIfNeeded(nextDelivery); // Small delay to let the UI update before map auto-navigates if (nextDelivery.id != delivery.id && mounted) { await Future.delayed(const Duration(milliseconds: 200)); } } if (mounted) { final l10n = AppLocalizations.of(context)!; ToastHelper.showSuccess(context, l10n.deliverySuccessful); } } }, onError: (error) { if (mounted) { Navigator.of(context).pop(); } debugPrint('Complete delivery failed - Type: ${error.type}, Message: ${error.message}'); debugPrint('Error details: ${error.details}'); if (mounted) { final l10n = AppLocalizations.of(context)!; String errorMessage = l10n.error(error.message); if (error.statusCode == 500) { errorMessage = 'Server error - Please contact support'; } ToastHelper.showError(context, errorMessage); } }, ); break; case 'uncomplete': if (mounted) { 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('Marking as uncompleted...'), ], ), ), ), ); }, ); } final result = await authClient.executeCommand( endpoint: 'markDeliveryAsUncompleted', command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id), ); result.when( success: (_) async { if (mounted) { Navigator.of(context).pop(); } if (mounted) { // Invalidate both providers to force refresh ref.invalidate(deliveriesProvider(routeFragmentId)); ref.invalidate(allDeliveriesProvider); // Wait for providers to refresh await Future.delayed(const Duration(milliseconds: 500)); if (mounted) { // Get refreshed deliveries final allDeliveries = await ref.read(allDeliveriesProvider.future); final updatedDelivery = allDeliveries.firstWhere( (d) => d.id == delivery.id, orElse: () => delivery, ); setState(() { _selectedDelivery = updatedDelivery; }); } if (mounted) { final l10n = AppLocalizations.of(context)!; ToastHelper.showSuccess(context, 'Delivery marked as uncompleted'); } } }, onError: (error) { if (mounted) { Navigator.of(context).pop(); } if (mounted) { final l10n = AppLocalizations.of(context)!; ToastHelper.showError(context, l10n.error(error.message)); } }, ); break; case 'photo': await _handlePhotoCapture(delivery); break; case 'note': await _showNotesDialog(delivery); break; } } Future _handlePhotoCapture( Delivery delivery, ) async { final authService = ref.read(authServiceProvider); // Ensure we have a valid token (automatically refreshes if needed) final token = await authService.ensureValidToken(); if (token == null) { if (mounted) { 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 (mounted) { ToastHelper.showError(context, 'Camera error: $e'); } return; } if (pickedFile == null) { return; } if (!mounted) return; final bool? confirmed = await showDialog( context: context, builder: (BuildContext dialogContext) { return AlertDialog( title: const Text('Confirm Photo'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(dialogContext).size.height * 0.5, maxWidth: MediaQuery.of(dialogContext).size.width * 0.8, ), child: Image.file( File(pickedFile!.path), 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 (!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.development.baseUrl}/api/delivery/uploadDeliveryPicture?deliveryId=${delivery.id}', ); // Create HTTP client that accepts self-signed certificates final client = HttpClientFactory.createClient( allowSelfSigned: ApiClientConfig.development.allowSelfSignedCertificate, ); 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 client.send(request); final http.Response response = await http.Response.fromStream(streamedResponse); client.close(); if (mounted) { Navigator.of(context).pop(); } if (response.statusCode >= 200 && response.statusCode < 300) { if (mounted) { ToastHelper.showSuccess(context, 'Photo uploaded successfully'); } ref.refresh(allDeliveriesProvider); } else { debugPrint('Photo upload failed - Status: ${response.statusCode}'); debugPrint('Response body: ${response.body}'); if (mounted) { String errorMessage = 'Upload failed'; if (response.statusCode == 500) { errorMessage = 'Server error - Please contact support'; } else if (response.statusCode == 401) { errorMessage = 'Authentication required - Please log in again'; } else { errorMessage = 'Upload failed: ${response.statusCode}'; } ToastHelper.showError(context, errorMessage); } } } catch (e) { if (mounted) { Navigator.of(context).pop(); ToastHelper.showError(context, 'Upload error: $e'); } } } bool _shouldAutoShowNotes(Delivery? delivery) { // Only auto-show notes if delivery is not yet delivered and has notes if (delivery == null || delivery.delivered) return false; final hasNotes = delivery.orders.any( (order) => order.note != null && order.note!.isNotEmpty, ); return hasNotes; } Future _autoShowNotesIfNeeded(Delivery? delivery) async { if (delivery != null && _shouldAutoShowNotes(delivery)) { // Use post-frame callback to ensure UI is ready WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _showNotesDialog(delivery); } }); } } Future _showNotesDialog(Delivery delivery) async { final notes = delivery.orders .where((order) => order.note != null && order.note!.isNotEmpty) .map((order) => order.note!) .toList(); if (!mounted) return; if (notes.isEmpty) { ToastHelper.showInfo(context, 'No notes attached to this delivery'); return; } await showDialog( context: context, builder: (BuildContext dialogContext) { return AlertDialog( title: Text('Notes for ${delivery.name}'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: notes.map((note) => Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Text( note, style: Theme.of(dialogContext).textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, fontSize: 16, ), ), )).toList(), ), ), actionsAlignment: MainAxisAlignment.center, actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), actions: [ SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Close'), ), ), ], ); }, ); } @override Widget build(BuildContext context) { final routesData = ref.watch(deliveryRoutesProvider); final allDeliveriesData = ref.watch(allDeliveriesProvider); final userProfile = ref.watch(userProfileProvider); final l10n = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( title: Text(l10n.deliveryRoutes), elevation: 0, scrolledUnderElevation: 0, actions: [ IconButton( icon: (routesData.isLoading || allDeliveriesData.isLoading) ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh), onPressed: (routesData.isLoading || allDeliveriesData.isLoading) ? null : () { ref.refresh(deliveryRoutesProvider); ref.refresh(allDeliveriesProvider); }, tooltip: 'Refresh', ), userProfile.when( data: (profile) { String getInitials(String? fullName) { if (fullName == null || fullName.isEmpty) return 'U'; final names = fullName.trim().split(' '); if (names.length == 1) { return names[0][0].toUpperCase(); } return '${names.first[0]}${names.last[0]}'.toUpperCase(); } return GestureDetector( onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const SettingsPage(), ), ); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: CircleAvatar( radius: 16, backgroundColor: Theme.of(context).colorScheme.primary, child: Text( getInitials(profile?.fullName), style: TextStyle( color: Theme.of(context).colorScheme.onPrimary, fontSize: 14, fontWeight: FontWeight.w600, ), ), ), ), ); }, loading: () => const Padding( padding: EdgeInsets.all(16.0), child: SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), ), error: (error, stackTrace) => const SizedBox(), ), ], ), body: routesData.when( data: (routes) { if (routes.isEmpty) { return Center( child: Text(l10n.noRoutes), ); } return allDeliveriesData.when( data: (allDeliveries) { return RefreshIndicator( onRefresh: () async { // ignore: unused_result ref.refresh(deliveryRoutesProvider); // ignore: unused_result ref.refresh(allDeliveriesProvider); }, child: Row( children: [ Expanded( child: DarkModeMapComponent( deliveries: allDeliveries, selectedDelivery: _selectedDelivery, onDeliverySelected: (delivery) { setState(() { _selectedDelivery = delivery; }); _autoShowNotesIfNeeded(delivery); }, onAction: (action) { if (_selectedDelivery != null && _selectedRoute != null) { _handleDeliveryAction(action, _selectedDelivery!, _selectedRoute!.id); } }, ), ), _selectedRoute == null ? CollapsibleRoutesSidebar( routes: routes, selectedRoute: null, onRouteSelected: _selectRoute, ) : DeliveriesPage( routeFragmentId: _selectedRoute!.id, routeName: _selectedRoute!.name, onBack: _backToRoutes, showAsEmbedded: true, selectedDelivery: _selectedDelivery, onDeliverySelected: (delivery) { setState(() { _selectedDelivery = delivery; }); _autoShowNotesIfNeeded(delivery); }, ), ], ), ); }, loading: () => const Center( child: CircularProgressIndicator(), ), error: (error, stackTrace) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(l10n.error(error.toString())), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.refresh(allDeliveriesProvider), child: Text(l10n.retry), ), ], ), ), ); }, loading: () => const Center( child: CircularProgressIndicator(), ), error: (error, stackTrace) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(l10n.error(error.toString())), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.refresh(deliveryRoutesProvider), child: Text(l10n.retry), ), ], ), ), ), ); } }