326 lines
10 KiB
Dart
326 lines
10 KiB
Dart
// 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';
|
|
}
|