Files
Siro/siro_rider/lib/controller/food/food_models.dart
T

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