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

271 lines
7.9 KiB
Dart

// 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<FoodMerchant> merchants = [];
String city = _defaultCityForCountry();
// ── تفاصيل مطعم ──
bool isLoadingMenu = false;
FoodMerchant? selectedMerchant;
List<FoodMenuCategory> categories = [];
// ── السلة (مقيّدة بمطعم واحد فقط) ──
int? cartMerchantId;
final Map<String, FoodCartLine> _cart = {};
List<FoodCartLine> 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<void> 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<void> 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<void> 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<FoodMenuCategory>;
} else {
mySnackbarWarning(res.message);
}
update();
}
// ── إدارة السلة ──
bool addToCart(FoodMenuItem item, {int quantity = 1, Map<int, List<String>>? 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<int, List<String>>? 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<bool> 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<int?> 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<void> _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<bool> 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<bool> 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<int>.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();
}
}