// food_models.dart — نماذج بيانات وحدة الطعام (جهة الراكب) class FoodMerchant { final int id; final String nameAr; final String? nameEn; final String? logoUrl; final String? coverUrl; final String? descriptionAr; final String city; final String? address; final String? category; final int minOrderAmount; final int avgPrepMinutes; final double ratingAvg; final int ratingCount; final bool isOpen; FoodMerchant({ required this.id, required this.nameAr, required this.city, this.nameEn, this.logoUrl, this.coverUrl, this.descriptionAr, this.address, this.category, this.minOrderAmount = 0, this.avgPrepMinutes = 20, this.ratingAvg = 0, this.ratingCount = 0, this.isOpen = true, }); factory FoodMerchant.fromJson(Map j) => FoodMerchant( id: int.tryParse(j['id'].toString()) ?? 0, nameAr: j['name_ar']?.toString() ?? '', nameEn: j['name_en']?.toString(), logoUrl: j['logo_url']?.toString(), coverUrl: j['cover_url']?.toString(), descriptionAr: j['description_ar']?.toString(), city: j['city']?.toString() ?? '', address: j['address']?.toString(), category: j['category']?.toString(), minOrderAmount: int.tryParse(j['min_order_amount']?.toString() ?? '0') ?? 0, avgPrepMinutes: int.tryParse(j['avg_prep_minutes']?.toString() ?? '20') ?? 20, ratingAvg: double.tryParse(j['rating_avg']?.toString() ?? '0') ?? 0, ratingCount: int.tryParse(j['rating_count']?.toString() ?? '0') ?? 0, isOpen: j['is_open'] == true || j['is_open']?.toString() == '1', ); } class FoodOptionChoice { final String id; final String labelAr; final int price; FoodOptionChoice({required this.id, required this.labelAr, required this.price}); factory FoodOptionChoice.fromJson(Map j) => FoodOptionChoice( id: j['id'].toString(), labelAr: j['label_ar']?.toString() ?? '', price: int.tryParse(j['price']?.toString() ?? '0') ?? 0, ); } class FoodItemOptionGroup { final int id; final String groupNameAr; final bool isRequired; final int maxSelect; final List choices; FoodItemOptionGroup({ required this.id, required this.groupNameAr, required this.isRequired, required this.maxSelect, required this.choices, }); factory FoodItemOptionGroup.fromJson(Map j) => FoodItemOptionGroup( id: int.tryParse(j['id'].toString()) ?? 0, groupNameAr: j['group_name_ar']?.toString() ?? '', isRequired: j['is_required']?.toString() == '1' || j['is_required'] == true, maxSelect: int.tryParse(j['max_select']?.toString() ?? '1') ?? 1, choices: (j['choices'] is List) ? (j['choices'] as List) .map((c) => FoodOptionChoice.fromJson(Map.from(c))) .toList() : [], ); } class FoodMenuItem { final int id; final int categoryId; final String nameAr; final String? nameEn; final String? descriptionAr; final String? imageUrl; final int price; final bool isAvailable; final List options; FoodMenuItem({ required this.id, required this.categoryId, required this.nameAr, required this.price, this.nameEn, this.descriptionAr, this.imageUrl, this.isAvailable = true, this.options = const [], }); factory FoodMenuItem.fromJson(Map j) => FoodMenuItem( id: int.tryParse(j['id'].toString()) ?? 0, categoryId: int.tryParse(j['category_id'].toString()) ?? 0, nameAr: j['name_ar']?.toString() ?? '', nameEn: j['name_en']?.toString(), descriptionAr: j['description_ar']?.toString(), imageUrl: j['image_url']?.toString(), price: int.tryParse(j['price']?.toString() ?? '0') ?? 0, isAvailable: j['is_available']?.toString() != '0' && j['is_available'] != false, options: (j['options'] is List) ? (j['options'] as List) .map((o) => FoodItemOptionGroup.fromJson(Map.from(o))) .toList() : [], ); } class FoodMenuCategory { final int id; final String nameAr; final String? nameEn; final List items; FoodMenuCategory({ required this.id, required this.nameAr, this.nameEn, this.items = const [], }); factory FoodMenuCategory.fromJson(Map j) => FoodMenuCategory( id: int.tryParse(j['id'].toString()) ?? 0, nameAr: j['name_ar']?.toString() ?? '', nameEn: j['name_en']?.toString(), items: (j['items'] is List) ? (j['items'] as List) .map((i) => FoodMenuItem.fromJson(Map.from(i))) .toList() : [], ); } // ── سطر في السلة (محلي فقط قبل الإرسال للخادم) ── class FoodCartLine { final FoodMenuItem item; int quantity; // key: option_group_id, value: قائمة choice_id المختارة final Map> selectedOptions; FoodCartLine({ required this.item, this.quantity = 1, Map>? selectedOptions, }) : selectedOptions = selectedOptions ?? {}; int get unitPrice { int total = item.price; for (final group in item.options) { final chosen = selectedOptions[group.id] ?? []; for (final choiceId in chosen) { final choice = group.choices.firstWhere( (c) => c.id == choiceId, orElse: () => FoodOptionChoice(id: '', labelAr: '', price: 0), ); total += choice.price; } } return total; } int get lineTotal => unitPrice * quantity; Map toQuotePayload() => { 'item_id': item.id, 'quantity': quantity, 'options': selectedOptions.entries .map((e) => {'option_group_id': e.key, 'choice_ids': e.value}) .toList(), }; // مفتاح تمييز محلي: نفس الصنف بخيارات مختلفة = سطر مختلف في السلة String get lineKey { final optKeys = selectedOptions.entries.map((e) => '${e.key}:${e.value.join(",")}').join('|'); return '${item.id}_$optKeys'; } } class FoodQuote { final String quoteToken; final int itemsTotal; final int deliveryFee; final int serviceFee; final int grandTotal; final int expiresIn; FoodQuote({ required this.quoteToken, required this.itemsTotal, required this.deliveryFee, required this.serviceFee, required this.grandTotal, required this.expiresIn, }); factory FoodQuote.fromJson(Map j) => FoodQuote( quoteToken: j['quote_token']?.toString() ?? '', itemsTotal: int.tryParse(j['items_total']?.toString() ?? '0') ?? 0, deliveryFee: int.tryParse(j['delivery_fee']?.toString() ?? '0') ?? 0, serviceFee: int.tryParse(j['service_fee']?.toString() ?? '0') ?? 0, grandTotal: int.tryParse(j['grand_total']?.toString() ?? '0') ?? 0, expiresIn: int.tryParse(j['expires_in']?.toString() ?? '600') ?? 600, ); } class FoodOrderItemLine { final String nameArSnapshot; final int unitPrice; final int quantity; final int lineTotal; FoodOrderItemLine({ required this.nameArSnapshot, required this.unitPrice, required this.quantity, required this.lineTotal, }); factory FoodOrderItemLine.fromJson(Map j) => FoodOrderItemLine( nameArSnapshot: j['name_ar_snapshot']?.toString() ?? '', unitPrice: int.tryParse(j['unit_price']?.toString() ?? '0') ?? 0, quantity: int.tryParse(j['quantity']?.toString() ?? '1') ?? 1, lineTotal: int.tryParse(j['line_total']?.toString() ?? '0') ?? 0, ); } class FoodOrder { final int id; final String status; final int itemsTotal; final int deliveryFee; final int grandTotal; final String? deliveryAddress; final int? rating; final DateTime? createdAt; final DateTime? deliveredAt; final String? merchantNameAr; final String? merchantLogoUrl; final List items; FoodOrder({ required this.id, required this.status, required this.itemsTotal, required this.deliveryFee, required this.grandTotal, this.deliveryAddress, this.rating, this.createdAt, this.deliveredAt, this.merchantNameAr, this.merchantLogoUrl, this.items = const [], }); factory FoodOrder.fromJson(Map j) => FoodOrder( id: int.tryParse(j['id'].toString()) ?? 0, status: j['status']?.toString() ?? 'pending', itemsTotal: int.tryParse(j['items_total']?.toString() ?? '0') ?? 0, deliveryFee: int.tryParse(j['delivery_fee']?.toString() ?? '0') ?? 0, grandTotal: int.tryParse(j['grand_total']?.toString() ?? '0') ?? 0, deliveryAddress: j['delivery_address']?.toString(), rating: j['rating'] == null ? null : int.tryParse(j['rating'].toString()), createdAt: DateTime.tryParse(j['created_at']?.toString() ?? ''), deliveredAt: j['delivered_at'] == null ? null : DateTime.tryParse(j['delivered_at'].toString()), merchantNameAr: j['merchant_name_ar']?.toString(), merchantLogoUrl: j['merchant_logo_url']?.toString(), items: (j['items'] is List) ? (j['items'] as List) .map((i) => FoodOrderItemLine.fromJson(Map.from(i))) .toList() : [], ); } // ترتيب حالات الطلب لعرض خط زمني تصاعدي في شاشة التتبع const List foodOrderStatusFlow = [ 'pending', 'merchant_accepted', 'preparing', 'ready', 'courier_assigned', 'picked_up', 'delivered', ]; bool foodOrderIsCancelled(String status) => status.startsWith('cancelled') || status == 'rejected'; // المبالغ في backend/food مخزّنة بأصغر وحدة نقدية (fils) — العامل هنا يطابق // FOOD_CURRENCY_DIVISOR الافتراضي في docker/.env.example (1000). إن غُيّر // هناك يجب تغييره هنا أيضاً — القيمتان يجب أن تبقيا متطابقتين دائماً. const int foodCurrencyDivisor = 1000; String foodFormatPrice(int smallestUnit, {String currencySymbol = 'د.أ'}) { final decimal = smallestUnit / foodCurrencyDivisor; return '${decimal.toStringAsFixed(3)} $currencySymbol'; }