import 'package:get/get.dart'; import 'package:flutter/foundation.dart'; import 'dart:convert'; import 'package:nearle/views/helpers/constants/apiconstants.dart'; import 'package:nearle/Models/riders/riders_models.dart'; import 'package:nearle/providers/Riderlog/riderlog_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:async'; import 'package:geolocator/geolocator.dart'; import 'dart:math'; import 'dart:io' show Platform; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:nearle/helpers/shift_end_alarm.dart'; import 'package:nearle/controllers/logcontroller.dart'; import 'package:nearle/background/foreground_service.dart' as fg; import 'package:nearle/utils/kalman_filter.dart'; import 'package:nearle/utils/mqtt_service.dart'; import 'package:nearle/views/helpers/constants/mqtt_constants.dart'; import 'package:battery_plus/battery_plus.dart'; class RiderLogController extends GetxController { final _createProvider = CreateRiderLogProvider(); final _updateProvider = UpdateRiderLogProvider(); final _getProvider = GetRiderLogProvider(); final _breakProvider = BreakRiderLogProvider(); final RxBool isLoading = false.obs; final RxList riderLogs = [].obs; final RxInt riderCount = 0.obs; bool _isEnsuringSession = false; bool _startBreakInFlight = false; bool _endBreakInFlight = false; StreamSubscription? _autoLoginSubscription; StreamSubscription? _batterySubscription; bool _lowBatteryAlertSent = false; NearleKalmanFilter? _kf; DateTime? _lastUpdateTime; Future fetchRiderLogs({required String userId}) async { isLoading.value = true; try { final base = ApiConstants.mainRoute == 'live' ? ApiConstants.getRiderLogLive : ApiConstants.getRiderLogDev; final url = "$base?userid=$userId"; final response = await _getProvider.getRiderLog(url); // Handle either {details: {...}} or {data: [...]} shapes if (response == null) { riderLogs.clear(); } else if (response['details'] is Map) { final Map details = response['details'] as Map; // Only accept if non-zero logid or userid present final int logIdVal = int.tryParse('${details['logid'] ?? 0}') ?? 0; if (logIdVal > 0) { riderLogs.value = [RiderLog.fromJson(details)]; } else { riderLogs.clear(); } } else if (response['data'] is List) { final List dataList = response['data'] as List; riderLogs.value = dataList .whereType>() .map((e) => RiderLog.fromJson(e)) .toList(); } else { riderLogs.clear(); } // Persist key identifiers for later API calls (only if valid) try { if (riderLogs.isNotEmpty) { final RiderLog current = riderLogs.first; final prefs = await SharedPreferences.getInstance(); if ((current.userid ?? 0) > 0) { await prefs.setInt('userid', current.userid!); } if ((current.partnerid ?? 0) > 0) { await prefs.setInt('partnerid', current.partnerid!); await prefs.setInt('partnerId', current.partnerid!); } if ((current.shiftid ?? 0) > 0) { await prefs.setInt('shiftid', current.shiftid!); await prefs.setInt('shiftId', current.shiftid!); } if ((current.logid ?? 0) > 0) { await prefs.setInt('logid', current.logid!); await prefs.setInt('logId', current.logid!); } if (current.onduty != null) { await prefs.setInt('onduty', current.onduty!); } if ((current.tenantid ?? 0) > 0) { await prefs.setInt('tenantid', current.tenantid!); } if ((current.locationid ?? 0) > 0) { await prefs.setInt('locationid', current.locationid!); } if ((current.applocationid ?? 0) > 0) { await prefs.setInt('applocationid', current.applocationid!); } if (current.userfcmtoken != null && current.userfcmtoken!.isNotEmpty) { await prefs.setString('userfcmtoken', current.userfcmtoken!); } // Persist full rider log template from server for future rider log payloads. // Latitude/longitude will always be overridden with real GPS values when sending. try { await prefs.setString( 'riderlog_template', jsonEncode(current.toJson()), ); } catch (_) {} } } catch (_) {} } finally { isLoading.value = false; } } Future fetchRiderCount({required String userId}) async { final base = ApiConstants.mainRoute == 'live' ? ApiConstants.getRiderCountLive : ApiConstants.getRiderCountDev; final url = "$base?userid=$userId"; final response = await _getProvider.getRiderCount(url); final dynamic cnt = response != null ? response['count'] : null; riderCount.value = cnt is num ? cnt.toInt() : int.tryParse('${cnt ?? 0}') ?? 0; } Future createLogin(RiderLogin login) async { final base = ApiConstants.mainRoute == 'live' ? ApiConstants.createRiderLogLive : ApiConstants.createRiderLogDev; final ok = await _createProvider.createRiderLog( base, _CreateRiderLogRequestCompat.fromRiderLogin(login), ); return ok != null && ok.isNotEmpty; } Future updateLog(RiderUpdate update) async { final base = ApiConstants.mainRoute == 'live' ? ApiConstants.updateRiderLogLive : ApiConstants.updateRiderLogDev; final ok = await _updateProvider.updateRiderLog( base, _UpdateRiderLogRequestCompat.fromRiderUpdate(update), ); return ok != null && ok.isNotEmpty; } Future createBreak(RiderBreak brk) async { final base = ApiConstants.mainRoute == 'live' ? ApiConstants.createBreakRiderLogLive : ApiConstants.createBreakRiderLogDev; final ok = await _breakProvider.createBreakRiderLog( base, _BreakLogRequestCompat.fromRiderBreak(brk), ); return ok != null && ok.isNotEmpty; } Future updateBreak(RiderBreak brk) async { final base = ApiConstants.mainRoute == 'live' ? ApiConstants.updateBreakRiderLogLive : ApiConstants.updateBreakRiderLogDev; final ok = await _breakProvider.updateBreakRiderLog( base, _BreakLogRequestCompat.fromRiderBreak(brk), ); return ok != null && ok.isNotEmpty; } Future updateBreakCustom({ required int breakid, required int logid, required String breakdate, required int userid, required int partnerid, required int shiftid, required String breakstart, required String breakend, required double breakhours, required String latitude, required String longitude, }) async { final url = ApiConstants.mainRoute == 'live' ? ApiConstants.updateBreakRiderLogLive : ApiConstants.updateBreakRiderLogDev; final payload = { "breakid": breakid, "logid": logid, "breakdate": breakdate, "userid": userid, "partnerid": partnerid, "shiftid": shiftid, "breakstart": breakstart, "breakend": breakend, "breakhours": breakhours, "latitude": latitude, "longitude": longitude, }; try { // Debug: show exact URL and JSON body debugPrint('[BREAK][UPDATE] URL: $url'); debugPrint('[BREAK][UPDATE] Body: $payload'); final response = await _breakProvider.updateBreakRiderLog(url, payload); // Debug: show raw response debugPrint( '[BREAK][UPDATE] Response: ${response == null ? 'null' : response.toString()}', ); return response != null && response.isNotEmpty; } catch (e) { return false; } } // Public helper: create login immediately after successful auth Future createLoginNowV2() async { try { final prefs = await SharedPreferences.getInstance(); final int onduty = prefs.getInt('onduty') ?? 0; if (onduty != 1) { debugPrint( '[RIDERLOG][CREATE LOGIN NOW] Skipped because onduty=$onduty', ); return false; } final int? userid = prefs.getInt('userId') ?? prefs.getInt('userid'); final int? partnerid = prefs.getInt('partnerId') ?? prefs.getInt('partnerid'); final int? shiftid = prefs.getInt('shiftId') ?? prefs.getInt('shiftid'); final int? tenantid = prefs.getInt('tenantid'); final int? locationid = prefs.getInt('locationid'); final int? applocationid = prefs.getInt('applocationid'); final String? userfcmtoken = prefs.getString('userfcmtoken'); // Prefer explicit username, then fallback to stored full name or first/last String? username = prefs.getString('username'); username ??= prefs.getString('user_name'); if (username == null || username.trim().isEmpty) { final first = prefs.getString('firstname') ?? ''; final last = prefs.getString('lastname') ?? ''; final combined = ('$first $last').trim(); if (combined.isNotEmpty) { username = combined; } } if ((userid ?? 0) == 0) return false; // ✅ Check if there are active deliveries to set status final bool hasActiveDeliveries = prefs.getBool('has_live_deliveries') ?? false; final String riderStatus = hasActiveDeliveries ? 'active' : 'idle'; final String orderId = prefs.getString('current_riding_order_id') ?? ''; debugPrint( '[RIDERLOG][CREATE LOGIN NOW] Active deliveries: $hasActiveDeliveries -> status: $riderStatus', ); final now = DateTime.now(); final iso = _formatDateTimeFull(now); // e.g. 2025-10-30 15:00:00 final loginTime = _formatTime(now); // e.g. 12:00:00 final loc = await _ensureLatLng('0', '0'); // Start from rider log template stored after login, then override // dynamic fields like date/time and GPS coordinates. Map baseTemplate = {}; try { final rawTemplate = prefs.getString('riderlog_template'); if (rawTemplate != null && rawTemplate.isNotEmpty) { final decoded = jsonDecode(rawTemplate); if (decoded is Map) { baseTemplate = decoded; } else if (decoded is Map) { baseTemplate = decoded.cast(); } } } catch (_) {} final Map payload = { // Base from server template ...baseTemplate, // Always ensure required identifiers are correct 'logid': baseTemplate['logid'], // let server decide new logid 'userid': userid, 'partnerid': partnerid, 'shiftid': shiftid, // Always override with current time and real GPS 'logdate': iso, 'login': loginTime, 'latitude': loc['lat'] ?? '0', 'longitude': loc['lng'] ?? '0', 'raw_latitude': loc['raw_lat'] ?? '0', 'raw_longitude': loc['raw_lng'] ?? '0', 'velocity_lat': loc['velocity_lat'] ?? '0', 'velocity_lng': loc['velocity_lng'] ?? '0', 'speed': loc['speed'] ?? '0', 'heading': loc['heading'] ?? '0', 'onduty': 1, 'status': riderStatus, 'contactno': prefs.getString('contactno') ?? '', 'tenantid': tenantid ?? 0, 'locationid': locationid ?? 0, 'applocationid': applocationid ?? 0, 'userfcmtoken': userfcmtoken ?? '', 'orderid': orderId, }; // --- MQTT LOGIC ( Lane Split ) --- final mqttService = NearleMqttService(); if (!mqttService.isConnected) { await mqttService.connect(); } // 1. Lane: Status mqttService.updateStatus(riderStatus == 'active' ? 'Active' : MqttConstants.statusOnline); // 2. Lane: Profile (Send only if significantly changed or first time) // For simplicity, we send it here once when duty starts or during logs, // but in a separate topic so it doesn't clutter the GPS stream. mqttService.publishProfile({ 'userid': userid, 'username': (username ?? '').trim(), 'firstname': prefs.getString('firstname') ?? '', 'lastname': prefs.getString('lastname') ?? '', 'contactno': prefs.getString('contactno') ?? '', 'userfcmtoken': userfcmtoken ?? '', 'app_version': prefs.getString('CurrentVersion') ?? '', 'device_info': Platform.operatingSystem, }); // 3. Lane: Periodic Log (Lightweight) mqttService.publishLog('rider_periodic_log', { 'userid': userid, 'logdate': iso, 'latitude': loc['lat'] ?? '0', 'longitude': loc['lng'] ?? '0', 'speed': loc['speed'] ?? '0', 'heading': loc['heading'] ?? '0', 'status': riderStatus, 'orderid': orderId, }); // Ensure rider identity fields are clean: // - always send username key (even if empty) so backend sees it payload['username'] = (username ?? '').trim(); final String cNo = prefs.getString('contactno') ?? ''; debugPrint('[RIDERLOG] Contact No from Prefs: "$cNo"'); payload['contactno'] = cNo; final firstName = prefs.getString('firstname') ?? ''; final lastName = prefs.getString('lastname') ?? ''; if (firstName.trim().isNotEmpty) { payload['firstname'] = firstName.trim(); } else { payload.remove('firstname'); } if (lastName.trim().isNotEmpty) { payload['lastname'] = lastName.trim(); } else { payload.remove('lastname'); } final base = ApiConstants.mainRoute == 'live' ? ApiConstants.createRiderLogLive : ApiConstants.createRiderLogDev; debugPrint('[RIDERLOG][CREATE LOGIN NOW] URL: $base'); debugPrint('[RIDERLOG][CREATE LOGIN NOW] Payload: $payload'); final resp = await _createProvider.createRiderLog(base, payload); debugPrint( '[RIDERLOG][CREATE LOGIN NOW] Response: ${resp == null ? 'null' : resp.toString()}', ); if (resp == null || resp.isEmpty) { // Offline fallback debugPrint( '[RIDERLOG][CREATE LOGIN NOW] Failed, saving to offline queue', ); await _saveToOfflineQueue(base, payload); return false; } final det = (resp['details'] is Map) ? (resp['details'] as Map) : resp; final newLogId = int.tryParse('${det['logid'] ?? 0}') ?? (det['logid'] as int? ?? 0); if (newLogId > 0) { await prefs.setInt('logId', newLogId); await prefs.setInt('logid', newLogId); } // Attempt to flush any other pending logs since we have a success flushOfflineLogs(); return true; } catch (_) { // Offline fallback on exception try { final prefs = await SharedPreferences.getInstance(); final int? userid = prefs.getInt('userId') ?? prefs.getInt('userid'); final int? partnerid = prefs.getInt('partnerId') ?? prefs.getInt('partnerid'); final int? shiftid = prefs.getInt('shiftId') ?? prefs.getInt('shiftid'); final int? tenantid = prefs.getInt('tenantid'); final int? locationid = prefs.getInt('locationid'); final int? applocationid = prefs.getInt('applocationid'); final String? userfcmtoken = prefs.getString('userfcmtoken'); // Prefer explicit username, then fallback to stored full name or first/last String? username = prefs.getString('username'); username ??= prefs.getString('user_name'); if (username == null || username.trim().isEmpty) { final first = prefs.getString('firstname') ?? ''; final last = prefs.getString('lastname') ?? ''; final combined = ('$first $last').trim(); if (combined.isNotEmpty) { username = combined; } } if ((userid ?? 0) != 0) { final now = DateTime.now(); final iso = _formatDateTimeFull(now); final loginTime = _formatTime(now); final loc = await _ensureLatLng('0', '0'); // Offline fallback also uses stored template plus real GPS/time. Map baseTemplate = {}; try { final rawTemplate = prefs.getString('riderlog_template'); if (rawTemplate != null && rawTemplate.isNotEmpty) { final decoded = jsonDecode(rawTemplate); if (decoded is Map) { baseTemplate = decoded; } else if (decoded is Map) { baseTemplate = decoded.cast(); } } } catch (_) {} // Check status for offline fallback too final bool hasActiveDeliveries = prefs.getBool('has_live_deliveries') ?? false; final String riderStatus = hasActiveDeliveries ? 'active' : 'idle'; final Map payload = { ...baseTemplate, 'logid': baseTemplate['logid'], 'userid': userid, 'partnerid': partnerid, 'shiftid': shiftid, 'logdate': iso, 'login': loginTime, 'latitude': loc['lat'] ?? '0', 'longitude': loc['lng'] ?? '0', 'raw_latitude': loc['raw_lat'] ?? '0', 'raw_longitude': loc['raw_lng'] ?? '0', 'velocity_lat': loc['velocity_lat'] ?? '0', 'velocity_lng': loc['velocity_lng'] ?? '0', 'speed': loc['speed'] ?? '0', 'heading': loc['heading'] ?? '0', 'onduty': 1, 'status': riderStatus, 'contactno': prefs.getString('contactno') ?? '', 'tenantid': tenantid ?? 0, 'locationid': locationid ?? 0, 'applocationid': applocationid ?? 0, 'userfcmtoken': userfcmtoken ?? '', }; // Ensure rider identity fields are clean in offline payload too // Always send username key (even if empty) for parity with online payload payload['username'] = (username ?? '').trim(); final firstName = prefs.getString('firstname') ?? ''; final lastName = prefs.getString('lastname') ?? ''; if (firstName.trim().isNotEmpty) { payload['firstname'] = firstName.trim(); } else { payload.remove('firstname'); } if (lastName.trim().isNotEmpty) { payload['lastname'] = lastName.trim(); } else { payload.remove('lastname'); } final base = ApiConstants.mainRoute == 'live' ? ApiConstants.createRiderLogLive : ApiConstants.createRiderLogDev; await _saveToOfflineQueue(base, payload); } } catch (e) { debugPrint('[RIDERLOG] Error saving offline log: $e'); } return false; } } // Toggle onduty via updateLog Future setOnDuty(bool on) async { try { final prefs = await SharedPreferences.getInstance(); final int? userid = prefs.getInt('userId') ?? prefs.getInt('userid'); if ((userid ?? 0) == 0) return false; final loc = await _ensureLatLng('0', '0'); final payload = RiderUpdate( userid: userid, onduty: on ? 1 : 0, latitude: loc['lat'] ?? '0', longitude: loc['lng'] ?? '0', ); debugPrint('[RIDERLOG][SET ONDUTY] -> ${payload.toJson()}'); final ok = await updateLog(payload); if (ok) { await prefs.setInt('onduty', on ? 1 : 0); if (on) { await createLoginNowV2(); final int interval = prefs.getInt('logseconds') ?? 0; if (interval > 0) { startAutoCreateLoginLoop(seconds: interval); } // Ensure foreground logging notification is started when going on-duty try { if (Get.isRegistered()) { await Get.find().startLogging(); } } catch (_) {} // ✅ Schedule shift end alarm (works even when app is killed) try { final String endTime = prefs.getString('endtime') ?? ''; final String startTime = prefs.getString('starttime') ?? ''; debugPrint( '[RIDERLOG][SET ONDUTY] 📅 Checking shift times - endTime: "$endTime", startTime: "$startTime"', ); if (endTime.isNotEmpty) { debugPrint( '[RIDERLOG][SET ONDUTY] 📅 Scheduling shift end alarm for: $endTime', ); final scheduled = await ShiftEndAlarm.scheduleAlarm( endTime: endTime, startTime: startTime, ); if (scheduled) { debugPrint( '[RIDERLOG][SET ONDUTY] ✅ Successfully scheduled shift end alarm for: $endTime', ); } else { debugPrint( '[RIDERLOG][SET ONDUTY] ⚠️ Failed to schedule shift end alarm for: $endTime', ); } } else { debugPrint( '[RIDERLOG][SET ONDUTY] ⚠️ endTime is empty - cannot schedule alarm', ); } } catch (e) { debugPrint('[RIDERLOG][SET ONDUTY] ❌ Error scheduling alarm: $e'); } _startBatteryMonitoring(); // Force MQTT connection on duty start NearleMqttService().connect(); } else { await stopAutoCreateLoginLoop(); _stopBatteryMonitoring(); // ✅ Cancel shift end alarm when going offline try { await ShiftEndAlarm.cancelAlarm(); debugPrint('[RIDERLOG][SET ONDUTY] ✅ Cancelled shift end alarm'); } catch (e) { debugPrint('[RIDERLOG][SET ONDUTY] Error cancelling alarm: $e'); } // --- MQTT LOGIC --- // disconnect() publishes Offline status then fully closes the connection, // freeing the broker slot and resetting _currentRiderId for a clean reconnect. NearleMqttService().disconnect(); } } return ok; } catch (_) { return false; } } // Start periodic createRiderLog calls based on seconds (or prefs 'logseconds') // ✅ CRITICAL: When there are active deliveries, use 30 seconds (same as delivery logs) // Otherwise, use the configured logseconds interval void startAutoCreateLoginLoop({int? seconds}) async { final prefs = await SharedPreferences.getInstance(); // ✅ Check if there are active deliveries - if yes, use 30 seconds (same as delivery logs) final bool hasActiveDeliveries = prefs.getBool('has_live_deliveries') ?? false; final int baseInterval = seconds ?? (prefs.getInt('logseconds') ?? 0); // When there are active deliveries, post rider logs every 30 seconds (matching delivery logs) // Otherwise, use the configured interval final int interval = hasActiveDeliveries ? 30 : baseInterval; if (hasActiveDeliveries && interval != 30) { debugPrint( '[RIDERLOG][AUTO LOOP] Active delivery detected - using 30 second interval (matching delivery logs)', ); } await stopAutoCreateLoginLoop(); // Cancel existing subscription // Attempt to flush offline logs on loop start flushOfflineLogs(); if (interval <= 0) return; final int onduty = prefs.getInt('onduty') ?? 0; if (onduty != 1) { debugPrint('[RIDERLOG][AUTO LOOP] Not starting - onduty=$onduty'); return; } if (Platform.isAndroid) { try { // Check if we can use foreground service (Android 14+ restrictions) // Initialize and start Android foreground service for reliable background ticks FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: 'nearle_bg_service', channelName: 'Background Service', channelDescription: 'Keeps Nearle online updates running in background.', channelImportance: NotificationChannelImportance.LOW, priority: NotificationPriority.LOW, ), iosNotificationOptions: const IOSNotificationOptions( showNotification: true, playSound: false, ), foregroundTaskOptions: ForegroundTaskOptions( interval: Duration(seconds: interval).inMilliseconds, isOnceEvent: false, autoRunOnBoot: false, allowWakeLock: true, allowWifiLock: true, ), ); // Check permissions before starting service to prevent Android 14 crash final permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { debugPrint( '[RIDERLOG] Location permission missing, cannot start foreground service', ); _startStreamBasedLoop(interval); return; } // Try to start the service with error handling ServiceRequestResult? started; try { started = await FlutterForegroundTask.startService( notificationTitle: 'Nearle is running', notificationText: 'Auto rider log active', callback: fg.riderLogCallback, ); } catch (error) { // If foreground service fails, fall back to stream-based approach debugPrint( '[RIDERLOG] Foreground service failed, using stream fallback: $error', ); } // If foreground service failed to start, use stream fallback if (started != ServiceRequestResult.success) { debugPrint( '[RIDERLOG] Using stream-based periodic updates instead of foreground service', ); _startStreamBasedLoop(interval); return; } } catch (e) { // If any error occurs, fall back to stream-based approach debugPrint( '[RIDERLOG] Error starting foreground service, using stream fallback: $e', ); _startStreamBasedLoop(interval); return; } } else { _startStreamBasedLoop(interval); } } // Stream-based periodic loop (fallback when foreground service is unavailable) void _startStreamBasedLoop(int interval) { _autoLoginSubscription?.cancel(); _autoLoginSubscription = Stream.periodic(Duration(seconds: interval), (_) {}) .asyncMap((_) async { await createLoginNowV2(); }) .listen( (_) {}, // Success handler onError: (error) { // Handle errors gracefully without crashing debugPrint('[RIDERLOG][STREAM ERROR] $error'); }, cancelOnError: false, // Continue even on errors ); } Future stopAutoCreateLoginLoop() async { if (Platform.isAndroid) { try { await FlutterForegroundTask.stopService(); } catch (e) { debugPrint('[RIDERLOG] Error stopping foreground service: $e'); } } await _autoLoginSubscription?.cancel(); _autoLoginSubscription = null; } void _startBatteryMonitoring() { _batterySubscription?.cancel(); final battery = Battery(); _batterySubscription = battery.onBatteryStateChanged.listen((BatteryState state) async { final level = await battery.batteryLevel; if (level <= 20 && !_lowBatteryAlertSent) { NearleMqttService().publishLog('critical_battery', { 'level': level, 'state': state.toString(), 'rider_id': riderLogs.isNotEmpty ? riderLogs.first.userid : 'unknown', 'timestamp': DateTime.now().toIso8601String(), }); _lowBatteryAlertSent = true; debugPrint('[MQTT] ⚠️ Critical Battery Alert Sent: $level%'); } else if (level > 25) { _lowBatteryAlertSent = false; // Reset if they started charging } }); } void _stopBatteryMonitoring() { _batterySubscription?.cancel(); _batterySubscription = null; } @override void onClose() { stopAutoCreateLoginLoop(); _stopBatteryMonitoring(); super.onClose(); } // ---------------- Break flow parity with xpressrider ---------------- String _two(int n) => n.toString().padLeft(2, '0'); String _formatDateTimeFull(DateTime dt) { final y = dt.year.toString(); final m = _two(dt.month); final d = _two(dt.day); final hh = _two(dt.hour); final mm = _two(dt.minute); final ss = _two(dt.second); return "$y-$m-$d $hh:$mm:$ss"; } String _formatTime(DateTime dt) { final hh = _two(dt.hour); final mm = _two(dt.minute); final ss = _two(dt.second); return "$hh:$mm:$ss"; } // ignore: unused_element String _durationToHourDotMinute(Duration d) { final hours = d.inHours; final minutes = d.inMinutes.remainder(60); return "$hours.${_two(minutes)}"; // e.g. 1.30 } Future> _ensureLatLng(String lat, String lng) async { Map result = { 'lat': lat, 'lng': lng, 'raw_lat': lat, 'raw_lng': lng, 'speed': '0', 'heading': '0', 'velocity_lat': '0', 'velocity_lng': '0', }; try { final needsFetch = (lat == '0' || lat.isEmpty || lng == '0' || lng.isEmpty); if (!needsFetch) return result; final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) return result; LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { return result; } final Position pos = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, ), ); final now = DateTime.now(); double outLat = pos.latitude; double outLng = pos.longitude; double speed = pos.speed; double heading = pos.heading; // Decompose velocity for Kalman final double headingRadians = heading * (pi / 180.0); final double velocityLng = speed * sin(headingRadians); final double velocityLat = speed * 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; return { 'lat': outLat.toStringAsFixed(6), 'lng': outLng.toStringAsFixed(6), 'raw_lat': pos.latitude.toStringAsFixed(6), 'raw_lng': pos.longitude.toStringAsFixed(6), 'speed': speed.toStringAsFixed(2), 'heading': heading.toStringAsFixed(2), 'velocity_lat': velocityLat.toStringAsFixed(4), 'velocity_lng': velocityLng.toStringAsFixed(4), }; } catch (_) { return result; } } Future startBreakAuto({ String latitude = '0', String longitude = '0', }) async { try { if (_startBreakInFlight) return false; _startBreakInFlight = true; // Resolve location if not provided final ll = await _ensureLatLng(latitude, longitude); latitude = ll['lat'] ?? latitude; longitude = ll['lng'] ?? longitude; final prefs = await SharedPreferences.getInstance(); int? userid = prefs.getInt('userId') ?? prefs.getInt('userid'); int? partnerid = prefs.getInt('partnerId') ?? prefs.getInt('partnerid'); int? shiftid = prefs.getInt('shiftId') ?? prefs.getInt('shiftid'); int? logid = prefs.getInt('logId') ?? prefs.getInt('logid'); // Ensure a rider log session exists (no GET calls) if ((userid ?? 0) == 0 || (partnerid ?? 0) == 0 || (shiftid ?? 0) == 0 || (logid ?? 0) == 0) { final ok = await _ensureLogSession(); if (ok) { userid = prefs.getInt('userId') ?? prefs.getInt('userid'); partnerid = prefs.getInt('partnerId') ?? prefs.getInt('partnerid'); shiftid = prefs.getInt('shiftId') ?? prefs.getInt('shiftid'); logid = prefs.getInt('logId') ?? prefs.getInt('logid'); } } // Default missing optional IDs to 0, require at least userid and a logid partnerid = partnerid ?? 0; shiftid = shiftid ?? 0; // If still missing, fall back to zeros to allow call and see server response userid = userid ?? 0; logid = logid ?? 0; final now = DateTime.now(); final int localBreakId = Random().nextInt(900) + 100; // 3-digit ID: 100-999 final breakdate = _formatDateTimeFull(now); // e.g. 2025-10-16 16:36:16 final breakstart = _formatTime(now); // e.g. 16:36:16 final url = ApiConstants.mainRoute == 'live' ? ApiConstants.createBreakRiderLogLive : ApiConstants.createBreakRiderLogDev; // Build payload with all required fields in the exact format the API expects final payload = { "breakid": localBreakId, "logid": logid, "breakdate": breakdate, // YYYY-MM-DD HH:MM:SS "userid": userid, "partnerid": partnerid, "shiftid": shiftid, "breakstart": breakstart, // HH:MM:SS "breakend": "", "breakhours": 0.0, "latitude": latitude, "longitude": longitude, }; // Debug logs for terminal visibility debugPrint('[BREAK][CREATE] URL: $url'); debugPrint('[BREAK][CREATE] Payload: $payload'); // Persist local break id immediately to ensure update can reference it await prefs.setInt('breakId', localBreakId); final resp = await _breakProvider.createBreakRiderLog(url, payload); debugPrint( '[BREAK][CREATE] Response: ${resp == null ? 'null' : resp.toString()}', ); if (resp == null || resp.isEmpty) return false; final det = (resp['details'] is Map) ? (resp['details'] as Map) : resp; await prefs.setString( 'breakStart', (det['breakstart'] ?? breakstart).toString(), ); final dynamic rawId = det['breakid'] ?? det['message_id'] ?? 0; final int serverBreakId = int.tryParse('$rawId') ?? 0; if (serverBreakId > 0) { await prefs.setInt('breakId', serverBreakId); } final int persistedLogId = int.tryParse('${det['logid'] ?? 0}') ?? (det['logid'] as int? ?? 0); await prefs.setInt('logId', persistedLogId); await prefs.setInt('break_start_epoch', now.millisecondsSinceEpoch); return true; } catch (_) { return false; } finally { _startBreakInFlight = false; } } Future endBreakAuto({ String latitude = '0', String longitude = '0', }) async { try { if (_endBreakInFlight) return false; _endBreakInFlight = true; // Resolve location if not provided final ll = await _ensureLatLng(latitude, longitude); latitude = ll['lat'] ?? latitude; longitude = ll['lng'] ?? longitude; final prefs = await SharedPreferences.getInstance(); int? userid = prefs.getInt('userId') ?? prefs.getInt('userid'); int? partnerid = prefs.getInt('partnerId') ?? prefs.getInt('partnerid'); int? shiftid = prefs.getInt('shiftId') ?? prefs.getInt('shiftid'); int? logid = prefs.getInt('logId') ?? prefs.getInt('logid'); int? breakid = prefs.getInt('breakId'); final startEpoch = prefs.getInt('break_start_epoch'); final savedBreakStart = prefs.getString('breakStart') ?? ''; // Ensure a rider log session exists (no GET calls) if ((userid ?? 0) == 0 || (partnerid ?? 0) == 0 || (shiftid ?? 0) == 0 || (logid ?? 0) == 0) { final ok = await _ensureLogSession(); if (ok) { userid = prefs.getInt('userId') ?? prefs.getInt('userid'); partnerid = prefs.getInt('partnerId') ?? prefs.getInt('partnerid'); shiftid = prefs.getInt('shiftId') ?? prefs.getInt('shiftid'); logid = prefs.getInt('logId') ?? prefs.getInt('logid'); } } // Default missing optional IDs to 0; require userid, logid, breakid, start time partnerid = partnerid ?? 0; shiftid = shiftid ?? 0; // If missing, default to zeros so we still hit API and get response for visibility userid = userid ?? 0; logid = logid ?? 0; breakid = breakid ?? 0; if (breakid == 0) { // No break in progress; nothing to update. // Remove any stale tracking data and exit gracefully. await prefs.remove('break_start_epoch'); await prefs.remove('breakId'); debugPrint( '[BREAK][UPDATE] Skipped endBreakAuto because breakId is 0/missing', ); return true; } final now = DateTime.now(); final breakdate = _formatDateTimeFull(now); final breakend = _formatTime(now); final int startEpochVal = startEpoch ?? DateTime.now().millisecondsSinceEpoch; final startTime = DateTime.fromMillisecondsSinceEpoch(startEpochVal); final duration = now.difference(startTime); final breakhoursDouble = duration.inSeconds / 3600.0; // Debug logs for terminal visibility debugPrint( '[BREAK][UPDATE] URL: ${ApiConstants.mainRoute == 'live' ? ApiConstants.updateBreakRiderLogLive : ApiConstants.updateBreakRiderLogDev}', ); debugPrint( '[BREAK][UPDATE] Fields: breakid=$breakid, logid=$logid, userid=$userid, partnerid=$partnerid, shiftid=$shiftid, breakend=$breakend, breakhours=$breakhoursDouble, lat=$latitude, lng=$longitude', ); return await updateBreakCustom( breakid: breakid, logid: logid, breakdate: breakdate, userid: userid, partnerid: partnerid, shiftid: shiftid, breakstart: savedBreakStart, breakend: breakend, breakhours: breakhoursDouble, latitude: latitude, longitude: longitude, ); } catch (_) { return false; } finally { _endBreakInFlight = false; } } // Ensure rider log session by creating login if missing; persists logid/ids. Future _ensureLogSession() async { try { if (_isEnsuringSession) return false; _isEnsuringSession = true; final prefs = await SharedPreferences.getInstance(); final int onduty = prefs.getInt('onduty') ?? 0; if (onduty != 1) { debugPrint('[RIDERLOG][ENSURE SESSION] Skipped because onduty=$onduty'); return false; } int? userid = prefs.getInt('userId') ?? prefs.getInt('userid'); int? partnerid = prefs.getInt('partnerId') ?? prefs.getInt('partnerid'); int? shiftid = prefs.getInt('shiftId') ?? prefs.getInt('shiftid'); int? logid = prefs.getInt('logId') ?? prefs.getInt('logid'); final int? tenantid = prefs.getInt('tenantid'); final int? locationid = prefs.getInt('locationid'); final int? applocationid = prefs.getInt('applocationid'); final String? userfcmtoken = prefs.getString('userfcmtoken'); if ((userid ?? 0) == 0) return false; if ((partnerid ?? 0) == 0) return false; if ((shiftid ?? 0) == 0) return false; if ((logid ?? 0) > 0) return true; final now = DateTime.now(); final iso = _formatDateTimeFull(now); final loginTime = _formatTime(now); final url = ApiConstants.mainRoute == 'live' ? ApiConstants.createRiderLogLive : ApiConstants.createRiderLogDev; // ✅ Check if there are active deliveries to set status final bool hasActiveDeliveries = prefs.getBool('has_live_deliveries') ?? false; final String riderStatus = hasActiveDeliveries ? 'active' : 'idle'; // Resolve rider display name (username) from prefs to include in log String? username = prefs.getString('username'); username ??= prefs.getString('user_name'); if (username == null || username.trim().isEmpty) { final first = prefs.getString('firstname') ?? ''; final last = prefs.getString('lastname') ?? ''; final combined = ('$first $last').trim(); if (combined.isNotEmpty) { username = combined; } } // Resolve current location for accurate login lat/lng final loc = await _ensureLatLng('0', '0'); final payload = RiderLogin( userid: userid, partnerid: partnerid, shiftid: shiftid, logdate: iso.substring(0, 10), login: loginTime, latitude: loc['lat'] ?? '0', longitude: loc['lng'] ?? '0', onduty: 1, status: riderStatus, // ✅ Add status field: "active" or "idle" username: username, tenantid: tenantid ?? 0, locationid: locationid ?? 0, applocationid: applocationid ?? 0, userfcmtoken: userfcmtoken ?? '', ).toJson(); debugPrint('[RIDERLOG][CREATE LOGIN] URL: $url'); debugPrint('[RIDERLOG][CREATE LOGIN] Payload: $payload'); final resp = await _createProvider.createRiderLog(url, payload); debugPrint( '[RIDERLOG][CREATE LOGIN] Response: ${resp == null ? 'null' : resp.toString()}', ); if (resp == null || resp.isEmpty) return false; final det = (resp['details'] is Map) ? (resp['details'] as Map) : resp; final newLogId = int.tryParse('${det['logid'] ?? 0}') ?? (det['logid'] as int? ?? 0); if (newLogId > 0) { await prefs.setInt('logId', newLogId); await prefs.setInt('logid', newLogId); return true; } return false; } catch (_) { return false; } finally { _isEnsuringSession = false; } } // ---------------- Offline Queue Logic ---------------- static const String _offlineLogKey = 'offline_rider_logs'; bool _isFlushing = false; /// Call this on app start or network restoration Future flushOfflineLogs() async { if (_isFlushing) return; _isFlushing = true; try { final prefs = await SharedPreferences.getInstance(); final List queue = prefs.getStringList(_offlineLogKey) ?? []; if (queue.isEmpty) return; debugPrint( '[RIDERLOG][OFFLINE] Flushing ${queue.length} offline logs...', ); final List remaining = []; bool anySuccess = false; for (final itemStr in queue) { try { final Map item = jsonDecode(itemStr); final String url = item['url'] ?? ''; final Map payload = Map.from( item['payload'] ?? {}, ); if (url.isEmpty || payload.isEmpty) continue; // Determine type of log based on URL or payload structure if needed // For now, we assume these are mostly createRiderLog calls from createLoginNow // We can use _createProvider generic call debugPrint('[RIDERLOG][OFFLINE] Retrying: $payload'); final resp = await _createProvider.createRiderLog(url, payload); if (resp != null && resp.isNotEmpty) { debugPrint('[RIDERLOG][OFFLINE] Success!'); anySuccess = true; } else { // Keep in queue if failed remaining.add(itemStr); } } catch (e) { debugPrint('[RIDERLOG][OFFLINE] Error processing item: $e'); remaining.add(itemStr); // Keep on error } } if (anySuccess || remaining.length != queue.length) { await prefs.setStringList(_offlineLogKey, remaining); debugPrint( '[RIDERLOG][OFFLINE] Flush complete. Remaining: ${remaining.length}', ); } } catch (e) { debugPrint('[RIDERLOG][OFFLINE] Flush error: $e'); } finally { _isFlushing = false; } } Future _saveToOfflineQueue( String url, Map payload, ) async { try { final prefs = await SharedPreferences.getInstance(); List queue = prefs.getStringList(_offlineLogKey) ?? []; final item = jsonEncode({ 'url': url, 'payload': payload, 'timestamp': DateTime.now().millisecondsSinceEpoch, }); queue.add(item); // Cap the queue at 50 entries — keep the newest ones. // Prevents unbounded growth on devices where the endpoint is unreachable. const maxQueueSize = 50; if (queue.length > maxQueueSize) { queue = queue.sublist(queue.length - maxQueueSize); debugPrint('[RIDERLOG][OFFLINE] Queue capped at $maxQueueSize entries.'); } await prefs.setStringList(_offlineLogKey, queue); debugPrint('[RIDERLOG][OFFLINE] Saved to queue. Total: ${queue.length}'); } catch (e) { debugPrint('[RIDERLOG][OFFLINE] Error saving to queue: $e'); } } } // 🔹 Helper Adapters class _CreateRiderLogRequestCompat { static dynamic fromRiderLogin(RiderLogin login) { return login.toJson(); } } class _UpdateRiderLogRequestCompat { static dynamic fromRiderUpdate(RiderUpdate update) { return update.toJson(); } } class _BreakLogRequestCompat { static dynamic fromRiderBreak(RiderBreak brk) { return brk.toJson(); } }