233 lines
8.4 KiB
Dart
233 lines
8.4 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'package:miler/data/api_config.dart';
|
|
import 'package:miler/data/miler_api.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// DUTY, BREAKS AND THE LOCATION HEARTBEAT
|
|
///
|
|
/// ── What was deleted here ──
|
|
///
|
|
/// A `_buildSslBypassClient()` that returned an `IOClient` whose
|
|
/// `badCertificateCallback` returned `true` for every certificate, pinned a
|
|
/// hardcoded IP for `queue.workolik.com`, and hand-rolled its own
|
|
/// `SecureSocket` upgrade. It existed to work around a carrier's broken DNS on
|
|
/// a backend that is no longer deployed — and it meant every request it carried
|
|
/// could be read and rewritten by anything on the path, including a rider's GPS
|
|
/// track and his duty state.
|
|
///
|
|
/// It is gone along with the rest of the legacy path. `api.doormile.com` is
|
|
/// reached over ordinary, verified TLS.
|
|
///
|
|
/// ── The class names survive their implementations ──
|
|
///
|
|
/// `RiderLogController` drives duty through create/update/get "rider log"
|
|
/// calls, which is the legacy shape. Rather than rewrite that controller, these
|
|
/// four classes keep their names and map the intent onto the real endpoints:
|
|
/// the first log of a session starts duty, later ones are location heartbeats,
|
|
/// and an off-duty payload ends it.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
String _s(dynamic v) => v == null ? '' : v.toString();
|
|
|
|
double? _d(dynamic v) => double.tryParse(_s(v));
|
|
|
|
/// Legacy `{status, details}` for the duty-state readers.
|
|
Map<String, dynamic> _dutyEnvelope(Map data) {
|
|
final bool on = data['onduty'] == true;
|
|
final dutylogid = data['dutylogid'] ?? 0;
|
|
return {
|
|
'status': true,
|
|
'code': 200,
|
|
'details': {
|
|
'logid': dutylogid,
|
|
'onduty': on ? 1 : 0,
|
|
'login': data['loginat'],
|
|
},
|
|
};
|
|
}
|
|
|
|
Future<Map<String, dynamic>?> _startDuty(Map data) async {
|
|
final res = await MilerApi.startDuty(
|
|
lat: _d(data['latitude']),
|
|
lon: _d(data['longitude']),
|
|
);
|
|
if (res.ok) {
|
|
final id = int.tryParse('${res.map['dutylogid'] ?? 0}') ?? 0;
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setInt('logid', id);
|
|
await prefs.setInt('logId', id);
|
|
await prefs.setInt('dutylogid', id);
|
|
return {
|
|
'status': true,
|
|
'code': 200,
|
|
'details': {
|
|
'logid': id,
|
|
'login': res.map['loginat'],
|
|
'onduty': 1,
|
|
},
|
|
};
|
|
}
|
|
// "Already on duty" is a 400 and is not a failure — it is the app and the
|
|
// server disagreeing about state, which the current duty answers.
|
|
if (res.status == 400) return _currentDuty();
|
|
return {'status': false, 'code': res.status};
|
|
}
|
|
|
|
Future<Map<String, dynamic>?> _currentDuty() async {
|
|
final res = await MilerApi.dutyCurrent();
|
|
if (!res.ok) return {'status': false, 'code': res.status};
|
|
|
|
final on = res.map['onduty'] == true;
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setInt('onduty', on ? 1 : 0);
|
|
if (on) {
|
|
final id = int.tryParse('${res.map['dutylogid'] ?? 0}') ?? 0;
|
|
await prefs.setInt('logid', id);
|
|
await prefs.setInt('logId', id);
|
|
}
|
|
return _dutyEnvelope(res.map);
|
|
}
|
|
|
|
/// The GPS heartbeat, which is two writes with different lifetimes:
|
|
///
|
|
/// • `PUT /miler/location` — Redis, and the index dispatch searches. This is
|
|
/// the one that decides whether the rider is findable.
|
|
/// • `POST /miler/logs` — the telemetry trail, for reconstructing a shift
|
|
/// afterwards.
|
|
///
|
|
/// Both are best-effort and neither blocks the rider. The location write is
|
|
/// reported back because duty state depends on it; the log is fire-and-forget.
|
|
Future<Map<String, dynamic>?> _heartbeat(Map data) async {
|
|
final lat = _d(data['latitude']);
|
|
final lon = _d(data['longitude']);
|
|
if (lat == null || lon == null) return ApiConfig.okEnvelope('no fix');
|
|
|
|
final res = await MilerApi.pushLocation(
|
|
latitude: lat,
|
|
longitude: lon,
|
|
pincode: _s(data['pincode']).isEmpty ? null : _s(data['pincode']),
|
|
speed: _d(data['speed']),
|
|
heading: _d(data['heading']),
|
|
);
|
|
|
|
// Deliberately not awaited into the result: a telemetry failure must never
|
|
// look like a location failure, because the caller treats the latter as duty
|
|
// state going wrong.
|
|
unawaited(
|
|
MilerApi.postLog(
|
|
latitude: lat,
|
|
longitude: lon,
|
|
speed: _d(data['speed']),
|
|
heading: _d(data['heading']),
|
|
accuracy: _d(data['accuracy']),
|
|
status: _s(data['status']).isEmpty ? null : _s(data['status']),
|
|
orderId: data['orderid'],
|
|
battery: int.tryParse(_s(data['battery'])),
|
|
isCharging: data['is_charging'] == true,
|
|
connection: _s(data['connection']).isEmpty
|
|
? null
|
|
: _s(data['connection']),
|
|
isBackground: data['is_background'] == true,
|
|
),
|
|
);
|
|
|
|
return res.ok ? ApiConfig.okEnvelope() : {'status': false, 'code': res.status};
|
|
}
|
|
|
|
/// True when the payload means "go off duty".
|
|
bool _isOffDutyIntent(Map data) {
|
|
final status = _s(data['status']).toLowerCase();
|
|
final onduty = data['onduty'];
|
|
final bool ondutyZero = onduty == 0 || _s(onduty) == '0';
|
|
return status.contains('offline') ||
|
|
status.contains('logout') ||
|
|
data.containsKey('logout') ||
|
|
(data.containsKey('onduty') && ondutyZero);
|
|
}
|
|
|
|
class CreateRiderLogProvider {
|
|
/// The first call of a session starts duty; every later one is a heartbeat.
|
|
Future<Map<String, dynamic>?> createRiderLog(
|
|
Map<String, dynamic> data,
|
|
) async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final int existing = prefs.getInt('dutylogid') ?? 0;
|
|
if (existing <= 0) return _startDuty(data);
|
|
return _heartbeat(data);
|
|
}
|
|
}
|
|
|
|
class UpdateRiderLogProvider {
|
|
Future<Map<String, dynamic>?> updateRiderLog(
|
|
Map<String, dynamic> data,
|
|
) async {
|
|
if (_isOffDutyIntent(data)) {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setInt('dutylogid', 0);
|
|
final res = await MilerApi.endDuty();
|
|
if (res.ok) await prefs.setInt('onduty', 0);
|
|
return res.ok
|
|
? ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true})
|
|
: {'status': false, 'code': res.status};
|
|
}
|
|
|
|
final onduty = data['onduty'];
|
|
if (onduty == 1 || _s(onduty) == '1') {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
if ((prefs.getInt('dutylogid') ?? 0) <= 0) return _startDuty(data);
|
|
}
|
|
return _heartbeat(data);
|
|
}
|
|
}
|
|
|
|
class GetRiderLogProvider {
|
|
Future<Map<String, dynamic>?> getRiderLog() => _currentDuty();
|
|
|
|
/// There is no rider-count endpoint in the contract, and no screen that needs
|
|
/// one — the dashboard figures come from bookings and earnings. Kept as a
|
|
/// zero so the one legacy caller does not have to branch.
|
|
Future<Map<String, dynamic>?> getRiderCount() async {
|
|
ApiConfig.logGap('getRiderCount', 'No count endpoint in v1.');
|
|
return {'status': true, 'count': 0};
|
|
}
|
|
}
|
|
|
|
class BreakRiderLogProvider {
|
|
Future<Map<String, dynamic>?> createBreakRiderLog(
|
|
Map<String, dynamic> data,
|
|
) async {
|
|
final bt = _s(data['breaktype'] ?? data['reason'] ?? 'Personal');
|
|
final res = await MilerApi.startBreak(bt);
|
|
if (res.ok) {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setInt(
|
|
'breaklogid',
|
|
int.tryParse('${res.map['breaklogid'] ?? 0}') ?? 0,
|
|
);
|
|
// The availability enum's break value is `Break`, not `On_Break` — the
|
|
// obvious guess is the wrong one and is silently rejected.
|
|
unawaited(MilerApi.setAvailability('Break'));
|
|
return ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true});
|
|
}
|
|
return {'status': false, 'code': res.status};
|
|
}
|
|
|
|
Future<Map<String, dynamic>?> updateBreakRiderLog() async {
|
|
final res = await MilerApi.endBreak();
|
|
if (res.ok) unawaited(MilerApi.setAvailability('Available'));
|
|
return res.ok
|
|
? ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true})
|
|
: {'status': false, 'code': res.status};
|
|
}
|
|
}
|
|
|
|
/// Local `unawaited`, so a fire-and-forget call reads as deliberate rather than
|
|
/// as a missing `await`.
|
|
void unawaited(Future<void> future) {
|
|
future.catchError((Object e) {
|
|
debugPrint('[RIDERLOG] background call failed: $e');
|
|
});
|
|
}
|