Update: 2026-08-02 17:52:28
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
// 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<String, dynamic> 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<String, dynamic> 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<FoodOptionChoice> choices;
|
||||
|
||||
FoodItemOptionGroup({
|
||||
required this.id,
|
||||
required this.groupNameAr,
|
||||
required this.isRequired,
|
||||
required this.maxSelect,
|
||||
required this.choices,
|
||||
});
|
||||
|
||||
factory FoodItemOptionGroup.fromJson(Map<String, dynamic> 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<String, dynamic>.from(c)))
|
||||
.toList()
|
||||
: <FoodOptionChoice>[],
|
||||
);
|
||||
}
|
||||
|
||||
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<FoodItemOptionGroup> 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<String, dynamic> 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<String, dynamic>.from(o)))
|
||||
.toList()
|
||||
: <FoodItemOptionGroup>[],
|
||||
);
|
||||
}
|
||||
|
||||
class FoodMenuCategory {
|
||||
final int id;
|
||||
final String nameAr;
|
||||
final String? nameEn;
|
||||
final List<FoodMenuItem> items;
|
||||
|
||||
FoodMenuCategory({
|
||||
required this.id,
|
||||
required this.nameAr,
|
||||
this.nameEn,
|
||||
this.items = const [],
|
||||
});
|
||||
|
||||
factory FoodMenuCategory.fromJson(Map<String, dynamic> 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<String, dynamic>.from(i)))
|
||||
.toList()
|
||||
: <FoodMenuItem>[],
|
||||
);
|
||||
}
|
||||
|
||||
// ── سطر في السلة (محلي فقط قبل الإرسال للخادم) ──
|
||||
class FoodCartLine {
|
||||
final FoodMenuItem item;
|
||||
int quantity;
|
||||
// key: option_group_id, value: قائمة choice_id المختارة
|
||||
final Map<int, List<String>> selectedOptions;
|
||||
|
||||
FoodCartLine({
|
||||
required this.item,
|
||||
this.quantity = 1,
|
||||
Map<int, List<String>>? 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<FoodOrderItemLine> 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<String, dynamic> 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<String, dynamic>.from(i)))
|
||||
.toList()
|
||||
: <FoodOrderItemLine>[],
|
||||
);
|
||||
}
|
||||
|
||||
// ترتيب حالات الطلب لعرض خط زمني تصاعدي في شاشة التتبع
|
||||
const List<String> 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';
|
||||
}
|
||||
Reference in New Issue
Block a user