Files
Siro/siro_driver/lib/controller/food_delivery/food_delivery_models.dart
T

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';
}