Files
Xpress-rider/lib/background/backgroundservice.dart

970 lines
35 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> 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<Map<String, dynamic>>().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<String, String>? coords = await _getCoordinatesWithFallback();
if (coords != null) {
await _checkProximityAlert(order, coords);
}
// Get payload from SharedPreferences or create new one
Map<String, dynamic>? 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<void> _postDeliveryLog(
String orderId,
Map<String, dynamic> payload, {
Map<String, String>? currentCoords,
}) async {
Map<String, dynamic>? 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 = <String, dynamic>{
...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<String, dynamic> _createPayload(
Map<String, dynamic> delivery,
int userId,
) {
return <String, dynamic>{
'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<Map<String, String>?> _getCoordinatesWithFallback() async {
Map<String, String> 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<Map<String, String>?> _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<void> _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<Map<String, dynamic>?> _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<String, dynamic>) return decoded;
if (decoded is Map) return decoded.cast<String, dynamic>();
} catch (_) {}
return null;
}
/// Persist payload to SharedPreferences
static Future<void> _persistPayload(
String orderId,
Map<String, dynamic> payload,
) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('$_payloadKeyPrefix$orderId', jsonEncode(payload));
} catch (_) {}
}
/// Mark order as active in SharedPreferences
static Future<void> _markAsActive(String orderId) async {
try {
final prefs = await SharedPreferences.getInstance();
final list = prefs.getStringList(_activeDeliveriesKey) ?? <String>[];
if (!list.contains(orderId)) {
list.add(orderId);
await prefs.setStringList(_activeDeliveriesKey, list);
}
} catch (_) {}
}
/// Fetch deliveries from API using v1 endpoint
static Future<List<dynamic>> _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<String, dynamic>
? (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<int> _resolveLogStatus(
String orderId,
Map<String, String> 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<String, dynamic>) {
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<void> _persistLastLogLocation(
String orderId,
Map<String, String> 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<void> _flushOfflineLogs() async {
try {
final prefs = await SharedPreferences.getInstance();
final List<String> queue = prefs.getStringList(_offlineLogKey) ?? [];
if (queue.isEmpty) return;
debugPrint(
'[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Flushing ${queue.length} offline logs...',
);
final List<String> remaining = [];
bool anySuccess = false;
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createDeliveryLogLive
: ApiConstants.createDeliveryLogDev;
for (final itemStr in queue) {
try {
final Map<String, dynamic> item = jsonDecode(itemStr);
final String orderId = item['orderId'] ?? '';
final Map<String, dynamic> payload = Map<String, dynamic>.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<void> _saveToOfflineQueue(
String orderId,
Map<String, dynamic> payload,
) async {
try {
final prefs = await SharedPreferences.getInstance();
final List<String> 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<void> 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<void> _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<String, String>? 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 = <String, dynamic>{
"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<void> _checkProximityAlert(
Map<String, dynamic> order,
Map<String, String> 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<bool> _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;
}
}