import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Base URL, bearer token, and the adapter that turns a v1 booking into the /// legacy stop shape the UI reads. /// /// ── The flag is gone ── /// /// This class used to carry `useNewApi`, a `--dart-define` switch between the /// v1 backend and a legacy one (`jupiter.doormile.app` / `queue.workolik.com`). /// Every provider branched on it, so the app shipped two implementations of /// every call and only one of them was ever exercised. /// /// It was also the switch that turned the mock layer on: `USE_NEW_API=false` /// gave a fake "Demo Rider" login that accepted any four digits, seeded demo /// routes, and invented earnings figures. A build flag that silently swaps /// authentication for a bypass is not a development convenience. /// /// One backend, one path, no flag. `MilerApi` is where the endpoints live; what /// remains here is the base URL, the token, and the booking→stop mapping. class ApiConfig { ApiConfig._(); /// API base URL (no trailing slash). static const String newBase = 'https://api.doormile.com/api/v1'; static String url(String path) => '$newBase${path.startsWith('/') ? path : '/$path'}'; // --------------------------------------------------------------------------- // Bearer token // --------------------------------------------------------------------------- static const String _kTokenKey = 'authtoken'; static Future setToken(String token) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_kTokenKey, token); } static Future getToken() async { final prefs = await SharedPreferences.getInstance(); final t = prefs.getString(_kTokenKey); return (t != null && t.isNotEmpty) ? t : null; } static Future clearToken() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_kTokenKey); } /// Headers for every authenticated request (Content-Type + Bearer token). static Future> authHeaders() async { final headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', }; final token = await getToken(); if (token != null) headers['Authorization'] = 'Bearer $token'; return headers; } // --------------------------------------------------------------------------- // Response envelope adapter: {success,data,message} -> {status,details} // --------------------------------------------------------------------------- /// Wrap a decoded new-API response into the legacy envelope the controllers /// expect. `_isSuccess()` reads `status` (bool); readers read `details`. static Map toLegacyEnvelope(dynamic decoded) { if (decoded is Map) { final success = decoded['success']; final bool ok = success is bool ? success : success == true; return { 'status': ok, 'code': ok ? 200 : 400, 'message': decoded['message']?.toString() ?? '', 'details': decoded['data'] ?? decoded['details'] ?? decoded, }; } // Bare list/value response. return {'status': true, 'code': 200, 'details': decoded}; } /// A generic "it worked" legacy envelope (for no-op / not-yet-supported flows). static Map okEnvelope([String message = '']) => {'status': true, 'code': 200, 'message': message}; // --------------------------------------------------------------------------- // STATUS mapping (new booking status <-> legacy orderstatus) // --------------------------------------------------------------------------- // // New (in order): Miler_Assigned -> Pickup_Scheduled -> At_Customer // -> Picked_Up -> Converted_To_Consignment -> Cancelled // Legacy the UI branches on: accepted / active / arrived / "Picked up" // / picked / skipped / cancelled / rejected static String legacyStatusFromNew(String? newStatus) { switch ((newStatus ?? '').trim()) { case 'Miler_Assigned': // Admin-assigned but NOT yet accepted by the rider. This must land on // the Home tab as a pending booking to accept/reject — it becomes // 'accepted' (a client-side marker in accepted_store) only once the // rider accepts it, which is what moves it to the Bookings tab. return 'assigned'; case 'Pickup_Scheduled': return 'active'; case 'At_Customer': return 'arrived'; case 'Picked_Up': return 'Picked up'; case 'Converted_To_Consignment': return 'picked'; case 'Cancelled': return 'cancelled'; default: return newStatus ?? ''; } } // --------------------------------------------------------------------------- // BOOKING -> legacy pickup object mapping // --------------------------------------------------------------------------- // // The UI reads legacy lowercase keys (pickupid, orderid, orderstatus, // pickuplat/pickuplong, dropaddress/droplat/droplon, pickupcustomer, // pickupcontactno, collectionamt, step, type ...). Translate a new `booking` // object into that shape so the existing cards/flows render unchanged. static Map pickupFromBooking(Map booking) { String s(dynamic v) => v == null ? '' : v.toString(); // First non-null value among several candidate keys — makes the adapter // tolerant of field-name variants (camelCase / snake_case / short forms) // so a naming mismatch can't map bookings to empty ids and drop them. dynamic pick(List keys) { for (final k in keys) { final v = booking[k]; if (v != null && v.toString().trim().isNotEmpty) return v; } return null; } // Some endpoints already return the legacy STOP shape the UI reads // (orderid/pickupid/orderstatus/pickupcustomer). Translating that as if it // were a new-booking object would map everything to empty ids (→ dropped) // and lose fields the translation doesn't cover (step, tenant, amounts). // Detect it and pass it through untouched. final looksNew = booking['bookingid'] != null || booking['bookingreference'] != null; final looksLegacy = booking['orderid'] != null || booking['pickupid'] != null || booking['orderstatus'] != null || booking['pickupcustomer'] != null; if (!looksNew && looksLegacy) { return Map.from(booking); } final id = pick(['bookingid', 'bookingId', 'id', 'booking_id', 'pickupid']); final ref = pick([ 'bookingreference', 'bookingRef', 'reference', 'referenceno', 'orderid', ]); final status = pick([ 'status', 'bookingstatus', 'booking_status', 'orderstatus', 'state', ]); return { // identity 'pickupid': id, 'orderid': ref ?? id, 'orderheaderid': id, 'bookingid': id, // keep original too 'bookingreference': s(ref), // status 'orderstatus': legacyStatusFromNew(s(status)), // pickup side 'pickupcustomer': s( pick([ 'customername', 'customerName', 'customer_name', 'name', 'pickupcustomer', ]), ), 'pickupcontactno': s( pick([ 'customerphone', 'customerPhone', 'customer_phone', 'phone', 'pickupcontactno', ]), ), 'pickupaddress': s( pick(['pickupaddress', 'pickupAddress', 'pickup_address']), ), 'pickuplat': s( pick([ 'pickuplatitude', 'pickupLatitude', 'pickup_latitude', 'pickuplat', ]), ), 'pickuplong': s( pick([ 'pickuplongitude', 'pickupLongitude', 'pickup_longitude', 'pickuplong', 'pickuplon', ]), ), 'pickuplon': s( pick([ 'pickuplongitude', 'pickupLongitude', 'pickup_longitude', 'pickuplong', 'pickuplon', ]), ), // delivery / drop side 'dropaddress': s( pick(['deliveryaddress', 'deliveryAddress', 'delivery_address']), ), 'droplat': s( pick(['deliverylatitude', 'deliveryLatitude', 'delivery_latitude']), ), 'droplon': s( pick(['deliverylongitude', 'deliveryLongitude', 'delivery_longitude']), ), // stop type — new bookings are first-mile PICKUPS; delivery legs come // through the consignment flow. Backend has no per-stop `type` yet. 'type': 'pickup', // money — NOT provided by the new booking object yet (see gaps doc) 'collectionamt': booking['collectionamt'] ?? 0, 'pickupamt': booking['pickupamt'] ?? 0, // misc passthrough 'parcels': booking['parcels'] ?? const [], 'starttime': s(booking['createdat']), 'eta': s(booking['eta']), }; } static List> pickupsFromBookings(dynamic data) { if (data is List) { return data.whereType().map((b) => pickupFromBooking(b)).toList(); } // A single booking object (some endpoints return one, not a list). if (data is Map && (data['bookingid'] != null || data['bookingreference'] != null)) { return >[pickupFromBooking(data)]; } return >[]; } static void logGap(String where, String detail) { debugPrint('[API_GAP][$where] $detail'); } }