import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_tts/flutter_tts.dart'; import 'package:geolocator/geolocator.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:audioplayers/audioplayers.dart'; import 'dart:math' as math; import 'package:nearle/providers/deliverylog/deliverylog_provider.dart'; import 'package:nearle/providers/notifications/notificationservce.dart'; import 'package:nearle/views/helpers/constants/apiconstants.dart'; import 'package:nearle/utils/kalman_filter.dart'; /// Background service for managing active delivery logs class BackgroundDeliveryLog { static NearleKalmanFilter? _kf; static DateTime? _lastUpdateTime; static final CreateDeliveryLogProvider _logProvider = CreateDeliveryLogProvider(); static final http.Client _httpClient = http.Client(); static final FlutterTts _tts = FlutterTts(); static final AudioPlayer _proximityPlayer = AudioPlayer(); static const double _idleThresholdMeters = 10; // ~5-10m tolerance static const double _proximityThresholdMeters = 50.0; // 50 meters for arrival alert static const String _activeDeliveriesKey = 'active_deliveries'; static const String _payloadKeyPrefix = 'delivery_payload_'; static const String _offlineLogKey = 'offline_delivery_logs'; static const String _proximityAlertKeyPrefix = 'delivery_proximity_alerted_'; static bool _isPosting = false; static bool _ttsReady = false; static bool _audioReady = false; /// Process active deliveries: fetch from API, filter active, and post logs static Future processActiveDeliveries() async { if (_isPosting) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] Already posting, skipping...'); return; } _isPosting = true; try { // Get userid from SharedPreferences final prefs = await SharedPreferences.getInstance(); final userId = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0; debugPrint('[ACTIVE_DELIVERY_LOG][BG] Checking for user: $userId'); if (userId == 0) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] ❌ No user ID found in prefs'); return; } // Get current date dynamically (YYYY-MM-DD format) final now = DateTime.now(); final today = '${now.year}-${_pad(now.month)}-${_pad(now.day)}'; // Build API URL using v1 endpoint (getdeliveries) final bool isLive = ApiConstants.mainRoute == 'live'; final baseUrl = isLive ? ApiConstants.currentDeliveryLive : ApiConstants.currentDeliveryDev; final uri = Uri.parse(baseUrl).replace( queryParameters: { 'userid': userId.toString(), 'fromdate': today, 'todate': today, 't': DateTime.now().millisecondsSinceEpoch.toString(), }, ); debugPrint('[ACTIVE_DELIVERY_LOG][BG] Fetching deliveries: $uri'); // Fetch deliveries from API using v1 endpoint final deliveries = await _fetchDeliveriesFromApi(uri); debugPrint('[ACTIVE_DELIVERY_LOG][BG] API returned ${deliveries.length} items'); // Filter for active orders only final activeOrders = deliveries.whereType>().where(( order, ) { final status = (order['orderstatus']?.toString().toLowerCase() ?? '') .trim(); final isActive = status == 'active'; return isActive; }).toList(); if (activeOrders.isEmpty) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] No active orders found'); // ✅ Still check shift end even if no active deliveries debugPrint('[ACTIVE_DELIVERY_LOG][BG] 🔍 Checking shift end time...'); await checkShiftEnd(); return; } debugPrint( '[ACTIVE_DELIVERY_LOG][BG] Found ${activeOrders.length} active deliveries', ); // Post logs for each active delivery for (final order in activeOrders) { final orderId = (order['orderid'] ?? '').toString(); if (orderId.isEmpty) continue; // Get current coordinates once per order to reuse for proximity + log final Map? coords = await _getCoordinatesWithFallback(); if (coords != null) { await _checkProximityAlert(order, coords); } // Get payload from SharedPreferences or create new one Map? payload = await _loadPayload(orderId); if (payload == null) { // Create new payload from order data with userId from SharedPreferences payload = _createPayload(order, userId); await _persistPayload(orderId, payload); await _markAsActive(orderId); } else { // Ensure userid is set correctly in existing payload payload['userid'] = userId; } // Post the log await _postDeliveryLog(orderId, payload, currentCoords: coords); } // ✅ CRITICAL: Check if shift end time has passed (backup to alarm) await checkShiftEnd(); // Attempt to flush offline logs await _flushOfflineLogs(); } catch (e) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] Error processing: $e'); } finally { _isPosting = false; } } /// Post delivery log for a specific order static Future _postDeliveryLog( String orderId, Map payload, { Map? currentCoords, }) async { Map? payloadWithCoords; try { // Get current coordinates final coords = currentCoords ?? await _getCoordinatesWithFallback(); if (coords == null) { return; } // Distance calculation is now fully handled by LiveTrackingService in the background. // 03-14: Removed redundant 60-second background calculations to avoid logic conflicts. final deliveryId = payload['deliveryid']?.toString() ?? ''; // Create log date timestamp final now = DateTime.now(); final logDate = '${now.year}-${_pad(now.month)}-${_pad(now.day)} ${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}'; // Retrieve final cumulative KM to send double riderKmsToSend = 0.0; if (deliveryId.isNotEmpty && deliveryId != '0') { try { final prefs = await SharedPreferences.getInstance(); await prefs.reload(); // Force reload so we get the latest value written by the main isolate riderKmsToSend = double.tryParse(prefs.getString('delivery_tracking_${deliveryId}_cumulativeKm') ?? '0') ?? 0.0; } catch (_) {} } // Build payload with coordinates and timestamp payloadWithCoords = { ...payload, 'logdate': logDate, 'latitude': coords['lat'] ?? '0', 'longitude': coords['lng'] ?? '0', 'raw_latitude': coords['raw_lat'] ?? '0', 'raw_longitude': coords['raw_lng'] ?? '0', 'velocity_lat': coords['velocity_lat'] ?? '0', 'velocity_lng': coords['velocity_lng'] ?? '0', 'speed': coords['speed'] ?? '0', 'heading': coords['heading'] ?? '0', 'riderkms': riderKmsToSend, 'logstatus': await _resolveLogStatus(orderId, coords), }; // Determine API endpoint final url = ApiConstants.mainRoute == 'live' ? ApiConstants.createDeliveryLogLive : ApiConstants.createDeliveryLogDev; // Post the log debugPrint('[ACTIVE_DELIVERY_LOG][BG] Posting log for $orderId'); final result = await _logProvider .createDeliveryLog(url, payloadWithCoords) .timeout( const Duration(seconds: 8), onTimeout: () => throw TimeoutException( 'Delivery log post timeout for $orderId', ), ); if (result != null) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] Success for $orderId'); await _persistLastLogLocation(orderId, coords); } else { debugPrint( '[ACTIVE_DELIVERY_LOG][BG] Warning: No response for $orderId', ); } } catch (e) { debugPrint( '[ACTIVE_DELIVERY_LOG][BG] Failed to post log for $orderId: $e', ); // Offline fallback if (payloadWithCoords != null) { await _saveToOfflineQueue(orderId, payloadWithCoords); } } } /// Create payload from delivery order data static Map _createPayload( Map delivery, int userId, ) { return { 'logid': 0, 'tenantid': delivery['tenantid'] ?? 0, 'partnerid': delivery['partnerid'] ?? 0, 'locationid': delivery['locationid'] ?? 0, 'orderheaderid': delivery['orderheaderid'] ?? 0, 'deliveryid': delivery['deliveryid'] ?? 0, 'userid': userId, 'orderid': (delivery['orderid'] ?? '').toString(), 'orderstatus': 'active', }; } /// Get coordinates with fallback (current position -> last known -> cached) static Future?> _getCoordinatesWithFallback() async { Map result = { 'lat': '0', 'lng': '0', 'raw_lat': '0', 'raw_lng': '0', 'speed': '0', 'heading': '0', 'velocity_lat': '0', 'velocity_lng': '0', }; try { // 1. Check if location services are enabled final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] Location services disabled'); return await _getCachedCoordinatesAsMap(); } // 2. Check permissions final permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] Location permission denied'); return await _getCachedCoordinatesAsMap(); } Position? position; try { position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, ), ).timeout(const Duration(seconds: 5)); } catch (e) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] Failed to get current pos: $e'); position = await Geolocator.getLastKnownPosition(); } if (position != null) { // Reject mocked GPS (anti-cheat) if (position.isMocked) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] Mocked position — using cached'); return await _getCachedCoordinatesAsMap(); } // Reject very poor accuracy to avoid phantom movements in delivery logs if (position.accuracy > 50.0) { debugPrint( '[ACTIVE_DELIVERY_LOG][BG] Low-accuracy position (${position.accuracy.toStringAsFixed(0)}m) — using cached', ); return await _getCachedCoordinatesAsMap(); } final now = DateTime.now(); double outLat = position.latitude; double outLng = position.longitude; double speed = position.speed; double heading = position.heading; // Decompose velocity for Kalman final double headingRadians = heading * (math.pi / 180.0); final double velocityLng = speed * math.sin(headingRadians); final double velocityLat = speed * math.cos(headingRadians); if (_kf == null) { _kf = NearleKalmanFilter(lat: outLat, lng: outLng); } else { final double dt = _lastUpdateTime != null ? now.difference(_lastUpdateTime!).inMilliseconds / 1000.0 : 30.0; _kf!.predict(dt); _kf!.update(outLat, outLng); outLat = _kf!.x[0]; outLng = _kf!.x[1]; } _lastUpdateTime = now; result = { 'lat': outLat.toString(), 'lng': outLng.toString(), 'raw_lat': position.latitude.toString(), 'raw_lng': position.longitude.toString(), 'speed': speed.toStringAsFixed(2), 'heading': heading.toStringAsFixed(2), 'velocity_lat': velocityLat.toStringAsFixed(4), 'velocity_lng': velocityLng.toStringAsFixed(4), }; await _cacheCoordinates(result['lat']!, result['lng']!); return result; } return await _getCachedCoordinatesAsMap(); } catch (e) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] Error getting coords: $e'); return await _getCachedCoordinatesAsMap(); } } static Future?> _getCachedCoordinatesAsMap() async { final coords = await _getCachedCoordinates(); if (coords == null) return null; return { 'lat': coords.$1, 'lng': coords.$2, 'raw_lat': coords.$1, 'raw_lng': coords.$2, 'speed': '0', 'heading': '0', 'velocity_lat': '0', 'velocity_lng': '0', }; } /// Cache coordinates to SharedPreferences static Future _cacheCoordinates(String lat, String lng) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('last_lat', lat); await prefs.setString('last_lng', lng); } catch (_) {} } /// Get cached coordinates from SharedPreferences static Future<(String, String)?> _getCachedCoordinates() async { try { final prefs = await SharedPreferences.getInstance(); final lat = (prefs.getString('last_lat') ?? '').trim(); final lng = (prefs.getString('last_lng') ?? '').trim(); if (lat.isNotEmpty && lng.isNotEmpty) { return (lat, lng); } } catch (_) {} return null; } /// Load payload from SharedPreferences static Future?> _loadPayload(String orderId) async { try { final prefs = await SharedPreferences.getInstance(); final jsonStr = prefs.getString('$_payloadKeyPrefix$orderId'); if (jsonStr == null || jsonStr.isEmpty) return null; final decoded = jsonDecode(jsonStr); if (decoded is Map) return decoded; if (decoded is Map) return decoded.cast(); } catch (_) {} return null; } /// Persist payload to SharedPreferences static Future _persistPayload( String orderId, Map payload, ) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('$_payloadKeyPrefix$orderId', jsonEncode(payload)); } catch (_) {} } /// Mark order as active in SharedPreferences static Future _markAsActive(String orderId) async { try { final prefs = await SharedPreferences.getInstance(); final list = prefs.getStringList(_activeDeliveriesKey) ?? []; if (!list.contains(orderId)) { list.add(orderId); await prefs.setStringList(_activeDeliveriesKey, list); } } catch (_) {} } /// Fetch deliveries from API using v1 endpoint static Future> _fetchDeliveriesFromApi(Uri uri) async { try { final res = await _httpClient .get(uri) .timeout(const Duration(seconds: 10)); if (res.statusCode >= 200 && res.statusCode < 300) { final decoded = json.decode(res.body); final data = decoded is Map ? (decoded['details'] ?? decoded['data'] ?? decoded) : decoded; if (data is List) { return data; } if (data is Map && data['items'] is List) { return data['items'] as List; } return []; } return []; } catch (e) { return []; } } /// Helper to pad numbers with leading zero static String _pad(int n) => n.toString().padLeft(2, '0'); static Future _resolveLogStatus( String orderId, Map coords, ) async { try { final last = await _loadLastLogLocation(orderId); if (last != null) { final lastLat = last.$1; final lastLng = last.$2; final currentLat = double.tryParse(coords['lat'] ?? '0') ?? 0; final currentLng = double.tryParse(coords['lng'] ?? '0') ?? 0; if (currentLat != 0 && currentLng != 0 && lastLat != 0 && lastLng != 0) { final distance = Geolocator.distanceBetween( lastLat, lastLng, currentLat, currentLng, ); if (distance <= _idleThresholdMeters) { return 1; // idle } } } } catch (e) { debugPrint('[ACTIVE_DELIVERY_LOG][BG] logstatus error: $e'); } return 0; // moving } static Future<(double, double)?> _loadLastLogLocation(String orderId) async { try { final prefs = await SharedPreferences.getInstance(); final jsonStr = prefs.getString('last_log_loc_$orderId'); if (jsonStr == null || jsonStr.isEmpty) return null; final decoded = jsonDecode(jsonStr); if (decoded is Map) { final lat = double.tryParse('${decoded['lat']}') ?? 0; final lng = double.tryParse('${decoded['lng']}') ?? 0; if (lat != 0 && lng != 0) return (lat, lng); } } catch (_) {} return null; } static Future _persistLastLogLocation( String orderId, Map coords, ) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString( 'last_log_loc_$orderId', jsonEncode({'lat': coords['lat'], 'lng': coords['lng']}), ); } catch (_) {} } // ---------------- Offline Queue Logic ---------------- static Future _flushOfflineLogs() async { try { final prefs = await SharedPreferences.getInstance(); final List queue = prefs.getStringList(_offlineLogKey) ?? []; if (queue.isEmpty) return; debugPrint( '[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Flushing ${queue.length} offline logs...', ); final List remaining = []; bool anySuccess = false; final url = ApiConstants.mainRoute == 'live' ? ApiConstants.createDeliveryLogLive : ApiConstants.createDeliveryLogDev; for (final itemStr in queue) { try { final Map item = jsonDecode(itemStr); final String orderId = item['orderId'] ?? ''; final Map payload = Map.from( item['payload'] ?? {}, ); if (payload.isEmpty) continue; debugPrint( '[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Retrying for orderId: $orderId', ); final result = await _logProvider .createDeliveryLog(url, payload) .timeout(const Duration(seconds: 8)); if (result != null) { debugPrint( '[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Success for orderId: $orderId', ); anySuccess = true; } else { remaining.add(itemStr); } } catch (e) { remaining.add(itemStr); } } if (anySuccess || remaining.length != queue.length) { await prefs.setStringList(_offlineLogKey, remaining); } } catch (e) { debugPrint('[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Flush error: $e'); } } static Future _saveToOfflineQueue( String orderId, Map payload, ) async { try { final prefs = await SharedPreferences.getInstance(); final List queue = prefs.getStringList(_offlineLogKey) ?? []; final item = jsonEncode({ 'orderId': orderId, 'payload': payload, 'timestamp': DateTime.now().millisecondsSinceEpoch, }); queue.add(item); await prefs.setStringList(_offlineLogKey, queue); debugPrint( '[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Saved to queue. Total: ${queue.length}', ); } catch (e) { debugPrint( '[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Error saving to queue: $e', ); } } // ---------------- Auto Shift End Logic ---------------- /// Check if shift has ended and trigger auto-break if needed static Future checkShiftEnd() async { try { debugPrint('[AUTO_SHIFT_END] 🔍 Starting shift end check...'); final prefs = await SharedPreferences.getInstance(); // 1. Check if currently On Duty final int onduty = prefs.getInt('onduty') ?? 0; debugPrint('[AUTO_SHIFT_END] 📊 Current onduty status: $onduty'); if (onduty != 1) { debugPrint('[AUTO_SHIFT_END] ⏭️ Rider not on duty, skipping check'); return; // Already offline } // 2. Get Shift Timings final String startTimeStr = prefs.getString('starttime') ?? ''; final String endTimeStr = prefs.getString('endtime') ?? ''; debugPrint('[AUTO_SHIFT_END] ⏰ Shift times - Start: "$startTimeStr", End: "$endTimeStr"'); if (endTimeStr.isEmpty) { debugPrint('[AUTO_SHIFT_END] ⚠️ No endtime found, cannot check shift end'); return; } // 3. Parse Times (Assumed format HH:mm:ss) final now = DateTime.now(); final endParts = endTimeStr.split(':'); if (endParts.length < 2) return; final int endHour = int.tryParse(endParts[0]) ?? 0; final int endMinute = int.tryParse(endParts[1]) ?? 0; final int endSecond = endParts.length > 2 ? (int.tryParse(endParts[2]) ?? 0) : 0; // Create DateTime for end time on TODAY final DateTime endToday = DateTime( now.year, now.month, now.day, endHour, endMinute, endSecond, ); bool isShiftOver = false; if (startTimeStr.isNotEmpty) { final startParts = startTimeStr.split(':'); if (startParts.length >= 2) { final int startHour = int.tryParse(startParts[0]) ?? 0; final int startMinute = int.tryParse(startParts[1]) ?? 0; // Create DateTime for start time on TODAY final DateTime startToday = DateTime( now.year, now.month, now.day, startHour, startMinute, ); // Check for overnight shift (Start > End in 24-hour format, e.g. 22:00 to 06:00) // This means shift crosses midnight final double startVal = startHour + (startMinute / 60.0); final double endVal = endHour + (endMinute / 60.0); if (startVal > endVal) { // ✅ OVERNIGHT SHIFT (crosses midnight, e.g. 22:00 to 06:00) // Shift is over if current time is AFTER end time today AND BEFORE start time today // Example: End 06:00, Start 22:00 // - Now 23:00 -> After 22:00 (start) -> Still in shift (ACTIVE) // - Now 05:00 -> Before 06:00 (end) -> Still in shift from yesterday (ACTIVE) // - Now 10:00 -> After 06:00 (end) AND Before 22:00 (start) -> Shift OVER debugPrint('[AUTO_SHIFT_END] 🌙 Overnight shift detected (Start: ${_pad(startHour)}:${_pad(startMinute)}, End: ${_pad(endHour)}:${_pad(endMinute)})'); debugPrint('[AUTO_SHIFT_END] 📅 Current time: ${_pad(now.hour)}:${_pad(now.minute)}'); if (now.isAfter(endToday) && now.isBefore(startToday)) { // We're in the gap period between end and start -> shift is OVER isShiftOver = true; debugPrint('[AUTO_SHIFT_END] ✅ Overnight shift: In gap period -> SHIFT OVER'); } else { // We're either before end (still in shift from yesterday) or after start (still in shift today) isShiftOver = false; debugPrint('[AUTO_SHIFT_END] ✅ Overnight shift: Still active'); } } else { // ✅ NORMAL DAY SHIFT (e.g. 09:00 to 17:00, doesn't cross midnight) // Shift is over if current time is AFTER end time debugPrint('[AUTO_SHIFT_END] ☀️ Day shift detected (Start: ${_pad(startHour)}:${_pad(startMinute)}, End: ${_pad(endHour)}:${_pad(endMinute)})'); debugPrint('[AUTO_SHIFT_END] 📅 Current time: ${_pad(now.hour)}:${_pad(now.minute)}, End time: ${_pad(endHour)}:${_pad(endMinute)}'); if (now.isAfter(endToday) || now.isAtSameMomentAs(endToday)) { isShiftOver = true; debugPrint('[AUTO_SHIFT_END] ✅ Day shift: Shift ended'); } else { isShiftOver = false; debugPrint('[AUTO_SHIFT_END] ✅ Day shift: Still active'); } } } else { // Fallback if start time parse fails: assume day shift debugPrint('[AUTO_SHIFT_END] ⚠️ Could not parse start time, assuming day shift'); if (now.isAfter(endToday) || now.isAtSameMomentAs(endToday)) { isShiftOver = true; } } } else { // Fallback if no start time: assume day shift debugPrint('[AUTO_SHIFT_END] ⚠️ No start time provided, assuming day shift'); if (now.isAfter(endToday) || now.isAtSameMomentAs(endToday)) { isShiftOver = true; } } // 4. Trigger Auto End if Shift is Over debugPrint('[AUTO_SHIFT_END] 📅 Time check result - isShiftOver: $isShiftOver, Current: ${_pad(now.hour)}:${_pad(now.minute)}, End: ${_pad(endHour)}:${_pad(endMinute)}'); if (isShiftOver) { debugPrint('[AUTO_SHIFT_END] ⏰ Shift ended (Start: $startTimeStr, End: $endTimeStr). Current: ${_pad(now.hour)}:${_pad(now.minute)}'); await _autoEndShift(prefs); } else { debugPrint('[AUTO_SHIFT_END] ✅ Shift not ended yet, continuing...'); } } catch (e) { debugPrint('[AUTO_SHIFT_END] ❌ Error checking shift end: $e'); debugPrint('[AUTO_SHIFT_END] Stack trace: ${StackTrace.current}'); } } static Future _autoEndShift(SharedPreferences prefs) async { try { debugPrint( '[AUTO_SHIFT_END] 🚀 Initiating auto-break and offline sequence...', ); // 1. Get Required IDs final int userid = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0; final int partnerid = prefs.getInt('partnerid') ?? prefs.getInt('partnerId') ?? 0; final int shiftid = prefs.getInt('shiftid') ?? prefs.getInt('shiftId') ?? 0; final int logid = prefs.getInt('logid') ?? prefs.getInt('logId') ?? 0; if (userid == 0) { debugPrint( '[AUTO_SHIFT_END] ❌ Missing userid, cannot create break log', ); return; } // 2. Get Location final Map? coords = await _getCoordinatesWithFallback(); final String lat = coords?['lat'] ?? '0'; final String lng = coords?['lng'] ?? '0'; // 3. Prepare Break Log Payload final now = DateTime.now(); final int localBreakId = (DateTime.now().millisecondsSinceEpoch % 900) + 100; // Random-ish ID final String breakdate = '${now.year}-${_pad(now.month)}-${_pad(now.day)} ${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}'; final String breakstart = '${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}'; final payload = { "breakid": localBreakId, "logid": logid, "breakdate": breakdate, "userid": userid, "partnerid": partnerid, "shiftid": shiftid, "breakstart": breakstart, "breakend": "", "breakhours": 0.0, "latitude": lat, "longitude": lng, }; // 4. Call API to Create Break final url = ApiConstants.mainRoute == 'live' ? ApiConstants.createBreakRiderLogLive : ApiConstants.createBreakRiderLogDev; debugPrint('[AUTO_SHIFT_END] Creating break log: $url'); // We use a separate provider instance or http call if needed, // but _logProvider is for delivery logs. We need a generic post or use http directly. // Since we don't have BreakRiderLogProvider here, we'll use http directly for simplicity and isolation. try { final response = await _httpClient .post( Uri.parse(url), headers: {'Content-Type': 'application/json'}, body: jsonEncode(payload), ) .timeout(const Duration(seconds: 10)); if (response.statusCode >= 200 && response.statusCode < 300) { debugPrint('[AUTO_SHIFT_END] ✅ Break log created successfully'); // Parse response to get server break ID if needed, but mainly we just need to go offline final body = jsonDecode(response.body); final det = (body['details'] is Map) ? body['details'] : body; final serverBreakId = det['breakid']; if (serverBreakId != null) { await prefs.setInt( 'breakId', int.tryParse('$serverBreakId') ?? localBreakId, ); } await prefs.setString('breakStart', breakstart); await prefs.setInt('break_start_epoch', now.millisecondsSinceEpoch); } else { debugPrint( '[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${response.statusCode}', ); } } catch (e) { debugPrint('[AUTO_SHIFT_END] ❌ API Error: $e'); // Even if API fails, we MUST go offline locally to prevent further issues } // 5. Set Offline Locally await prefs.setInt('onduty', 0); await prefs.setBool('online', false); // 6. Update Rider Log (Set Duty = 0) // We should also update the main rider log to say onduty=0 final updateUrl = ApiConstants.mainRoute == 'live' ? ApiConstants.updateRiderLogLive : ApiConstants.updateRiderLogDev; final updatePayload = { "userid": userid, "onduty": 0, "latitude": lat, "longitude": lng, }; try { await _httpClient .post( Uri.parse(updateUrl), headers: {'Content-Type': 'application/json'}, body: jsonEncode(updatePayload), ) .timeout(const Duration(seconds: 5)); debugPrint('[AUTO_SHIFT_END] ✅ Rider status updated to Offline'); } catch (_) {} debugPrint('[AUTO_SHIFT_END] 🏁 Auto-shift end sequence complete.'); } catch (e) { debugPrint('[AUTO_SHIFT_END] Critical error in _autoEndShift: $e'); } } /// Trigger proximity alert once per delivery when within threshold static Future _checkProximityAlert( Map order, Map coords, ) async { try { final deliveryId = (order['deliveryid'] ?? order['orderid'] ?? '').toString(); if (deliveryId.isEmpty) return; final dropLatRaw = (order['droplat'] ?? order['DropLat'] ?? order['deliverylat'] ?? '') .toString(); final dropLngRaw = (order['droplon'] ?? order['droplong'] ?? order['DropLon'] ?? order['DropLong'] ?? order['deliverylong'] ?? '') .toString(); final dropLat = double.tryParse(dropLatRaw) ?? 0.0; final dropLng = double.tryParse(dropLngRaw) ?? 0.0; if (dropLat == 0 || dropLng == 0 || dropLat.abs() > 90 || dropLng.abs() > 180) { return; } final riderLat = double.tryParse(coords['lat'] ?? '0') ?? 0.0; final riderLng = double.tryParse(coords['lng'] ?? '0') ?? 0.0; if (riderLat == 0 || riderLng == 0 || riderLat.abs() > 90 || riderLng.abs() > 180) { return; } final distanceMeters = Geolocator.distanceBetween( dropLat, dropLng, riderLat, riderLng, ); if (distanceMeters <= _proximityThresholdMeters) { final prefs = await SharedPreferences.getInstance(); final alreadyAlerted = prefs.getBool(‘$_proximityAlertKeyPrefix$deliveryId’) ?? false; if (alreadyAlerted) { debugPrint(‘[PROXIMITY] Already alerted for deliveryId=$deliveryId, skipping’); return; } await NotificationServce.showLocalNotification( title: ‘Near delivery location’, body: ‘You\’ve reached your destination. Please update the status.’, ); final played = await _playProximityAudio(); await prefs.setBool(‘$_proximityAlertKeyPrefix$deliveryId’, true); debugPrint( ‘[PROXIMITY] Alerted deliveryId=$deliveryId at ${distanceMeters.toStringAsFixed(1)}m ‘ ‘target=($dropLat,$dropLng) rider=($riderLat,$riderLng) played=$played’, ); } else { debugPrint( ‘[PROXIMITY] Skipped alert for deliveryId=$deliveryId | distance=${distanceMeters.toStringAsFixed(1)}m’, ); } } catch (e) { debugPrint('[PROXIMITY] Error sending alert: $e'); } } static Future _playProximityAudio() async { try { if (!_audioReady) { await _proximityPlayer.setReleaseMode(ReleaseMode.stop); await _proximityPlayer.setVolume(1.0); _audioReady = true; } // Attempt to play bundled destination audio await _proximityPlayer.stop(); await _proximityPlayer.play(AssetSource('audio/destination.mp3')); debugPrint('[PROXIMITY][AUDIO] Playing destination.mp3'); return true; } catch (e) { debugPrint('[PROXIMITY][AUDIO] Error playing destination clip: $e'); } // Fallback to TTS if audio fails try { // Initialize once if (!_ttsReady) { await _tts.setLanguage('en-US'); await _tts.setSpeechRate(0.9); await _tts.setVolume(1.0); await _tts.setPitch(1.0); _ttsReady = true; } // Speak without awaiting completion to avoid blocking await _tts.speak('You have reached your destination. Please update the delivery status.'); debugPrint('[PROXIMITY][TTS] Spoke destination prompt'); return true; } catch (e) { debugPrint('[PROXIMITY][TTS] Error speaking prompt: $e'); } return false; } }