first commit
This commit is contained in:
272
lib/data/api_config.dart
Normal file
272
lib/data/api_config.dart
Normal file
@@ -0,0 +1,272 @@
|
||||
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<void> setToken(String token) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kTokenKey, token);
|
||||
}
|
||||
|
||||
static Future<String?> getToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final t = prefs.getString(_kTokenKey);
|
||||
return (t != null && t.isNotEmpty) ? t : null;
|
||||
}
|
||||
|
||||
static Future<void> clearToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_kTokenKey);
|
||||
}
|
||||
|
||||
/// Headers for every authenticated request (Content-Type + Bearer token).
|
||||
static Future<Map<String, String>> authHeaders() async {
|
||||
final headers = <String, String>{
|
||||
'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<String, dynamic> toLegacyEnvelope(dynamic decoded) {
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final success = decoded['success'];
|
||||
final bool ok = success is bool ? success : success == true;
|
||||
return <String, dynamic>{
|
||||
'status': ok,
|
||||
'code': ok ? 200 : 400,
|
||||
'message': decoded['message']?.toString() ?? '',
|
||||
'details': decoded['data'] ?? decoded['details'] ?? decoded,
|
||||
};
|
||||
}
|
||||
// Bare list/value response.
|
||||
return <String, dynamic>{'status': true, 'code': 200, 'details': decoded};
|
||||
}
|
||||
|
||||
/// A generic "it worked" legacy envelope (for no-op / not-yet-supported flows).
|
||||
static Map<String, dynamic> okEnvelope([String message = '']) =>
|
||||
<String, dynamic>{'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<String, dynamic> 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<String> 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<String, dynamic>.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 <String, dynamic>{
|
||||
// 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<Map<String, dynamic>> pickupsFromBookings(dynamic data) {
|
||||
if (data is List) {
|
||||
return data.whereType<Map>().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 <Map<String, dynamic>>[pickupFromBooking(data)];
|
||||
}
|
||||
return <Map<String, dynamic>>[];
|
||||
}
|
||||
|
||||
static void logGap(String where, String detail) {
|
||||
debugPrint('[API_GAP][$where] $detail');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user