Update: 2026-08-08 12:58:16

This commit is contained in:
Hamza-Ayed
2026-08-08 12:58:17 +03:00
parent 611b5e616d
commit fae0e4a38a
55 changed files with 5771 additions and 1820 deletions
+210
View File
@@ -0,0 +1,210 @@
<?php
/**
* ride/earnings/summary.php — ملخّص أرباح السائق من نقطة واحدة
* ─────────────────────────────────────────────────────────────
* ‏كانت شاشة الأرباح تبني نفسها من أربعة نداءات مختلفة، لكلٍّ منها لحظة
* ‏وصول ومنطق تجميع مستقلّان. النتيجة أن السائق يرى أرقاماً لا تتفق —
* ‏والسائق الذي لا يثق برقم أرباحه لا يثق بالمنصة كلها.
*
* ‏هذه النقطة تُرجع كل شيء بختم زمني واحد.
*
* ‏**تعريف مقصود:** «الأرباح» هنا من الرحلات لا من المحفظة. سؤال «كم
* ‏كسبت؟» غير سؤال «كم رصيدي؟»: الأول عن العمل، والثاني عن المال المتاح
* ‏بعد السحب والشحن والاقتطاعات. خلطهما هو ما أنتج الرقمين المتناقضين.
* ‏الرصيد يبقى في شاشة المحفظة، ويُشار إليه هنا بالاقتطاعات فقط.
*
* ‏خطآن في `gamification/getWeeklyAggregate.php` عولجا هنا:
*
* ١. `status = 'Finished'` وحدها. الحالة مكتوبة بحرفين مختلفين في
* ‏الجدول تاريخياً ('Finished' و'finished') — كما وثّق
* ‏`driver_assurance/eligibility.php` — فكان نصف رحلات السائق
* ‏تقريباً يسقط من أرباحه المعروضة.
*
* ٢. `TIMESTAMPDIFF(MINUTE, r.time, r.endtime)` على عمودَي TIME.
* ‏رحلة تبدأ 23:40 وتنتهي 00:10 تُحسب بـ**سالب** ٢٣ ساعة ونصف،
* ‏فتأكل ساعات يوم كامل. نستعمل `rideTimeStart`/`rideTimeFinish`
* ‏وهما timestamps كاملة.
*/
require_once __DIR__ . '/../../connect.php';
$driverId = $user_id ?? '';
if (empty($driverId) || ($role ?? '') !== 'driver') {
jsonError('Unauthorized', 401);
}
// ‏الحالتان معاً في كل استعلام أدناه. ثابت واحد يمنع أن يُصحَّح أحدها
// ‏ويُنسى الآخر.
const EARNINGS_FINISHED = "r.status IN ('Finished', 'finished')";
/**
* ‏مجاميع فترة واحدة.
*
* ‏`price_for_driver` هو حصة السائق المثبَّتة لحظة عرض السعر — وهو ما
* ‏وُعد به قبل القبول. استعماله (لا اشتقاق نسبة العمولة الآن) يضمن أن
* ‏ما يراه في شاشة الأرباح هو ما رآه في شاشة الطلب.
*/
function earningsPeriod(PDO $con, string $driverId, string $dateCondition): array
{
$sql = "
SELECT
COUNT(r.id) AS trips,
COALESCE(SUM(r.price_for_driver), 0) AS net,
COALESCE(SUM(r.price), 0) AS gross,
COALESCE(SUM(r.distance), 0) AS distance,
COALESCE(SUM(
GREATEST(TIMESTAMPDIFF(MINUTE, r.rideTimeStart, r.rideTimeFinish), 0)
), 0) AS minutes
FROM ride r
WHERE r.driver_id = ?
AND " . EARNINGS_FINISHED . "
AND $dateCondition
";
$st = $con->prepare($sql);
$st->execute([$driverId]);
$row = $st->fetch(PDO::FETCH_ASSOC) ?: [];
$net = round((float) ($row['net'] ?? 0), 2);
$gross = round((float) ($row['gross'] ?? 0), 2);
return [
'trips' => (int) ($row['trips'] ?? 0),
'net' => $net,
'gross' => $gross,
// ‏العمولة تُعرض صراحةً لا تُطرح بصمت. السائق الذي يرى «كسبتَ ٢٠،
// ‏عمولة المنصة ٣، لك ١٧» يفهم؛ والذي يرى ١٧ وحدها يخمّن ويشكّ.
'commission' => round(max($gross - $net, 0), 2),
'distance' => round((float) ($row['distance'] ?? 0), 1),
'minutes' => (int) ($row['minutes'] ?? 0),
];
}
try {
$today = earningsPeriod($con, $driverId, 'DATE(r.created_at) = CURDATE()');
$week = earningsPeriod($con, $driverId, 'r.created_at >= DATE(NOW()) - INTERVAL 6 DAY');
$month = earningsPeriod($con, $driverId, 'r.created_at >= DATE_FORMAT(NOW(), "%Y-%m-01")');
// ‏الأسبوع السابق للمقارنة. المقارنة بالذات لا بالسائقين الآخرين:
// ‏«أعلى من أسبوعك الماضي» تحفّز الجميع، بينما ترتيب السائقين يُحبط
// ‏أكثرهم — ومن يحتاج التحفيز أكثر هو من سيقع في أسفل الترتيب.
$prevWeek = earningsPeriod(
$con,
$driverId,
'r.created_at >= DATE(NOW()) - INTERVAL 13 DAY AND r.created_at < DATE(NOW()) - INTERVAL 6 DAY'
);
$trend = null;
if ($prevWeek['net'] > 0) {
$trend = round((($week['net'] - $prevWeek['net']) / $prevWeek['net']) * 100, 1);
}
// ── السلسلة اليومية للرسم البياني ──
$st = $con->prepare("
SELECT DATE(r.created_at) AS day,
COUNT(r.id) AS trips,
COALESCE(SUM(r.price_for_driver), 0) AS net,
COALESCE(SUM(
GREATEST(TIMESTAMPDIFF(MINUTE, r.rideTimeStart, r.rideTimeFinish), 0)
), 0) AS minutes
FROM ride r
WHERE r.driver_id = ?
AND " . EARNINGS_FINISHED . "
AND r.created_at >= DATE(NOW()) - INTERVAL 6 DAY
GROUP BY DATE(r.created_at)
ORDER BY DATE(r.created_at) ASC
");
$st->execute([$driverId]);
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
// ‏الأيام الفارغة تُملأ بأصفار هنا لا في التطبيق: رسم بياني ينقصه يوم
// ‏يُزيح باقي الأعمدة فيقرأ السائق الثلاثاء مكان الأربعاء.
$byDay = [];
foreach ($rows as $r) {
$byDay[$r['day']] = $r;
}
$daily = [];
for ($i = 6; $i >= 0; $i--) {
$day = date('Y-m-d', strtotime("-$i day"));
$r = $byDay[$day] ?? null;
$daily[] = [
'day' => $day,
'trips' => (int) ($r['trips'] ?? 0),
'net' => round((float) ($r['net'] ?? 0), 2),
'minutes' => (int) ($r['minutes'] ?? 0),
];
}
$best = null;
foreach ($daily as $d) {
if ($best === null || $d['net'] > $best['net']) {
$best = $d;
}
}
} catch (PDOException $e) {
error_log('[earnings/summary] ' . $e->getMessage());
jsonError('تعذّر حساب الأرباح حالياً', 500);
}
// ── الاقتطاعات ──
// ‏تُعرض في شاشة الأرباح لا في شاشة المنتج وحدها. بعد محرك الالتزامات
// ‏صار يُخصم من السائق قسط تأمين ودَين وقود، واقتطاعٌ لا يظهر بجانب
// ‏الأرباح التي اقتُطع منها يُقرأ سرقةً مهما كان مشروعاً.
$deductions = ['today' => 0.0, 'week' => 0.0, 'month' => 0.0, 'outstanding' => 0.0, 'items' => []];
try {
$st = $con->prepare("
SELECT
COALESCE(SUM(CASE WHEN DATE(updated_at) = CURDATE() THEN amount_collected ELSE 0 END), 0) AS today,
COALESCE(SUM(CASE WHEN updated_at >= DATE(NOW()) - INTERVAL 6 DAY THEN amount_collected ELSE 0 END), 0) AS week,
COALESCE(SUM(CASE WHEN updated_at >= DATE_FORMAT(NOW(), '%Y-%m-01') THEN amount_collected ELSE 0 END), 0) AS month,
COALESCE(SUM(amount_remaining), 0) AS outstanding
FROM obligation_ledger
WHERE driver_id = ?
");
$st->execute([$driverId]);
$d = $st->fetch(PDO::FETCH_ASSOC) ?: [];
$deductions['today'] = round((float) ($d['today'] ?? 0), 2);
$deductions['week'] = round((float) ($d['week'] ?? 0), 2);
$deductions['month'] = round((float) ($d['month'] ?? 0), 2);
$deductions['outstanding'] = round((float) ($d['outstanding'] ?? 0), 2);
// ‏تفصيل حسب المنتج: «اقتُطع ٣» سؤال، و«٣ لمحفظة الوقود» جواب.
$st = $con->prepare("
SELECT l.product_code, p.name_ar,
COALESCE(SUM(l.amount_remaining), 0) AS outstanding
FROM obligation_ledger l
JOIN driver_obligations o ON o.id = l.obligation_id
JOIN obligation_products p ON p.id = o.product_id
WHERE l.driver_id = ? AND l.amount_remaining > 0
GROUP BY l.product_code, p.name_ar
");
$st->execute([$driverId]);
$deductions['items'] = array_map(static fn(array $r): array => [
'product' => $r['product_code'],
'name' => $r['name_ar'],
'outstanding' => round((float) $r['outstanding'], 2),
], $st->fetchAll(PDO::FETCH_ASSOC));
} catch (PDOException $e) {
// ‏غياب جداول الالتزامات (قبل تنفيذ الترحيل) ليس خطأً يُفشل الشاشة:
// ‏الأرباح تُعرض والاقتطاعات أصفار. إسقاط الشاشة كلها لأن ميزةً
// ‏جانبية غير مهيّأة عقوبةٌ في غير محلها.
error_log('[earnings/summary] تعذّرت قراءة الاقتطاعات: ' . $e->getMessage());
}
jsonSuccess([
'today' => $today,
'week' => $week,
'month' => $month,
'prev_week' => $prevWeek,
'trend_percent' => $trend,
'daily' => $daily,
'best_day' => $best,
'deductions' => $deductions,
// ‏ختم زمني واحد لكل الأرقام. التطبيق يعرضه كـ«آخر تحديث»، فيعرف
// ‏السائق متى قِيست هذه الأرقام بدل أن يظنّ القديم حاضراً.
'as_of' => date('c'),
], 'success');