324 lines
11 KiB
Dart
324 lines
11 KiB
Dart
// food_delivery_models.dart — نماذج بيانات وحدة التوصيل (جهة السائق)
|
|
|
|
int _int(dynamic v) => int.tryParse(v?.toString() ?? '') ?? 0;
|
|
double? _doubleOrNull(dynamic v) => double.tryParse(v?.toString() ?? '');
|
|
double _double(dynamic v) => double.tryParse(v?.toString() ?? '') ?? 0;
|
|
DateTime? _date(dynamic v) => DateTime.tryParse(v?.toString() ?? '');
|
|
|
|
class FoodDeliveryTask {
|
|
final int id;
|
|
final String status; // courier_assigned | picked_up
|
|
final int merchantId;
|
|
final String merchantNameAr;
|
|
final double? merchantLat;
|
|
final double? merchantLng;
|
|
final String? merchantAddress;
|
|
final int deliveryFee;
|
|
final int itemsTotal;
|
|
final int serviceFee;
|
|
final int discount;
|
|
final int grandTotal;
|
|
final String paymentMethod; // wallet | cash
|
|
final int cashToCollect;
|
|
final int courierOwes;
|
|
final int itemsCount;
|
|
final String? customerNote;
|
|
final String? deliveryAddress;
|
|
final double deliveryLat;
|
|
final double deliveryLng;
|
|
final DateTime? createdAt;
|
|
final DateTime? readyAt;
|
|
final DateTime? courierAssignedAt;
|
|
final DateTime? pickedUpAt;
|
|
|
|
FoodDeliveryTask({
|
|
required this.id,
|
|
required this.status,
|
|
required this.merchantId,
|
|
required this.merchantNameAr,
|
|
required this.deliveryFee,
|
|
required this.deliveryLat,
|
|
required this.deliveryLng,
|
|
this.itemsTotal = 0,
|
|
this.serviceFee = 0,
|
|
this.discount = 0,
|
|
this.grandTotal = 0,
|
|
this.paymentMethod = 'wallet',
|
|
this.cashToCollect = 0,
|
|
this.courierOwes = 0,
|
|
this.itemsCount = 0,
|
|
this.customerNote,
|
|
this.merchantLat,
|
|
this.merchantLng,
|
|
this.merchantAddress,
|
|
this.deliveryAddress,
|
|
this.createdAt,
|
|
this.readyAt,
|
|
this.courierAssignedAt,
|
|
this.pickedUpAt,
|
|
});
|
|
|
|
bool get isPickedUp => status == 'picked_up';
|
|
bool get isCash => paymentMethod == 'cash';
|
|
|
|
factory FoodDeliveryTask.fromJson(Map<String, dynamic> j) => FoodDeliveryTask(
|
|
id: _int(j['id']),
|
|
status: j['status']?.toString() ?? '',
|
|
merchantId: _int(j['merchant_id']),
|
|
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
|
|
merchantLat: _doubleOrNull(j['merchant_lat']),
|
|
merchantLng: _doubleOrNull(j['merchant_lng']),
|
|
merchantAddress: j['merchant_address']?.toString(),
|
|
deliveryFee: _int(j['delivery_fee']),
|
|
itemsTotal: _int(j['items_total']),
|
|
serviceFee: _int(j['service_fee']),
|
|
discount: _int(j['discount']),
|
|
grandTotal: _int(j['grand_total']),
|
|
paymentMethod: j['payment_method']?.toString() ?? 'wallet',
|
|
cashToCollect: _int(j['cash_to_collect']),
|
|
courierOwes: _int(j['courier_owes']),
|
|
itemsCount: _int(j['items_count']),
|
|
customerNote: j['customer_note']?.toString(),
|
|
deliveryAddress: j['delivery_address']?.toString(),
|
|
deliveryLat: _double(j['delivery_lat']),
|
|
deliveryLng: _double(j['delivery_lng']),
|
|
createdAt: _date(j['created_at']),
|
|
readyAt: _date(j['ready_at']),
|
|
courierAssignedAt: _date(j['courier_assigned_at']),
|
|
pickedUpAt: _date(j['picked_up_at']),
|
|
);
|
|
}
|
|
|
|
// عرض توصيل وارد — يصل عبر سوكيت الطعام ('food_delivery_offer') وبنفس الحقول
|
|
// من courier/pending_offers.php عند انقطاع السوكيت (foodBuildCourierOfferPayload).
|
|
class FoodDeliveryOffer {
|
|
final int orderId;
|
|
final int merchantId;
|
|
final String merchantNameAr;
|
|
final String? merchantAddress;
|
|
final double? merchantLat;
|
|
final double? merchantLng;
|
|
final double deliveryLat;
|
|
final double deliveryLng;
|
|
final int deliveryFee;
|
|
final int itemsCount;
|
|
final String paymentMethod;
|
|
final int cashToCollect;
|
|
final int ttlSeconds;
|
|
final DateTime receivedAt;
|
|
final DateTime? offeredAt;
|
|
|
|
FoodDeliveryOffer({
|
|
required this.orderId,
|
|
required this.merchantId,
|
|
required this.merchantNameAr,
|
|
required this.deliveryFee,
|
|
this.merchantAddress,
|
|
this.merchantLat,
|
|
this.merchantLng,
|
|
this.deliveryLat = 0,
|
|
this.deliveryLng = 0,
|
|
this.itemsCount = 0,
|
|
this.paymentMethod = 'wallet',
|
|
this.cashToCollect = 0,
|
|
this.ttlSeconds = 20,
|
|
DateTime? receivedAt,
|
|
this.offeredAt,
|
|
}) : receivedAt = receivedAt ?? DateTime.now();
|
|
|
|
bool get isCash => paymentMethod == 'cash';
|
|
|
|
factory FoodDeliveryOffer.fromJson(Map<String, dynamic> j) => FoodDeliveryOffer(
|
|
orderId: _int(j['order_id']),
|
|
merchantId: _int(j['merchant_id']),
|
|
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
|
|
merchantAddress: j['merchant_address']?.toString(),
|
|
merchantLat: _doubleOrNull(j['merchant_lat']),
|
|
merchantLng: _doubleOrNull(j['merchant_lng']),
|
|
deliveryLat: _double(j['delivery_lat']),
|
|
deliveryLng: _double(j['delivery_lng']),
|
|
deliveryFee: _int(j['delivery_fee']),
|
|
itemsCount: _int(j['items_count']),
|
|
paymentMethod: j['payment_method']?.toString() ?? 'wallet',
|
|
cashToCollect: _int(j['cash_to_collect']),
|
|
ttlSeconds: _int(j['offer_ttl_seconds']) == 0 ? 20 : _int(j['offer_ttl_seconds']),
|
|
offeredAt: _date(j['offered_at']),
|
|
);
|
|
|
|
// العدّاد للعرض فقط — الخادم هو الحكم الفعلي ويرفض offer_respond بعد المهلة.
|
|
// نبدأ من offered_at إن جاءت (مسار polling)، وإلا من لحظة الاستلام (مسار السوكيت).
|
|
int get secondsRemaining {
|
|
final start = offeredAt ?? receivedAt;
|
|
final elapsed = DateTime.now().difference(start).inSeconds;
|
|
return (ttlSeconds - elapsed).clamp(0, ttlSeconds);
|
|
}
|
|
|
|
bool get isExpired => secondsRemaining <= 0;
|
|
}
|
|
|
|
// صنف داخل الطلب — يراه السائق ليتأكد من محتوى الكيس عند الاستلام من المطعم
|
|
class FoodOrderItem {
|
|
final String nameAr;
|
|
final int quantity;
|
|
final int unitPrice;
|
|
final int lineTotal;
|
|
|
|
FoodOrderItem({
|
|
required this.nameAr,
|
|
required this.quantity,
|
|
required this.unitPrice,
|
|
required this.lineTotal,
|
|
});
|
|
|
|
factory FoodOrderItem.fromJson(Map<String, dynamic> j) => FoodOrderItem(
|
|
nameAr: j['name_ar']?.toString() ?? '',
|
|
quantity: _int(j['quantity']),
|
|
unitPrice: _int(j['unit_price']),
|
|
lineTotal: _int(j['line_total']),
|
|
);
|
|
}
|
|
|
|
class FoodOrderDetails {
|
|
final FoodDeliveryTask task;
|
|
final List<FoodOrderItem> items;
|
|
final String? merchantPhone;
|
|
|
|
FoodOrderDetails({required this.task, required this.items, this.merchantPhone});
|
|
|
|
factory FoodOrderDetails.fromJson(Map<String, dynamic> j) => FoodOrderDetails(
|
|
task: FoodDeliveryTask.fromJson(j),
|
|
items: (j['items'] is List)
|
|
? (j['items'] as List)
|
|
.map((i) => FoodOrderItem.fromJson(Map<String, dynamic>.from(i)))
|
|
.toList()
|
|
: <FoodOrderItem>[],
|
|
merchantPhone: j['merchant_phone']?.toString(),
|
|
);
|
|
}
|
|
|
|
class FoodCourierStatus {
|
|
final bool deliveryModeEnabled;
|
|
final int activeOrdersCount;
|
|
final int todayOrdersCount;
|
|
final int todayEarnings;
|
|
|
|
FoodCourierStatus({
|
|
required this.deliveryModeEnabled,
|
|
this.activeOrdersCount = 0,
|
|
this.todayOrdersCount = 0,
|
|
this.todayEarnings = 0,
|
|
});
|
|
|
|
factory FoodCourierStatus.fromJson(Map<String, dynamic> j) => FoodCourierStatus(
|
|
deliveryModeEnabled: j['delivery_mode_enabled'] == true ||
|
|
j['delivery_mode_enabled']?.toString() == '1' ||
|
|
j['delivery_mode_enabled']?.toString() == 'true',
|
|
activeOrdersCount: _int(j['active_orders_count']),
|
|
todayOrdersCount: _int(j['today_orders_count']),
|
|
todayEarnings: _int(j['today_earnings']),
|
|
);
|
|
}
|
|
|
|
class FoodEarningsDay {
|
|
final String day;
|
|
final int ordersCount;
|
|
final int earnings;
|
|
|
|
FoodEarningsDay({required this.day, required this.ordersCount, required this.earnings});
|
|
|
|
factory FoodEarningsDay.fromJson(Map<String, dynamic> j) => FoodEarningsDay(
|
|
day: j['day']?.toString() ?? '',
|
|
ordersCount: _int(j['orders_count']),
|
|
earnings: _int(j['earnings']),
|
|
);
|
|
}
|
|
|
|
class FoodEarnings {
|
|
final int todayEarnings;
|
|
final int todayOrders;
|
|
final int weekEarnings;
|
|
final int weekOrders;
|
|
final int monthEarnings;
|
|
final int monthOrders;
|
|
final int totalEarnings;
|
|
final int totalOrders;
|
|
final int pendingCashOwed;
|
|
final List<FoodEarningsDay> daily;
|
|
|
|
FoodEarnings({
|
|
required this.todayEarnings,
|
|
required this.todayOrders,
|
|
required this.weekEarnings,
|
|
required this.weekOrders,
|
|
required this.monthEarnings,
|
|
required this.monthOrders,
|
|
required this.totalEarnings,
|
|
required this.totalOrders,
|
|
required this.pendingCashOwed,
|
|
required this.daily,
|
|
});
|
|
|
|
factory FoodEarnings.fromJson(Map<String, dynamic> j) => FoodEarnings(
|
|
todayEarnings: _int(j['today_earnings']),
|
|
todayOrders: _int(j['today_orders']),
|
|
weekEarnings: _int(j['week_earnings']),
|
|
weekOrders: _int(j['week_orders']),
|
|
monthEarnings: _int(j['month_earnings']),
|
|
monthOrders: _int(j['month_orders']),
|
|
totalEarnings: _int(j['total_earnings']),
|
|
totalOrders: _int(j['total_orders']),
|
|
pendingCashOwed: _int(j['pending_cash_owed']),
|
|
daily: (j['daily'] is List)
|
|
? (j['daily'] as List)
|
|
.map((d) => FoodEarningsDay.fromJson(Map<String, dynamic>.from(d)))
|
|
.toList()
|
|
: <FoodEarningsDay>[],
|
|
);
|
|
}
|
|
|
|
class FoodHistoryEntry {
|
|
final int id;
|
|
final String merchantNameAr;
|
|
final String? merchantAddress;
|
|
final int deliveryFee;
|
|
final int grandTotal;
|
|
final String paymentMethod;
|
|
final int itemsCount;
|
|
final int? rating;
|
|
final int? durationMinutes;
|
|
final DateTime? deliveredAt;
|
|
|
|
FoodHistoryEntry({
|
|
required this.id,
|
|
required this.merchantNameAr,
|
|
required this.deliveryFee,
|
|
this.merchantAddress,
|
|
this.grandTotal = 0,
|
|
this.paymentMethod = 'wallet',
|
|
this.itemsCount = 0,
|
|
this.rating,
|
|
this.durationMinutes,
|
|
this.deliveredAt,
|
|
});
|
|
|
|
factory FoodHistoryEntry.fromJson(Map<String, dynamic> j) => FoodHistoryEntry(
|
|
id: _int(j['id']),
|
|
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
|
|
merchantAddress: j['merchant_address']?.toString(),
|
|
deliveryFee: _int(j['delivery_fee']),
|
|
grandTotal: _int(j['grand_total']),
|
|
paymentMethod: j['payment_method']?.toString() ?? 'wallet',
|
|
itemsCount: _int(j['items_count']),
|
|
rating: j['rating'] == null ? null : _int(j['rating']),
|
|
durationMinutes: j['duration_minutes'] == null ? null : _int(j['duration_minutes']),
|
|
deliveredAt: _date(j['delivered_at']),
|
|
);
|
|
}
|
|
|
|
const int foodCurrencyDivisor = 1000;
|
|
|
|
String foodFormatPrice(int smallestUnit, {String currencySymbol = 'د.أ'}) {
|
|
final decimal = smallestUnit / foodCurrencyDivisor;
|
|
return '${decimal.toStringAsFixed(3)} $currencySymbol';
|
|
}
|