// food_controller.dart — حالة تبويب الطعام (تصفح، سلة، تتبّع الطلب) import 'dart:async'; import 'dart:math'; import 'package:get/get.dart'; import '../../constant/box_name.dart'; import '../../main.dart'; import '../../views/widgets/error_snakbar.dart'; import 'food_models.dart'; import 'food_service.dart'; class FoodController extends GetxController { // ── تصفح ── bool isLoadingMerchants = false; List merchants = []; String city = _defaultCityForCountry(); // ── تفاصيل مطعم ── bool isLoadingMenu = false; FoodMerchant? selectedMerchant; List categories = []; // ── السلة (مقيّدة بمطعم واحد فقط) ── int? cartMerchantId; final Map _cart = {}; List get cartLines => _cart.values.toList(); int get cartItemsCount => _cart.values.fold(0, (sum, l) => sum + l.quantity); int get cartTotal => _cart.values.fold(0, (sum, l) => sum + l.lineTotal); // ── عرض السعر الحالي ── FoodQuote? currentQuote; bool isQuoting = false; // ── الطلب النشط (تتبّع) ── FoodOrder? activeOrder; Timer? _statusPollTimer; static String _defaultCityForCountry() { final country = box.read(BoxName.countryCode); switch (country) { case 'SY': return 'دمشق'; case 'EG': return 'القاهرة'; default: return 'عمان'; } } void setCity(String newCity) { if (newCity.trim().isEmpty) return; city = newCity.trim(); fetchMerchants(); } Future fetchMerchants({String? category}) async { isLoadingMerchants = true; update(); final res = await FoodService.browseMerchants(city: city, category: category); isLoadingMerchants = false; if (res.success) { merchants = res.data ?? []; } else { merchants = []; mySnackbarWarning(res.message); } update(); } Future searchMerchants(String query) async { if (query.trim().length < 2) return; isLoadingMerchants = true; update(); final res = await FoodService.searchMerchants(city: city, query: query.trim()); isLoadingMerchants = false; if (res.success) merchants = res.data ?? []; update(); } Future openMerchant(int merchantId) async { isLoadingMenu = true; selectedMerchant = null; categories = []; update(); final res = await FoodService.merchantDetails(merchantId); isLoadingMenu = false; if (res.success && res.data != null) { selectedMerchant = res.data!['merchant'] as FoodMerchant; categories = res.data!['categories'] as List; } else { mySnackbarWarning(res.message); } update(); } // ── إدارة السلة ── bool addToCart(FoodMenuItem item, {int quantity = 1, Map>? options}) { final line = FoodCartLine(item: item, quantity: quantity, selectedOptions: options ?? {}); final existing = _cart[line.lineKey]; if (existing != null) { existing.quantity += quantity; } else { _cart[line.lineKey] = line; } currentQuote = null; // أي تعديل بالسلة يُبطل العرض الموقّع القديم update(); return true; } // يُستدعى من صفحة المطعم مع فحص أن السلة إما فارغة أو لنفس المطعم bool addToCartForMerchant(int merchantId, FoodMenuItem item, {int quantity = 1, Map>? options}) { if (_cart.isNotEmpty && cartMerchantId != null && cartMerchantId != merchantId) { mySnackbarWarning(box.read(BoxName.lang) == 'ar' ? 'سلتك تحتوي أصنافاً من مطعم آخر. أفرغها أولاً لإضافة من هذا المطعم.' : 'Your cart has items from another restaurant. Clear it first.'); return false; } cartMerchantId = merchantId; return addToCart(item, quantity: quantity, options: options); } void removeFromCart(String lineKey) { _cart.remove(lineKey); if (_cart.isEmpty) cartMerchantId = null; currentQuote = null; update(); } void updateQuantity(String lineKey, int quantity) { final line = _cart[lineKey]; if (line == null) return; if (quantity <= 0) { removeFromCart(lineKey); return; } line.quantity = quantity; currentQuote = null; update(); } void clearCart() { _cart.clear(); cartMerchantId = null; currentQuote = null; update(); } Future refreshQuote() async { if (cartMerchantId == null || cartLines.isEmpty) return false; isQuoting = true; update(); final res = await FoodService.getQuote(merchantId: cartMerchantId!, lines: cartLines); isQuoting = false; if (res.success && res.data != null) { currentQuote = res.data; update(); return true; } currentQuote = null; mySnackbarWarning(res.message); update(); return false; } bool _placingOrder = false; bool get isPlacingOrder => _placingOrder; Future placeOrder({ required String deliveryAddress, required double deliveryLat, required double deliveryLng, required String paymentMethod, String? customerNote, }) async { if (currentQuote == null) { final ok = await refreshQuote(); if (!ok || currentQuote == null) return null; } _placingOrder = true; update(); final res = await FoodService.createOrder( quoteToken: currentQuote!.quoteToken, clientOrderUuid: _generateUuidV4(), deliveryAddress: deliveryAddress, deliveryLat: deliveryLat, deliveryLng: deliveryLng, paymentMethod: paymentMethod, customerNote: customerNote, ); _placingOrder = false; update(); if (res.success && res.data != null) { clearCart(); startTrackingOrder(res.data!); return res.data; } mySnackbarWarning(res.message); return null; } // ── تتبّع الطلب (polling — السوكيت ليس مصدر الحقيقة، انظر ملاحظة backend) ── void startTrackingOrder(int orderId) { _statusPollTimer?.cancel(); _pollOrderStatus(orderId); _statusPollTimer = Timer.periodic(const Duration(seconds: 5), (_) => _pollOrderStatus(orderId)); } Future _pollOrderStatus(int orderId) async { final res = await FoodService.getOrderStatus(orderId); if (res.success && res.data != null) { activeOrder = res.data; update(); if (activeOrder!.status == 'delivered' || foodOrderIsCancelled(activeOrder!.status)) { _statusPollTimer?.cancel(); } } } void stopTracking() { _statusPollTimer?.cancel(); activeOrder = null; } Future rateActiveOrder(int rating, {String? comment}) async { if (activeOrder == null) return false; final res = await FoodService.rateOrder(activeOrder!.id, rating, comment: comment); if (res.success) { await _pollOrderStatus(activeOrder!.id); return true; } mySnackbarWarning(res.message); return false; } Future cancelActiveOrder({String? reason}) async { if (activeOrder == null) return false; final res = await FoodService.cancelOrder(activeOrder!.id, reason: reason); if (res.success) { await _pollOrderStatus(activeOrder!.id); return true; } mySnackbarWarning(res.message); return false; } static String _generateUuidV4() { final rnd = Random.secure(); final bytes = List.generate(16, (_) => rnd.nextInt(256)); bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4 bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant String hex(int start, int end) => bytes.sublist(start, end).map((b) => b.toRadixString(16).padLeft(2, '0')).join(); return '${hex(0, 4)}-${hex(4, 6)}-${hex(6, 8)}-${hex(8, 10)}-${hex(10, 16)}'; } @override void onClose() { _statusPollTimer?.cancel(); super.onClose(); } }