Files
Siro/backend/ride/earnings/summary.php
T

211 lines
9.9 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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');