Food: single-source tracking, mode exclusivity, in-app navigation

This commit is contained in:
Hamza-Ayed
2026-08-04 02:41:54 +03:00
parent 788d3dcbe5
commit d695834d19
14 changed files with 607 additions and 83 deletions
@@ -16,6 +16,7 @@ import '../../main.dart';
import '../../print.dart';
import '../../views/food_delivery/food_offer_page.dart';
import '../../views/widgets/error_snakbar.dart';
import '../functions/location_controller.dart';
import '../voice_call_controller.dart';
import 'food_delivery_models.dart';
import 'food_delivery_service.dart';
@@ -235,6 +236,26 @@ class FoodDeliveryController extends GetxController {
await _fetchPendingOffers();
}
await fetchActiveTasks(silent: true);
await _broadcastLocation();
}
// موقع السائق يُبثّ فقط ما دامت هناك مهمة نشطة — لا شيء يُرسل قبل الإسناد
// ولا بعد التسليم، والخادم يرفضه في الحالتين أصلاً كطبقة ثانية.
Future<void> _broadcastLocation() async {
if (activeTasks.isEmpty) return;
if (!Get.isRegistered<LocationController>()) return;
final loc = Get.find<LocationController>();
if (loc.myLocation.latitude == 0 && loc.myLocation.longitude == 0) return;
for (final task in activeTasks) {
await FoodDeliveryService.sendLocation(
orderId: task.id,
lat: loc.myLocation.latitude,
lng: loc.myLocation.longitude,
heading: loc.heading,
);
}
}
Future<void> _fetchPendingOffers() async {
@@ -253,9 +274,42 @@ class FoodDeliveryController extends GetxController {
final res = await FoodDeliveryService.getActiveTasks();
isLoadingTasks = false;
if (res.success) activeTasks = res.data ?? [];
_syncRideAvailability();
update();
}
// ── مهمة واحدة في الوقت الواحد ──────────────────────────────────────────
// السائق أثناء التوصيل «مشغول» في نظام الرحلات تماماً كما لو كان في رحلة:
// نكتب statusDriverLocation='on' فينقله driver_socket من geo:drivers:available
// إلى geo:drivers:busy، فيتوقف توزيع الرحلات عليه — ويتوقف معه توزيع طلبات
// طعام إضافية، لأن مطابقة الطعام تتقاطع مع نفس المجموعة المتاحة.
// نعيده متاحاً بعد التسليم، وبشرطين: أن نكون نحن من شغَله، وألا تكون هناك
// رحلة جارية فعلاً (وإلا أفرغنا انشغالاً ليس لنا).
bool _markedBusyForDelivery = false;
void _syncRideAvailability() {
final hasDelivery = activeTasks.isNotEmpty;
if (hasDelivery && !_markedBusyForDelivery) {
_markedBusyForDelivery = true;
box.write(BoxName.statusDriverLocation, 'on');
Log.print('🍔 [FoodDelivery] السائق مشغول بتوصيل — أُوقف استقبال الرحلات');
return;
}
if (!hasDelivery && _markedBusyForDelivery) {
_markedBusyForDelivery = false;
final rideStatus = (box.read(BoxName.rideStatus) ?? '').toString();
final onRide = rideStatus == 'Begin' || rideStatus == 'Apply' || rideStatus == 'Arrived';
final blocked = box.read(BoxName.statusDriverLocation) == 'blocked';
if (!onRide && !blocked) {
box.write(BoxName.statusDriverLocation, 'off');
Log.print('🍔 [FoodDelivery] انتهى التوصيل — عاد استقبال الرحلات');
}
}
}
bool isRespondingToOffer(int orderId) => _respondingOfferIds.contains(orderId);
bool isTaskBusy(int orderId) => _busyTaskIds.contains(orderId);
@@ -98,6 +98,25 @@ class FoodDeliveryService {
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
/// بثّ موقع السائق للزبون أثناء المهمة. الخادم يرفضه بعد التسليم،
/// وينتهي آخر موقع من Redis تلقائياً — لا تتبّع خارج نافذة العمل.
static Future<void> sendLocation({
required int orderId,
required double lat,
required double lng,
double? heading,
}) async {
await CRUD().post(
link: '$_base/courier/location.php',
payload: {
'order_id': orderId.toString(),
'lat': lat.toString(),
'lng': lng.toString(),
if (heading != null) 'heading': heading.toStringAsFixed(0),
},
);
}
static Future<FoodDeliveryApiResult<void>> markPickedUp(int orderId) async {
final res = await CRUD().post(
link: '$_base/courier/picked_up.php',
@@ -0,0 +1,62 @@
// food_navigation.dart — تشغيل ملاحة سيرو الداخلية لمهام التوصيل
//
// لماذا النسخة نفسها (بلا tag) لا نسخة مستقلة للتوصيل:
// خطّ تحديث الموقع المركزي في LocationController يُغذّي نسخة واحدة فقط
// (Get.find<NavigationController> بلا وسم). نسخة موسومة للتوصيل ستُحرم من
// تحديثات الموقع فلا تعمل الملاحة أصلاً، وتغذيتها يستلزم تعديل مسار الموقع
// وهو المسار الأخطر في التطبيق. فنستعمل النسخة المشتركة، ونعالج التضارب
// الوحيد الممكن (سائق في ملاحة رحلة ثم يفتح مهمة توصيل) بسؤاله صراحةً
// بدل استبدال مساره صامتاً.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../views/widgets/error_snakbar.dart';
import '../home/navigation/navigation_controller.dart';
import '../home/navigation/navigation_view.dart';
class FoodNavigation {
/// يفتح ملاحة سيرو إلى [lat]/[lng]. يعيد false إذا ألغى السائق الاستبدال.
static Future<bool> startInApp({
required double lat,
required double lng,
required String title,
}) async {
final controller = Get.isRegistered<NavigationController>()
? Get.find<NavigationController>()
: Get.put(NavigationController());
if (controller.isNavigating) {
final replace = await Get.dialog<bool>(
AlertDialog(
title: const Text('ملاحة نشطة'),
content: Text('لديك ملاحة جارية. هل تستبدلها بالوجهة الجديدة؟\n$title'),
actions: [
TextButton(onPressed: () => Get.back(result: false), child: const Text('إبقاء الحالية')),
TextButton(onPressed: () => Get.back(result: true), child: const Text('استبدال')),
],
),
);
if (replace != true) return false;
}
Get.to(() => const NavigationView());
await controller.startNavigationTo(LatLng(lat, lng), infoWindowTitle: title);
return true;
}
/// خيار ثانوي لمن يفضّل جوجل — نفس ما يتيحه تدفّق الرحلات.
/// لا نمرّر في الرابط سوى الإحداثيات: لا اسم زبون ولا رقم طلب.
static Future<void> startExternal(double lat, double lng) async {
final nav = Uri.parse('google.navigation:q=$lat,$lng');
if (await canLaunchUrl(nav)) {
await launchUrl(nav);
return;
}
final web = Uri.parse('https://www.google.com/maps/dir/?api=1&destination=$lat,$lng');
if (!await launchUrl(web, mode: LaunchMode.externalApplication)) {
mySnackbarWarning('تعذّر فتح تطبيق الخرائط');
}
}
}