Update codebase
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user