Files
Xpress-rider/lib/controllers/riderlog.dart

1282 lines
45 KiB
Dart

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<RiderLog> riderLogs = <RiderLog>[].obs;
final RxInt riderCount = 0.obs;
bool _isEnsuringSession = false;
bool _startBreakInFlight = false;
bool _endBreakInFlight = false;
StreamSubscription<void>? _autoLoginSubscription;
StreamSubscription<BatteryState>? _batterySubscription;
bool _lowBatteryAlertSent = false;
NearleKalmanFilter? _kf;
DateTime? _lastUpdateTime;
Future<void> 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<String, dynamic>) {
final Map<String, dynamic> details =
response['details'] as Map<String, dynamic>;
// 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<dynamic> dataList = response['data'] as List<dynamic>;
riderLogs.value = dataList
.whereType<Map<String, dynamic>>()
.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<void> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<String, dynamic> baseTemplate = {};
try {
final rawTemplate = prefs.getString('riderlog_template');
if (rawTemplate != null && rawTemplate.isNotEmpty) {
final decoded = jsonDecode(rawTemplate);
if (decoded is Map<String, dynamic>) {
baseTemplate = decoded;
} else if (decoded is Map) {
baseTemplate = decoded.cast<String, dynamic>();
}
}
} catch (_) {}
final Map<String, dynamic> 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<String, dynamic>)
? (resp['details'] as Map<String, dynamic>)
: 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<String, dynamic> baseTemplate = {};
try {
final rawTemplate = prefs.getString('riderlog_template');
if (rawTemplate != null && rawTemplate.isNotEmpty) {
final decoded = jsonDecode(rawTemplate);
if (decoded is Map<String, dynamic>) {
baseTemplate = decoded;
} else if (decoded is Map) {
baseTemplate = decoded.cast<String, dynamic>();
}
}
} catch (_) {}
// Check status for offline fallback too
final bool hasActiveDeliveries =
prefs.getBool('has_live_deliveries') ?? false;
final String riderStatus = hasActiveDeliveries ? 'active' : 'idle';
final Map<String, dynamic> 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<bool> 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<LogController>()) {
await Get.find<LogController>().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<void> 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<Map<String, String>> _ensureLatLng(String lat, String lng) async {
Map<String, String> 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<bool> 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 = <String, dynamic>{
"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<String, dynamic>)
? (resp['details'] as Map<String, dynamic>)
: 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<bool> 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<bool> _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<String, dynamic>)
? (resp['details'] as Map<String, dynamic>)
: 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<void> flushOfflineLogs() async {
if (_isFlushing) return;
_isFlushing = true;
try {
final prefs = await SharedPreferences.getInstance();
final List<String> queue = prefs.getStringList(_offlineLogKey) ?? [];
if (queue.isEmpty) return;
debugPrint(
'[RIDERLOG][OFFLINE] Flushing ${queue.length} offline logs...',
);
final List<String> remaining = [];
bool anySuccess = false;
for (final itemStr in queue) {
try {
final Map<String, dynamic> item = jsonDecode(itemStr);
final String url = item['url'] ?? '';
final Map<String, dynamic> payload = Map<String, dynamic>.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<void> _saveToOfflineQueue(
String url,
Map<String, dynamic> payload,
) async {
try {
final prefs = await SharedPreferences.getInstance();
List<String> 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();
}
}