1282 lines
45 KiB
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();
|
|
}
|
|
}
|