Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-09 16:56:13 +03:00
parent 95e2e4f35d
commit b64debaa88
1058 changed files with 164327 additions and 113928 deletions
+38
View File
@@ -0,0 +1,38 @@
<?php
// food/courier/active.php — طلبات التوصيل الحالية للسائق (polling fallback + شاشة المهمة)
require_once __DIR__ . '/../connect_courier.php';
$st = $food_con->prepare(
"SELECT o.id, o.status, o.merchant_id, m.name_ar AS merchant_name_ar, m.latitude AS merchant_lat,
m.longitude AS merchant_lng, m.address AS merchant_address, m.avg_prep_minutes,
o.delivery_fee, o.items_total, o.service_fee, o.discount, o.grand_total, o.payment_method,
o.customer_note, o.delivery_address, o.delivery_lat, o.delivery_lng,
o.created_at, o.ready_at, o.courier_assigned_at, o.picked_up_at,
(SELECT COALESCE(SUM(quantity),0) FROM food_order_items WHERE order_id=o.id) AS items_count,
CASE WHEN o.status IN ('courier_assigned','picked_up') THEN o.delivery_address ELSE NULL END AS visible_address
FROM food_orders o
JOIN food_merchants m ON m.id = o.merchant_id
WHERE o.courier_id = ? AND o.status IN ('courier_assigned','picked_up')
ORDER BY o.courier_assigned_at ASC"
);
$st->execute([$food_courier_id]);
$orders = $st->fetchAll();
// بيانات الزبون (العنوان) تظهر فقط بعد courier_assigned وتُحجب بعد delivered — لا نُعيد أي طلب مسلَّم هنا أصلاً
foreach ($orders as &$o) {
unset($o['delivery_address']);
$o['delivery_address'] = $o['visible_address'];
unset($o['visible_address']);
$o['items_count'] = (int)$o['items_count'];
// نقداً: السائق يحصّل grand_total من الزبون ويحتفظ بأجرته (delivery_fee)،
// والباقي يبقى ديناً عليه حتى التسوية — نُظهر الرقمين صراحةً في التطبيق
// كي لا يجتهد السائق في الحساب على باب الزبون.
$o['cash_to_collect'] = $o['payment_method'] === 'cash' ? (int)$o['grand_total'] : 0;
$o['courier_owes'] = $o['payment_method'] === 'cash'
? (int)$o['grand_total'] - (int)$o['delivery_fee']
: 0;
}
unset($o);
jsonSuccess(['orders' => $orders]);
+48
View File
@@ -0,0 +1,48 @@
<?php
// food/courier/call_customer.php — السائق يتصل بالزبون عبر قناة مقنّعة
// لا رقم هاتف يُعرض لأي طرف: نفتح جلسة WebRTC ونُشعر الزبون بمعرّف الجلسة فقط.
require_once __DIR__ . '/../connect_courier.php';
$orderId = filterRequest('order_id', 'int');
if (!$orderId) jsonError('order_id is required');
$order = foodAssertOrderOwnership($orderId, 'courier', $food_courier_id);
if (!foodOrderAllowsCall($order)) {
// بعد التسليم أو قبل الإسناد لا حاجة تشغيلية للاتصال — والقناة تُقفل
jsonError('Calling is only allowed while the delivery is active', 403);
}
if (foodCallQuotaExceeded($orderId, 'courier')) {
jsonError('Too many call attempts for this order', 429);
}
$session = foodCreateCallSession($orderId, $food_courier_id, (string)$order['passenger_id']);
if (!$session) jsonError('Call service unavailable', 502);
// إشعار صامت للزبون عبر موضوع FCM الخاص به — نفس القناة المستعملة في وحدة
// الطعام أصلاً (ممنوع Database::get('main') هنا لجلب توكن الجهاز).
// الاسم المعروض عام عمداً: هوية السائق الحقيقية لا تُكشف للزبون.
foodSendNotificationToPassenger(
(string)$order['passenger_id'],
'مكالمة واردة',
'سائق التوصيل يتصل بك',
[
// تطبيق الراكب يفرز الإشعارات بـ data['category'] — ونُبقي 'type'
// للتوافق مع بقية حمولات وحدة الطعام.
'category' => 'incoming_call',
'type' => 'incoming_call',
'session_id' => $session['session_id'],
'caller_name' => 'سائق التوصيل',
'caller_avatar' => '',
'ride_id' => 'food_' . $orderId,
'food_order_id' => (string)$orderId,
]
);
appLog("[FOOD][CALL] courier {$food_courier_id} → passenger (order {$orderId})", 'INFO');
jsonSuccess([
'session_id' => $session['session_id'],
'expires_in' => $session['expires_in'],
]);
+40
View File
@@ -0,0 +1,40 @@
<?php
// food/courier/delivered.php — تسليم الطلب — تثبيت المال (capture) وقيد أرباح السائق
require_once __DIR__ . '/../connect_courier.php';
$orderId = filterRequest('order_id', 'int');
if (!$orderId) jsonError('order_id is required');
$order = foodAssertOrderOwnership($orderId, 'courier', $food_courier_id);
food_transition_status($orderId, 'delivered', 'courier', $food_courier_id);
if ($order['payment_method'] === 'wallet') {
// المبلغ خُصم بالكامل عند الإنشاء (انظر ملاحظة foodWalletMove) — capture هنا تسجيل محاسبي فقط
$food_con->prepare(
"INSERT INTO food_order_payments (order_id, type, amount, status) VALUES (?,'capture',?,'success')"
)->execute([$orderId, (int)$order['grand_total']]);
} else {
// نقداً: السائق حصّل grand_total كاملاً من الزبون. يحتفظ بـ delivery_fee (أجرته)
// ويبقى ديناً عليه الباقي (items_total+service_fee — حصة المطعم والمنصة) حتى يُسوّى
// إدارياً. هذا قيد محاسبي فقط هنا — لا حركة مالية آلية فعلية بعد (انظر التنبيه في
// admin/payouts.php حول عدم وجود عقد API مؤكد لتحصيل ديون السائق النقدية آلياً).
$courierOwed = (int)$order['grand_total'] - (int)$order['delivery_fee'];
$food_con->prepare(
"INSERT INTO food_order_payments (order_id, type, amount, status) VALUES (?,'cash_settlement',?,'pending')"
)->execute([$orderId, $courierOwed]);
}
// قيد أرباح السائق (أجرة التوصيل) — مُتراكم عمداً بقرار المالك: الأجرة
// تُصرف دورياً مع مقاصّة دَين النقد، لا فورياً عند كل تسليم. صرفها منفصلة
// عن الدَين يعني الدفع للسائق بينما هو يحمل مال المنصة من الطلبات النقدية.
//
// ‏التسوية في food/admin/courier_settlement.php.
//
// ‏(تصحيح لملاحظة سابقة هنا: عقد S2S لإيداع أرباح من المنصة **موجود
// ‏ومؤكد** — driverWallet/add_s2s_reward.php، وهو مستخدم في تعويض عدم
// ‏الحضور ورسوم الإلغاء وتعويضات الشكاوى.)
$food_con->prepare(
"INSERT INTO food_order_payments (order_id, type, amount, status) VALUES (?,'courier_payout',?,'pending')"
)->execute([$orderId, (int)$order['delivery_fee']]);
jsonSuccess(['order_id' => $orderId, 'status' => 'delivered']);
+57
View File
@@ -0,0 +1,57 @@
<?php
// food/courier/earnings.php — ملخص أرباح التوصيل للسائق (اليوم/الأسبوع/الشهر)
// المبالغ من food_orders.delivery_fee للطلبات المسلَّمة فقط — نفس ما يُقيَّد في
// food_order_payments (courier_payout)، والتسوية الفعلية خارج نطاق هذا الملف.
require_once __DIR__ . '/../connect_courier.php';
$sums = $food_con->prepare(
"SELECT
COUNT(*) AS total_orders,
COALESCE(SUM(delivery_fee),0) AS total_earnings,
COALESCE(SUM(CASE WHEN DATE(delivered_at)=CURDATE() THEN delivery_fee END),0) AS today_earnings,
COUNT(CASE WHEN DATE(delivered_at)=CURDATE() THEN 1 END) AS today_orders,
COALESCE(SUM(CASE WHEN delivered_at >= (NOW() - INTERVAL 7 DAY) THEN delivery_fee END),0) AS week_earnings,
COUNT(CASE WHEN delivered_at >= (NOW() - INTERVAL 7 DAY) THEN 1 END) AS week_orders,
COALESCE(SUM(CASE WHEN delivered_at >= (NOW() - INTERVAL 30 DAY) THEN delivery_fee END),0) AS month_earnings,
COUNT(CASE WHEN delivered_at >= (NOW() - INTERVAL 30 DAY) THEN 1 END) AS month_orders
FROM food_orders
WHERE courier_id=? AND status='delivered'"
);
$sums->execute([$food_courier_id]);
$row = $sums->fetch() ?: [];
// ديون التحصيل النقدي غير المسوّاة — السائق يحتاج يعرف كم عليه قبل ما يفاجأ بخصم
$owedSt = $food_con->prepare(
"SELECT COALESCE(SUM(p.amount),0) AS owed
FROM food_order_payments p
JOIN food_orders o ON o.id = p.order_id
WHERE o.courier_id=? AND p.type='cash_settlement' AND p.status='pending'"
);
$owedSt->execute([$food_courier_id]);
// آخر 7 أيام مفصّلة — للرسم البياني في التطبيق
$dailySt = $food_con->prepare(
"SELECT DATE(delivered_at) AS day, COUNT(*) AS orders_count, COALESCE(SUM(delivery_fee),0) AS earnings
FROM food_orders
WHERE courier_id=? AND status='delivered' AND delivered_at >= (NOW() - INTERVAL 7 DAY)
GROUP BY DATE(delivered_at)
ORDER BY day DESC"
);
$dailySt->execute([$food_courier_id]);
jsonSuccess([
'today_earnings' => (int)($row['today_earnings'] ?? 0),
'today_orders' => (int)($row['today_orders'] ?? 0),
'week_earnings' => (int)($row['week_earnings'] ?? 0),
'week_orders' => (int)($row['week_orders'] ?? 0),
'month_earnings' => (int)($row['month_earnings'] ?? 0),
'month_orders' => (int)($row['month_orders'] ?? 0),
'total_earnings' => (int)($row['total_earnings'] ?? 0),
'total_orders' => (int)($row['total_orders'] ?? 0),
'pending_cash_owed' => (int)($owedSt->fetch()['owed'] ?? 0),
'daily' => array_map(static fn($d) => [
'day' => $d['day'],
'orders_count' => (int)$d['orders_count'],
'earnings' => (int)$d['earnings'],
], $dailySt->fetchAll()),
]);
+34
View File
@@ -0,0 +1,34 @@
<?php
// food/courier/history.php — سجل طلبات التوصيل المنتهية لهذا السائق
require_once __DIR__ . '/../connect_courier.php';
$limit = (int)(filterRequest('limit', 'int') ?: 20);
$offset = (int)(filterRequest('offset', 'int') ?: 0);
$limit = max(1, min($limit, 50));
$offset = max(0, $offset);
// العنوان النصّي للزبون لا يُعاد في السجل — انتهت الحاجة التشغيلية إليه بالتسليم،
// ونفس قاعدة الحجب المطبَّقة في active.php.
$st = $food_con->prepare(
"SELECT o.id, o.status, o.merchant_id, m.name_ar AS merchant_name_ar, m.address AS merchant_address,
o.delivery_fee, o.grand_total, o.payment_method, o.rating,
o.courier_assigned_at, o.picked_up_at, o.delivered_at, o.created_at,
(SELECT COALESCE(SUM(quantity),0) FROM food_order_items WHERE order_id=o.id) AS items_count,
TIMESTAMPDIFF(MINUTE, o.courier_assigned_at, o.delivered_at) AS duration_minutes
FROM food_orders o
JOIN food_merchants m ON m.id = o.merchant_id
WHERE o.courier_id = ? AND o.status = 'delivered'
ORDER BY o.delivered_at DESC
LIMIT $limit OFFSET $offset"
);
$st->execute([$food_courier_id]);
$orders = array_map(static function (array $o): array {
$o['items_count'] = (int)$o['items_count'];
$o['delivery_fee'] = (int)$o['delivery_fee'];
$o['grand_total'] = (int)$o['grand_total'];
$o['duration_minutes'] = $o['duration_minutes'] === null ? null : (int)$o['duration_minutes'];
return $o;
}, $st->fetchAll());
jsonSuccess(['orders' => $orders, 'limit' => $limit, 'offset' => $offset]);
+48
View File
@@ -0,0 +1,48 @@
<?php
// food/courier/location.php — بثّ موقع السائق أثناء مهمة توصيل نشطة
//
// خصوصية: الموقع يُقبل فقط ما دام الطلب في courier_assigned/picked_up، ويُخزَّن
// في Redis بعمر 90 ثانية لا في قاعدة البيانات. بمجرد التسليم يرفض الخادم أي
// تحديث، وينتهي آخر موقع تلقائياً — فلا يتحول التتبّع إلى مراقبة للسائق بعد
// انتهاء عمله، ولا يبقى أثر دائم لتحركاته في وحدة الطعام.
require_once __DIR__ . '/../connect_courier.php';
$orderId = filterRequest('order_id', 'int');
$lat = filterRequest('lat');
$lng = filterRequest('lng');
if (!$orderId || $lat === null || $lng === null) {
jsonError('order_id, lat and lng are required');
}
$lat = (float)$lat;
$lng = (float)$lng;
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180 || ($lat === 0.0 && $lng === 0.0)) {
jsonError('Invalid coordinates');
}
$order = foodAssertOrderOwnership($orderId, 'courier', $food_courier_id);
if (!in_array($order['status'], ['courier_assigned', 'picked_up'], true)) {
jsonError('Location sharing is only allowed while the delivery is active', 403);
}
$payload = [
'order_id' => $orderId,
'lat' => round($lat, 6),
'lng' => round($lng, 6),
'heading' => filterRequest('heading') !== null ? (float)filterRequest('heading') : null,
'ts' => time(),
];
if ($redis) {
// مصدر مسار الاحتياط: تطبيق الراكب يسحبه من order/courier_location.php
$redis->setex("food:order:{$orderId}:courier_pos", 90, json_encode($payload));
}
// المسار اللحظي — غرفة الزبون على سوكيت الطعام
foodPushToSocket('courier_location', array_merge($payload, [
'passenger_id' => (string)$order['passenger_id'],
]));
jsonSuccess(['order_id' => $orderId, 'ts' => $payload['ts']]);
+76
View File
@@ -0,0 +1,76 @@
<?php
// food/courier/offer_respond.php — قبول/رفض عرض توصيل
// body: order_id, response ('accept'|'reject')
require_once __DIR__ . '/../connect_courier.php';
$orderId = filterRequest('order_id', 'int');
$response = filterRequest('response');
if (!$orderId || !in_array($response, ['accept', 'reject'], true)) {
jsonError('order_id and response (accept|reject) are required');
}
$assignSt = $food_con->prepare(
"SELECT id FROM food_courier_assignments WHERE order_id=? AND courier_id=? AND status='offered'
ORDER BY offered_at DESC LIMIT 1"
);
$assignSt->execute([$orderId, $food_courier_id]);
$assignment = $assignSt->fetch();
if (!$assignment) jsonError('No pending offer for this order', 404);
if ($response === 'reject') {
$food_con->prepare("UPDATE food_courier_assignments SET status='rejected', responded_at=NOW() WHERE id=?")
->execute([$assignment['id']]);
// أعد المحاولة على مرشّح آخر — يعيد استخدام نفس منطق ready.php
$orderSt = $food_con->prepare("SELECT merchant_id FROM food_orders WHERE id=? AND status='ready'");
$orderSt->execute([$orderId]);
if ($order = $orderSt->fetch()) {
$merchantSt = $food_con->prepare("SELECT latitude, longitude FROM food_merchants WHERE id=?");
$merchantSt->execute([$order['merchant_id']]);
if ($merchant = $merchantSt->fetch()) {
$candidates = foodFindNearbyCouriers((float)$merchant['latitude'], (float)$merchant['longitude']);
$priorSt = $food_con->prepare("SELECT courier_id FROM food_courier_assignments WHERE order_id=?");
$priorSt->execute([$orderId]);
$prior = array_column($priorSt->fetchAll(), 'courier_id');
foreach ($candidates as $candidateId) {
if (in_array($candidateId, $prior, true)) continue;
foodOfferOrderToCourier($orderId, $candidateId);
break;
}
}
}
jsonSuccess(['order_id' => $orderId, 'response' => 'rejected']);
}
// accept — القفل الذرّي يمنع سباق القبول لو انتهت مهلة سابقة وعُرض على اثنين معاً
if (!foodLockOrderForCourier($orderId, $food_courier_id)) {
$owner = foodOrderLockOwner($orderId);
if ($owner !== $food_courier_id) {
jsonError('Order was already claimed by another courier', 409);
}
}
// الطلب لم يُسند لأي سائق بعد عند هذه النقطة — لا فحص ملكية هنا، فقط فحص الحالة أدناه
$orderSt = $food_con->prepare("SELECT status FROM food_orders WHERE id=? LIMIT 1");
$orderSt->execute([$orderId]);
$orderRow = $orderSt->fetch();
if (!$orderRow || $orderRow['status'] !== 'ready') {
jsonError('Order is no longer available for assignment', 409);
}
$food_con->beginTransaction();
try {
$food_con->prepare("UPDATE food_orders SET courier_id=? WHERE id=?")->execute([$food_courier_id, $orderId]);
$food_con->prepare("UPDATE food_courier_assignments SET status='accepted', responded_at=NOW() WHERE id=?")
->execute([$assignment['id']]);
$food_con->commit();
} catch (Throwable $e) {
$food_con->rollBack();
appLog('[FOOD][COURIER][offer_respond] ' . $e->getMessage(), 'ERROR');
jsonError('Failed to accept offer', 500);
}
food_transition_status($orderId, 'courier_assigned', 'courier', $food_courier_id);
jsonSuccess(['order_id' => $orderId, 'response' => 'accepted']);
+64
View File
@@ -0,0 +1,64 @@
<?php
// food/courier/order_details.php — تفاصيل مهمة توصيل واحدة (شاشة المهمة عند السائق)
// أصناف الطلب + تفصيل المبالغ + هاتف المطعم للتواصل أثناء الاستلام
require_once __DIR__ . '/../connect_courier.php';
$orderId = filterRequest('order_id', 'int');
if (!$orderId) jsonError('order_id is required');
// يفشل لو لم يكن الطلب مُسنداً لهذا السائق — لا وصول لطلبات الآخرين
foodAssertOrderOwnership($orderId, 'courier', $food_courier_id);
$st = $food_con->prepare(
"SELECT o.id, o.status, o.merchant_id, o.items_total, o.delivery_fee, o.service_fee, o.discount,
o.grand_total, o.payment_method, o.customer_note, o.delivery_lat, o.delivery_lng,
o.created_at, o.ready_at, o.courier_assigned_at, o.picked_up_at, o.delivered_at,
CASE WHEN o.status IN ('courier_assigned','picked_up') THEN o.delivery_address ELSE NULL END AS delivery_address,
m.name_ar AS merchant_name_ar, m.address AS merchant_address, m.latitude AS merchant_lat,
m.longitude AS merchant_lng, m.avg_prep_minutes
FROM food_orders o
JOIN food_merchants m ON m.id = o.merchant_id
WHERE o.id = ? LIMIT 1"
);
$st->execute([$orderId]);
$order = $st->fetch();
if (!$order) jsonError('Order not found', 404);
$itemsSt = $food_con->prepare(
"SELECT name_ar_snapshot, quantity, unit_price, line_total, option_price_json
FROM food_order_items WHERE order_id=? ORDER BY id ASC"
);
$itemsSt->execute([$orderId]);
$items = array_map(static function (array $i): array {
return [
'name_ar' => $i['name_ar_snapshot'],
'quantity' => (int)$i['quantity'],
'unit_price' => (int)$i['unit_price'],
'line_total' => (int)$i['line_total'],
'options' => $i['option_price_json'] ? json_decode($i['option_price_json'], true) : null,
];
}, $itemsSt->fetchAll());
// هاتف المطعم — يُعاد فقط أثناء المهمة النشطة (قبل التسليم)، لا في السجل
$merchantPhone = null;
if (in_array($order['status'], ['courier_assigned', 'picked_up'], true)) {
$phoneSt = $food_con->prepare(
"SELECT phone FROM food_merchant_users
WHERE merchant_id=? AND is_active=1 ORDER BY role='owner' DESC, id ASC LIMIT 1"
);
$phoneSt->execute([$order['merchant_id']]);
$merchantPhone = $phoneSt->fetch()['phone'] ?? null;
}
// ملاحظة: لا هاتف للزبون هنا — قاعدة siro_food معزولة ولا تحمل بيانات الراكب،
// وممنوع Database::get('main') داخل backend/food/. التواصل مع الزبون يبقى عبر
// عنوان التسليم والملاحظة، إلى أن تُضاف قناة اتصال مقنّعة كما في الرحلات.
$order['items'] = $items;
$order['items_count'] = array_sum(array_column($items, 'quantity'));
$order['merchant_phone'] = $merchantPhone;
$order['cash_to_collect'] = $order['payment_method'] === 'cash' ? (int)$order['grand_total'] : 0;
$order['courier_owes'] = $order['payment_method'] === 'cash'
? (int)$order['grand_total'] - (int)$order['delivery_fee']
: 0;
jsonSuccess(['order' => $order]);
+29
View File
@@ -0,0 +1,29 @@
<?php
// food/courier/pending_offers.php — عروض التوصيل المعروضة على هذا السائق حالياً
// (polling fallback — food_socket يدفع 'food_delivery_offer' لحظياً، وهذا احتياطي
// لو انقطع اتصال السوكيت أو كان التطبيق لا يحمل اتصالاً حياً بالسوكيت)
require_once __DIR__ . '/../connect_courier.php';
$st = $food_con->prepare(
"SELECT a.order_id, a.offered_at
FROM food_courier_assignments a
JOIN food_orders o ON o.id = a.order_id
WHERE a.courier_id = ? AND a.status = 'offered' AND a.offered_at > (NOW() - INTERVAL 20 SECOND)
AND o.status = 'ready' AND o.courier_id IS NULL
ORDER BY a.offered_at ASC"
);
$st->execute([$food_courier_id]);
// نفس حمولة مسار السوكيت حرفياً (foodBuildCourierOfferPayload) — شاشة العرض
// عند السائق واحدة، فلا يجوز أن تختلف الحقول حسب المسار الذي وصل منه العرض.
$offers = [];
foreach ($st->fetchAll() as $row) {
$payload = foodBuildCourierOfferPayload((int)$row['order_id']);
if (!$payload) continue;
$offers[] = array_merge(
['order_id' => (int)$row['order_id'], 'offered_at' => $row['offered_at'], 'offer_ttl_seconds' => 20],
$payload
);
}
jsonSuccess(['offers' => $offers]);
+11
View File
@@ -0,0 +1,11 @@
<?php
// food/courier/picked_up.php — السائق استلم الطلب من المطعم
require_once __DIR__ . '/../connect_courier.php';
$orderId = filterRequest('order_id', 'int');
if (!$orderId) jsonError('order_id is required');
foodAssertOrderOwnership($orderId, 'courier', $food_courier_id);
food_transition_status($orderId, 'picked_up', 'courier', $food_courier_id);
jsonSuccess(['order_id' => $orderId, 'status' => 'picked_up']);
+32
View File
@@ -0,0 +1,32 @@
<?php
// food/courier/status.php — حالة السائق في وحدة التوصيل عند فتح التبويب
// (وضع التوصيل مخزَّن في Redis لا في التطبيق — بدون هذا النداء يفتح التطبيق
// على "مغلق" بينما السائق ما زال فعلياً في food:couriers:opted_in ويستقبل عروضاً)
require_once __DIR__ . '/../connect_courier.php';
$enabled = false;
if ($redis) {
$enabled = (bool)$redis->sIsMember('food:couriers:opted_in', $food_courier_id);
}
$activeSt = $food_con->prepare(
"SELECT COUNT(*) AS c FROM food_orders
WHERE courier_id=? AND status IN ('courier_assigned','picked_up')"
);
$activeSt->execute([$food_courier_id]);
$activeCount = (int)($activeSt->fetch()['c'] ?? 0);
$todaySt = $food_con->prepare(
"SELECT COUNT(*) AS orders_count, COALESCE(SUM(delivery_fee),0) AS earnings
FROM food_orders
WHERE courier_id=? AND status='delivered' AND DATE(delivered_at)=CURDATE()"
);
$todaySt->execute([$food_courier_id]);
$today = $todaySt->fetch();
jsonSuccess([
'delivery_mode_enabled' => $enabled,
'active_orders_count' => $activeCount,
'today_orders_count' => (int)$today['orders_count'],
'today_earnings' => (int)$today['earnings'],
]);
@@ -0,0 +1,11 @@
<?php
// food/courier/toggle_availability.php — تفعيل/إيقاف "وضع التوصيل" للسائق
// السائق لا يظهر لعروض التوصيل إلا إذا كان أيضاً متاحاً فعلياً في geo:drivers:available
require_once __DIR__ . '/../connect_courier.php';
$enable = filterRequest('enabled', 'bool');
if ($enable === null) jsonError('enabled (true|false) is required');
foodCourierOptIn($food_courier_id, $enable);
jsonSuccess(['courier_id' => $food_courier_id, 'delivery_mode_enabled' => $enable]);