part of 'deliveries.dart'; // ------------------------------------------------------------------------- // SCREEN 1: DELIVERY MAP PREVIEW (Shows route, has "Start" button) // ------------------------------------------------------------------------- class _DeliveryMapScreen extends StatefulWidget { final Map delivery; final _MyDeliveriesState? parentState; const _DeliveryMapScreen({ required this.delivery, this.parentState, }); @override State<_DeliveryMapScreen> createState() => _DeliveryMapScreenState(); } class _DeliveryMapScreenState extends State<_DeliveryMapScreen> { GoogleMapController? _mapController; late final LatLng _pickupLocation; late final LatLng _dropLocation; final Set _markers = {}; final Set _polylines = {}; late final PolylinePoints _polylinePoints; bool _isLoadingRoute = true; bool _isNavigating = false; // Prevent multiple clicks static const String _googleApiKey = 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q'; @override void initState() { super.initState(); _polylinePoints = PolylinePoints(apiKey: _googleApiKey); _resolveLocationsFromDelivery(); _setMarkers(); _isLoadingRoute = false; _createRealRoute(); } double _parseD(dynamic v) { if (v == null) return 0.0; if (v is num) return v.toDouble(); return double.tryParse(v.toString()) ?? 0.0; } void _resolveLocationsFromDelivery() { final d = widget.delivery; final double pickLat = _parseD(d['pickuplat'] ?? d['PickupLat']); final double pickLon = _parseD(d['pickuplon'] ?? d['PickupLon']); final double dropLat = _parseD( d['droplat'] ?? d['DropLat'] ?? d['deliverylat'], ); final double dropLon = _parseD( d['droplon'] ?? d['DropLon'] ?? d['deliverylong'], ); final double riderLat = _parseD(d['riderslat']); final double riderLon = _parseD(d['riderslon']); final bool hasPickup = pickLat != 0 && pickLon != 0; final bool hasDrop = dropLat != 0 && dropLon != 0; final LatLng pickup = hasPickup ? LatLng(pickLat, pickLon) : (riderLat != 0 && riderLon != 0 ? LatLng(riderLat, riderLon) : const LatLng(10.998356, 76.977596)); final LatLng drop = hasDrop ? LatLng(dropLat, dropLon) : const LatLng(11.004556, 76.967696); _pickupLocation = pickup; _dropLocation = drop; } void _setMarkers() { _markers.addAll([ Marker( markerId: const MarkerId('pickup'), position: _pickupLocation, infoWindow: const InfoWindow(title: 'Pickup Location'), icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed), ), Marker( markerId: const MarkerId('drop'), position: _dropLocation, infoWindow: const InfoWindow(title: 'Drop Location'), icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen), ), ]); } Future _createRealRoute() async { try { final request = PolylineRequest( origin: PointLatLng( _pickupLocation.latitude, _pickupLocation.longitude, ), destination: PointLatLng( _dropLocation.latitude, _dropLocation.longitude, ), mode: TravelMode.driving, ); final result = await _polylinePoints.getRouteBetweenCoordinates( request: request, ); if (result.points.isNotEmpty) { final routePoints = result.points .map((e) => LatLng(e.latitude, e.longitude)) .toList(); if (mounted) { setState(() { _polylines.add( Polyline( polylineId: const PolylineId('real_route'), color: ColorConstants.primaryColor, width: 6, points: routePoints, ), ); _isLoadingRoute = false; }); _fitMapToRoute(); } } else { debugPrint('[MAP_PREVIEW] No route found'); if (mounted) { setState(() => _isLoadingRoute = false); } } } catch (e) { debugPrint('[MAP_PREVIEW] Error creating route: $e'); if (mounted) { setState(() => _isLoadingRoute = false); } } } Future _fitMapToRoute() async { if (!mounted) return; if (_mapController == null) return; // Check if controller is still alive (important!) try { await _mapController!.getVisibleRegion(); } catch (e) { debugPrint("❌ Map controller is dead. Skip animateCamera."); return; } final bounds = LatLngBounds( southwest: LatLng( math.min(_pickupLocation.latitude, _dropLocation.latitude), math.min(_pickupLocation.longitude, _dropLocation.longitude), ), northeast: LatLng( math.max(_pickupLocation.latitude, _dropLocation.latitude), math.max(_pickupLocation.longitude, _dropLocation.longitude), ), ); // Try animate safely for (int i = 0; i < 10; i++) { if (!mounted) return; try { await _mapController!.animateCamera( CameraUpdate.newLatLngBounds(bounds, 80), ); return; } catch (e) { await Future.delayed(const Duration(milliseconds: 150)); } } debugPrint("❌ animateCamera failed after retries (map probably disposed)"); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ SizedBox.expand( child: GoogleMap( initialCameraPosition: CameraPosition( target: _pickupLocation, zoom: 14.5, ), onMapCreated: (controller) { _mapController = controller; Future.delayed(const Duration(milliseconds: 500), () { if (!_isLoadingRoute && _polylines.isNotEmpty) { _fitMapToRoute(); } }); }, markers: _markers, polylines: _polylines, zoomControlsEnabled: false, myLocationButtonEnabled: false, ), ), Positioned( top: 50, left: 16, child: CircleAvatar( backgroundColor: Colors.white, child: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () => Navigator.pop(context), ), ), ), Align( alignment: Alignment.bottomCenter, child: Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), boxShadow: [ BoxShadow( color: Colors.black26, blurRadius: 8, offset: Offset(0, -2), ), ], ), child: SafeArea( top: false, child: LayoutBuilder( builder: (ctx, constraints) { final double maxSheetHeight = MediaQuery.of(ctx).size.height * 0.35; // ignore: unused_local_variable final double allowedHeight = math.min( constraints.maxHeight, maxSheetHeight, ); return ListView( padding: EdgeInsets.zero, shrinkWrap: true, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Customer Details', style: TextStyle( fontSize: FontConstants.xxLarge(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), InkWell( onTap: () async { // Just launch dialer; PiP is handled only from navigation screen final phone = (widget.delivery['deliverycontactno'] ?? '') .toString(); final bool success = await launchPhoneDialer( phone.isNotEmpty ? phone : '9876543210', ); if (!success && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Could not launch dialer'), ), ); } }, child: Image.asset( 'assets/images/phone-call .png', height: 27, width: 27, errorBuilder: (c, e, s) => const Icon( Icons.phone, size: 27, color: Colors.green, ), ), ), ], ), const SizedBox(height: 8), const Divider(thickness: 1), const SizedBox(height: 8), _buildCustomerInfo(), const SizedBox(height: 20), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: ColorConstants.primaryColor, minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), onPressed: _isNavigating ? null : () async { // Prevent multiple clicks if (_isNavigating || !mounted || !context.mounted) return; setState(() { _isNavigating = true; }); try { // Capture screen size before navigation final screenSize = MediaQuery.of(context).size * MediaQuery.of(context).devicePixelRatio; final dc = Get.put( DeliveriesController(), permanent: true, ); final d = widget.delivery; final int deliveryId = int.tryParse( '${d['deliveryid'] ?? d['DeliveryId'] ?? 0}', ) ?? 0; final String orderId = (d['orderid'] ?? d['OrderId'] ?? '') .toString(); final int orderHeaderId = int.tryParse( '${d['orderheaderid'] ?? d['OrderHeaderId'] ?? 0}', ) ?? 0; // Save ridertime start at the moment navigation is started try { if (deliveryId > 0) { final prefs = await SharedPreferences.getInstance(); // 1) Save rider time start (existing behaviour) await prefs.setString( 'ridertime_start_$deliveryId', DateTime.now().toIso8601String(), ); // 2) Save ETA end time for this order so PiP timer can resume correctly final rawEta = d['eta']; int etaMinutes = 0; if (rawEta != null) { etaMinutes = int.tryParse(rawEta.toString()) ?? 0; } if (etaMinutes > 0) { final now = DateTime.now(); final endTime = now .add(Duration(minutes: etaMinutes)) .millisecondsSinceEpoch ~/ 1000; // store seconds await prefs.setInt( 'eta_endtime_$orderId', endTime, ); debugPrint( '[ACTIVE][ETA] Saved eta_endtime_$orderId -> $endTime (eta=$etaMinutes min)', ); } debugPrint( '[ACTIVE] Saved ridertime_start for deliveryId=$deliveryId', ); } } catch (e) { debugPrint( '[ACTIVE] Error saving ridertime_start: $e', ); } final parentState = widget.parentState ?? context .findAncestorStateOfType< _MyDeliveriesState >(); // ✅ CRITICAL: Check active delivery BEFORE navigation if (parentState != null) { final currentOrderId = (d['orderid'] ?? d['OrderId'] ?? '') .toString(); final activeOrderIds = parentState ._activeDeliveries .map( (del) => (del['orderid'] ?? del['OrderId'] ?? '') .toString(), ) .where((id) => id.isNotEmpty) .toSet(); // Block if there's a different active delivery if (parentState._activeDeliveries.isNotEmpty && !activeOrderIds.contains(currentOrderId)) { if (mounted) { setState(() { _isNavigating = false; }); showDialog( context: context, barrierDismissible: true, builder: (BuildContext dialogContext) { return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.orange.shade400, Colors.red.shade500, ], ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Icon Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, ), child: const Icon( Icons.warning_rounded, color: Colors.white, size: 48, ), ), const SizedBox(height: 20), // Title Text( 'Active Delivery in Progress', textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: FontConstants.xxLarge(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), const SizedBox(height: 12), // Message Text( 'Please complete your active delivery first before starting another delivery.', textAlign: TextAlign.center, style: TextStyle( color: Colors.white.withOpacity(0.95), fontSize: FontConstants.medium(context), height: 1.4, fontFamily: FontConstants.fontFamily, ), ), const SizedBox(height: 24), // Action Button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { Navigator.of(dialogContext).pop(); // Navigate to active delivery if (parentState._activeDeliveries.isNotEmpty) { parentState.startDelivery(parentState._activeDeliveries.first); } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: Colors.red.shade600, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 2, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.two_wheeler, size: 22, ), const SizedBox(width: 8), Text( 'View Active Delivery', style: TextStyle( fontSize: FontConstants.regular(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), ], ), ), ), const SizedBox(height: 12), // Close Button TextButton( onPressed: () { Navigator.of(dialogContext).pop(); }, child: Text( 'Close', style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: 15, fontWeight: FontWeight.w500, fontFamily: FontConstants.fontFamily, ), ), ), ], ), ), ); }, ); } return; // Return early to prevent starting another delivery } } if (deliveryId > 0 && orderId.isNotEmpty) { String riderLatStr = '0'; String riderLngStr = '0'; try { final position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.medium, timeLimit: Duration(seconds: 3), ), ).timeout(const Duration(seconds: 3)); riderLatStr = position.latitude .toStringAsFixed(6); riderLngStr = position.longitude .toStringAsFixed(6); } catch (e) { debugPrint( '[ACTIVE] Error getting rider location: $e', ); try { final lastPos = await Geolocator.getLastKnownPosition(); if (lastPos != null) { riderLatStr = lastPos.latitude .toStringAsFixed(6); riderLngStr = lastPos.longitude .toStringAsFixed(6); } } catch (_) {} } // ✅ CRITICAL: Navigate FIRST, then handle status updates if (!mounted || !context.mounted) { setState(() { _isNavigating = false; }); return; } // Navigate immediately - this must happen Navigator.of(context).push( MaterialPageRoute( builder: (context) => _RiderNavigationScreen( delivery: widget.delivery, parentState: widget.parentState, ), ), ); // Reset navigation state after navigation completes setState(() { _isNavigating = false; }); // Continue with status updates and PiP in background debugPrint( '[ACTIVE] Updating status for deliveryId=$deliveryId orderId=$orderId lat=$riderLatStr lng=$riderLngStr', ); final ok = await dc.updateActiveStatus( deliveryId: deliveryId, orderHeaderId: orderHeaderId, ridersLat: riderLatStr, ridersLng: riderLngStr, orderId: orderId, ); debugPrint( '[ACTIVE] Status update result: $ok', ); // ✅ CRITICAL: ENFORCE PiP when delivery becomes active (compulsory) if (ok && !dc.isPipEnabled.value) { try { debugPrint('[ACTIVE] Delivery is now active - Enforcing PiP mode'); final floating = Floating(); const rational = Rational.landscape(); final height = (screenSize.height * 0.5).toInt(); final width = (screenSize.width * 0.9).toInt(); final arguments = ImmediatePiP( aspectRatio: rational, sourceRectHint: math.Rectangle( ((screenSize.width - width) ~/ 2).toInt(), ((screenSize.height - height) ~/ 2).toInt(), width, height, ), ); await floating.enable(arguments); dc.isPipEnabled.value = true; debugPrint('[ACTIVE] PiP enabled successfully'); // Also try method channel as backup try { const channel = MethodChannel('nearle/pip'); await channel.invokeMethod('enterPip'); } catch (_) {} } catch (e) { debugPrint('[ACTIVE] Error enabling PiP: $e'); } } if (parentState != null) { final previousActive = parentState._activeDeliveryOrderId; if (previousActive != null && previousActive != orderId) { parentState._stopDeliveryPosting( previousActive, ); } parentState._activeDeliveryOrderId = orderId; d['orderstatus'] = 'active'; final orderKey = parentState._getOrderKey(d); if (parentState._skippedOrdersCache .containsKey(orderKey)) { parentState._skippedOrdersCache.remove( orderKey, ); parentState._skippedOrderTimestamps.remove( orderKey, ); await parentState._saveSkippedOrdersCache(); debugPrint( '[ACTIVE] Removed from skipped cache (resumed): $orderKey', ); } await parentState._startDeliveryPosting(d); try { final prefs = await SharedPreferences.getInstance(); await prefs.setString( 'active_delivery_order_id', orderId, ); } catch (e) { debugPrint( '[ACTIVE] Error saving active delivery ID: $e', ); } // ignore: invalid_use_of_protected_member parentState.setState(() {}); } } else { debugPrint( '[ACTIVE] Invalid deliveryId: $deliveryId or orderId: $orderId', ); setState(() { _isNavigating = false; }); } } catch (e) { debugPrint( '[ACTIVE] Error in navigation flow: $e', ); if (mounted) { setState(() { _isNavigating = false; }); } } }, child: _isNavigating ? Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ), const SizedBox(width: 12), Text( 'Starting...', style: TextStyle( fontSize: FontConstants.xLarge(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, color: Colors.white, ), ), ], ) : Text( 'Start Navigation', style: TextStyle( fontSize: FontConstants.xLarge(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, color: Colors.white, ), ), ), ], ); }, ), ), ), ), ], ), ); } Widget _buildCustomerInfo() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoRow( 'Name:', (widget.delivery['deliverycustomer'] ?? 'Customer').toString(), ), const SizedBox(height: 10), _buildInfoRow( 'Address:', (widget.delivery['deliveryaddress'] ?? 'Address not available') .toString(), isExpanded: true, ), const SizedBox(height: 10), _buildInfoRow( 'Order ID:', '#${(widget.delivery['orderid'] ?? '').toString()}', ), const SizedBox(height: 10), _buildInfoRow( 'Distance:', '${(widget.delivery['kms'] ?? '0').toString()} km', valueColor: Colors.red, ), ], ); } Widget _buildInfoRow( String label, String value, { bool isExpanded = false, Color? valueColor, }) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: FontConstants.xLarge(context), fontWeight: FontWeight.w600, fontFamily: FontConstants.fontFamily, ), ), const SizedBox(width: 15), isExpanded ? Expanded( child: Text( value, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: FontConstants.large(context), fontFamily: FontConstants.fontFamily, color: valueColor ?? Colors.black87, ), ), ) : Text( value, style: TextStyle( fontSize: FontConstants.large(context), fontFamily: FontConstants.fontFamily, color: valueColor ?? Colors.black87, ), ), ], ); } }