Update: 2026-08-02 17:52:28
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// food_delivery_controller.dart — حالة تبويب التوصيل (وضع التوصيل، العروض، المهام النشطة)
|
||||
import 'dart:async';
|
||||
import 'package:get/get.dart';
|
||||
import '../../views/widgets/error_snakbar.dart';
|
||||
import 'food_delivery_models.dart';
|
||||
import 'food_delivery_service.dart';
|
||||
|
||||
class FoodDeliveryController extends GetxController {
|
||||
bool isDeliveryModeEnabled = false;
|
||||
bool isTogglingMode = false;
|
||||
|
||||
List<FoodDeliveryOffer> pendingOffers = [];
|
||||
List<FoodDeliveryTask> activeTasks = [];
|
||||
bool isLoadingTasks = false;
|
||||
|
||||
final Set<int> _respondingOfferIds = {};
|
||||
final Set<int> _busyTaskIds = {};
|
||||
|
||||
Timer? _pollTimer;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_refreshAll();
|
||||
_pollTimer = Timer.periodic(const Duration(seconds: 4), (_) => _refreshAll());
|
||||
}
|
||||
|
||||
Future<void> _refreshAll() async {
|
||||
if (isDeliveryModeEnabled) {
|
||||
await _fetchPendingOffers();
|
||||
}
|
||||
await fetchActiveTasks();
|
||||
}
|
||||
|
||||
Future<void> toggleDeliveryMode(bool enable) async {
|
||||
isTogglingMode = true;
|
||||
update();
|
||||
|
||||
final res = await FoodDeliveryService.toggleAvailability(enable);
|
||||
isTogglingMode = false;
|
||||
|
||||
if (res.success) {
|
||||
isDeliveryModeEnabled = enable;
|
||||
if (!enable) pendingOffers = [];
|
||||
} else {
|
||||
mySnackbarWarning(res.message);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> _fetchPendingOffers() async {
|
||||
final res = await FoodDeliveryService.getPendingOffers();
|
||||
if (res.success) {
|
||||
pendingOffers = res.data ?? [];
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchActiveTasks() async {
|
||||
isLoadingTasks = true;
|
||||
final res = await FoodDeliveryService.getActiveTasks();
|
||||
isLoadingTasks = false;
|
||||
if (res.success) activeTasks = res.data ?? [];
|
||||
update();
|
||||
}
|
||||
|
||||
bool isRespondingToOffer(int orderId) => _respondingOfferIds.contains(orderId);
|
||||
bool isTaskBusy(int orderId) => _busyTaskIds.contains(orderId);
|
||||
|
||||
Future<void> respondToOffer(int orderId, bool accept) async {
|
||||
if (_respondingOfferIds.contains(orderId)) return;
|
||||
_respondingOfferIds.add(orderId);
|
||||
update();
|
||||
|
||||
final res = await FoodDeliveryService.respondToOffer(orderId, accept);
|
||||
|
||||
_respondingOfferIds.remove(orderId);
|
||||
pendingOffers.removeWhere((o) => o.orderId == orderId);
|
||||
|
||||
if (!res.success) {
|
||||
mySnackbarWarning(res.message);
|
||||
} else if (accept) {
|
||||
mySnackbarSuccess('تم قبول طلب التوصيل');
|
||||
}
|
||||
|
||||
update();
|
||||
await fetchActiveTasks();
|
||||
}
|
||||
|
||||
Future<void> markPickedUp(int orderId) async {
|
||||
if (_busyTaskIds.contains(orderId)) return;
|
||||
_busyTaskIds.add(orderId);
|
||||
update();
|
||||
|
||||
final res = await FoodDeliveryService.markPickedUp(orderId);
|
||||
_busyTaskIds.remove(orderId);
|
||||
|
||||
if (res.success) {
|
||||
mySnackbarSuccess('تم استلام الطلب من المطعم');
|
||||
await fetchActiveTasks();
|
||||
} else {
|
||||
mySnackbarWarning(res.message);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> markDelivered(int orderId) async {
|
||||
if (_busyTaskIds.contains(orderId)) return;
|
||||
_busyTaskIds.add(orderId);
|
||||
update();
|
||||
|
||||
final res = await FoodDeliveryService.markDelivered(orderId);
|
||||
_busyTaskIds.remove(orderId);
|
||||
|
||||
if (res.success) {
|
||||
mySnackbarSuccess('تم تسليم الطلب بنجاح');
|
||||
await fetchActiveTasks();
|
||||
} else {
|
||||
mySnackbarWarning(res.message);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_pollTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// food_delivery_models.dart — نماذج بيانات وحدة التوصيل (جهة السائق)
|
||||
|
||||
class FoodDeliveryTask {
|
||||
final int id;
|
||||
final String status; // courier_assigned | picked_up
|
||||
final int merchantId;
|
||||
final String merchantNameAr;
|
||||
final double? merchantLat;
|
||||
final double? merchantLng;
|
||||
final String? merchantAddress;
|
||||
final int deliveryFee;
|
||||
final String? deliveryAddress;
|
||||
final double deliveryLat;
|
||||
final double deliveryLng;
|
||||
final DateTime? createdAt;
|
||||
|
||||
FoodDeliveryTask({
|
||||
required this.id,
|
||||
required this.status,
|
||||
required this.merchantId,
|
||||
required this.merchantNameAr,
|
||||
required this.deliveryFee,
|
||||
required this.deliveryLat,
|
||||
required this.deliveryLng,
|
||||
this.merchantLat,
|
||||
this.merchantLng,
|
||||
this.merchantAddress,
|
||||
this.deliveryAddress,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
factory FoodDeliveryTask.fromJson(Map<String, dynamic> j) => FoodDeliveryTask(
|
||||
id: int.tryParse(j['id'].toString()) ?? 0,
|
||||
status: j['status']?.toString() ?? '',
|
||||
merchantId: int.tryParse(j['merchant_id'].toString()) ?? 0,
|
||||
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
|
||||
merchantLat: double.tryParse(j['merchant_lat']?.toString() ?? ''),
|
||||
merchantLng: double.tryParse(j['merchant_lng']?.toString() ?? ''),
|
||||
merchantAddress: j['merchant_address']?.toString(),
|
||||
deliveryFee: int.tryParse(j['delivery_fee']?.toString() ?? '0') ?? 0,
|
||||
deliveryAddress: j['delivery_address']?.toString(),
|
||||
deliveryLat: double.tryParse(j['delivery_lat']?.toString() ?? '0') ?? 0,
|
||||
deliveryLng: double.tryParse(j['delivery_lng']?.toString() ?? '0') ?? 0,
|
||||
createdAt: DateTime.tryParse(j['created_at']?.toString() ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
// عرض توصيل وارد — من polling على food/courier/pending_offers.php
|
||||
class FoodDeliveryOffer {
|
||||
final int orderId;
|
||||
final int merchantId;
|
||||
final String merchantNameAr;
|
||||
final double? merchantLat;
|
||||
final double? merchantLng;
|
||||
final int deliveryFee;
|
||||
final DateTime? offeredAt;
|
||||
|
||||
FoodDeliveryOffer({
|
||||
required this.orderId,
|
||||
required this.merchantId,
|
||||
required this.merchantNameAr,
|
||||
required this.deliveryFee,
|
||||
this.merchantLat,
|
||||
this.merchantLng,
|
||||
this.offeredAt,
|
||||
});
|
||||
|
||||
factory FoodDeliveryOffer.fromJson(Map<String, dynamic> j) => FoodDeliveryOffer(
|
||||
orderId: int.tryParse(j['order_id'].toString()) ?? 0,
|
||||
merchantId: int.tryParse(j['merchant_id'].toString()) ?? 0,
|
||||
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
|
||||
merchantLat: double.tryParse(j['merchant_lat']?.toString() ?? ''),
|
||||
merchantLng: double.tryParse(j['merchant_lng']?.toString() ?? ''),
|
||||
deliveryFee: int.tryParse(j['delivery_fee']?.toString() ?? '0') ?? 0,
|
||||
offeredAt: DateTime.tryParse(j['offered_at']?.toString() ?? ''),
|
||||
);
|
||||
|
||||
// مهلة العرض 20 ثانية من الخادم (SET NX EX 20) — عدّاد تقريبي للعرض فقط،
|
||||
// الخادم هو الحكم الفعلي (رفض offer_respond إن انتهت المهلة فعلاً).
|
||||
int get secondsRemaining {
|
||||
if (offeredAt == null) return 20;
|
||||
final elapsed = DateTime.now().difference(offeredAt!).inSeconds;
|
||||
return (20 - elapsed).clamp(0, 20);
|
||||
}
|
||||
}
|
||||
|
||||
const int foodCurrencyDivisor = 1000;
|
||||
|
||||
String foodFormatPrice(int smallestUnit, {String currencySymbol = 'د.أ'}) {
|
||||
final decimal = smallestUnit / foodCurrencyDivisor;
|
||||
return '${decimal.toStringAsFixed(3)} $currencySymbol';
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// food_delivery_service.dart — طبقة الاتصال بـ backend/food (جهة السائق/الموصّل)
|
||||
import '../functions/crud.dart';
|
||||
import '../../constant/links.dart';
|
||||
import 'food_delivery_models.dart';
|
||||
|
||||
class FoodDeliveryApiResult<T> {
|
||||
final bool success;
|
||||
final T? data;
|
||||
final String message;
|
||||
FoodDeliveryApiResult(this.success, this.data, this.message);
|
||||
}
|
||||
|
||||
class FoodDeliveryService {
|
||||
static String get _base => '${AppLink.server}/food';
|
||||
|
||||
static Future<FoodDeliveryApiResult<bool>> toggleAvailability(bool enabled) async {
|
||||
final res = await CRUD().post(
|
||||
link: '$_base/courier/toggle_availability.php',
|
||||
payload: {'enabled': enabled.toString()},
|
||||
);
|
||||
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, enabled, 'ok');
|
||||
return FoodDeliveryApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
|
||||
static Future<FoodDeliveryApiResult<List<FoodDeliveryTask>>> getActiveTasks() async {
|
||||
final res = await CRUD().post(link: '$_base/courier/active.php');
|
||||
if (res is Map && res['status'] == 'success') {
|
||||
final msg = res['message'];
|
||||
final list = (msg is Map && msg['orders'] is List)
|
||||
? (msg['orders'] as List)
|
||||
.map((o) => FoodDeliveryTask.fromJson(Map<String, dynamic>.from(o)))
|
||||
.toList()
|
||||
: <FoodDeliveryTask>[];
|
||||
return FoodDeliveryApiResult(true, list, 'ok');
|
||||
}
|
||||
return FoodDeliveryApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
|
||||
static Future<FoodDeliveryApiResult<List<FoodDeliveryOffer>>> getPendingOffers() async {
|
||||
final res = await CRUD().post(link: '$_base/courier/pending_offers.php');
|
||||
if (res is Map && res['status'] == 'success') {
|
||||
final msg = res['message'];
|
||||
final list = (msg is Map && msg['offers'] is List)
|
||||
? (msg['offers'] as List)
|
||||
.map((o) => FoodDeliveryOffer.fromJson(Map<String, dynamic>.from(o)))
|
||||
.toList()
|
||||
: <FoodDeliveryOffer>[];
|
||||
return FoodDeliveryApiResult(true, list, 'ok');
|
||||
}
|
||||
return FoodDeliveryApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
|
||||
static Future<FoodDeliveryApiResult<String>> respondToOffer(int orderId, bool accept) async {
|
||||
final res = await CRUD().post(
|
||||
link: '$_base/courier/offer_respond.php',
|
||||
payload: {'order_id': orderId.toString(), 'response': accept ? 'accept' : 'reject'},
|
||||
);
|
||||
if (res is Map && res['status'] == 'success') {
|
||||
final msg = res['message'];
|
||||
return FoodDeliveryApiResult(true, (msg is Map ? msg['response']?.toString() : null) ?? '', 'ok');
|
||||
}
|
||||
return FoodDeliveryApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
|
||||
static Future<FoodDeliveryApiResult<void>> markPickedUp(int orderId) async {
|
||||
final res = await CRUD().post(
|
||||
link: '$_base/courier/picked_up.php',
|
||||
payload: {'order_id': orderId.toString()},
|
||||
);
|
||||
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, null, 'ok');
|
||||
return FoodDeliveryApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
|
||||
static Future<FoodDeliveryApiResult<void>> markDelivered(int orderId) async {
|
||||
final res = await CRUD().post(
|
||||
link: '$_base/courier/delivered.php',
|
||||
payload: {'order_id': orderId.toString()},
|
||||
);
|
||||
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, null, 'ok');
|
||||
return FoodDeliveryApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
|
||||
static String _errMsg(dynamic res) {
|
||||
if (res == 'no_internet') return 'تحقق من اتصالك بالإنترنت';
|
||||
if (res == 'token_expired') return 'انتهت الجلسة، حاول مجدداً';
|
||||
if (res is Map && res['message'] is String) return res['message'];
|
||||
return 'حدث خطأ، حاول مجدداً';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user