Update: 2026-08-08 12:58:16
This commit is contained in:
@@ -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');
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* ride/earnings/trips.php — تفصيل أرباح كل رحلة
|
||||
* ─────────────────────────────────────────────────────────────
|
||||
* السؤال الأول عند كل سائق في العالم: «لماذا أخذت هذا المبلغ من هذه
|
||||
* الرحلة؟» — ولم يكن في التطبيق ما يجيبه. `payment_history_driver_page`
|
||||
* يعرض حركات محفظة (شحن، سحب، تحويل)، لا تركيبة أجرة رحلة.
|
||||
*
|
||||
* وأثر ذلك ليس شكاوى فحسب: المنصة تمنح إعفاء عمولة ٠٪ عند تحقيق سلسلة
|
||||
* (`hasZeroCommission` في `finish_ride_updates.php`) — هدية حقيقية
|
||||
* يدفع ثمنها الصندوق ولا يراها السائق في أي شاشة. نحن ندفع ولا نحصد.
|
||||
*
|
||||
* POST: date (اختياري YYYY-MM-DD) أو range=week|month، limit
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$driverId = $user_id ?? '';
|
||||
if (empty($driverId) || ($role ?? '') !== 'driver') {
|
||||
jsonError('Unauthorized', 401);
|
||||
}
|
||||
|
||||
$date = filterRequest('date');
|
||||
$range = filterRequest('range');
|
||||
$limit = (int) (filterRequest('limit', 'int') ?: 50);
|
||||
$limit = max(1, min($limit, 200));
|
||||
|
||||
$params = [$driverId];
|
||||
|
||||
if (!empty($date)) {
|
||||
// تاريخ اليوم الواحد: الاستعمال الأغلب — السائق ينقر عموداً في الرسم
|
||||
// البياني ليرى رحلات ذلك اليوم.
|
||||
$dateCondition = 'DATE(r.created_at) = ?';
|
||||
$params[] = $date;
|
||||
} elseif ($range === 'month') {
|
||||
$dateCondition = 'r.created_at >= DATE_FORMAT(NOW(), "%Y-%m-01")';
|
||||
} else {
|
||||
$dateCondition = 'r.created_at >= DATE(NOW()) - INTERVAL 6 DAY';
|
||||
}
|
||||
|
||||
try {
|
||||
$st = $con->prepare("
|
||||
SELECT r.id, r.created_at, r.date, r.time, r.endtime,
|
||||
r.rideTimeStart, r.rideTimeFinish,
|
||||
r.start_location, r.end_location,
|
||||
r.price, r.price_for_driver, r.price_for_passenger,
|
||||
r.distance, r.carType, r.paymentMethod,
|
||||
r.ai_negotiated_bonus,
|
||||
GREATEST(TIMESTAMPDIFF(MINUTE, r.rideTimeStart, r.rideTimeFinish), 0) AS minutes
|
||||
FROM ride r
|
||||
WHERE r.driver_id = ?
|
||||
AND r.status IN ('Finished', 'finished')
|
||||
AND $dateCondition
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT $limit
|
||||
");
|
||||
$st->execute($params);
|
||||
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log('[earnings/trips] ' . $e->getMessage());
|
||||
jsonError('تعذّر جلب تفاصيل الرحلات', 500);
|
||||
}
|
||||
|
||||
$trips = array_map(static function (array $r): array {
|
||||
$gross = round((float) $r['price'], 2);
|
||||
$net = round((float) $r['price_for_driver'], 2);
|
||||
$bonus = round((float) ($r['ai_negotiated_bonus'] ?? 0), 2);
|
||||
|
||||
// العمولة مشتقّة من الفرق لا محسوبة بنسبة اليوم: النسبة تتغيّر
|
||||
// بالمنطقة والوقت وخصومات العمولة، وحسابها الآن بالنسبة الحالية
|
||||
// يُظهر للسائق رقماً يخالف ما اقتُطع منه فعلاً وقت الرحلة.
|
||||
$commission = round(max($gross - $net, 0), 2);
|
||||
|
||||
// إعفاء العمولة: صفرٌ حقيقي لا غياب بيانات. نميّزه صراحةً ليظهر في
|
||||
// التطبيق كمكسب («عمولة معفاة») لا كسطر فارغ.
|
||||
$zeroCommission = $gross > 0 && $commission <= 0.009;
|
||||
|
||||
return [
|
||||
'ride_id' => (int) $r['id'],
|
||||
'at' => $r['created_at'],
|
||||
'from' => $r['start_location'],
|
||||
'to' => $r['end_location'],
|
||||
'car_type' => $r['carType'],
|
||||
'payment_method' => $r['paymentMethod'],
|
||||
'distance_km' => round((float) $r['distance'], 1),
|
||||
'minutes' => (int) $r['minutes'],
|
||||
'breakdown' => [
|
||||
'gross' => $gross,
|
||||
'commission' => $commission,
|
||||
'bonus' => $bonus,
|
||||
'net' => $net,
|
||||
'zero_commission' => $zeroCommission,
|
||||
],
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
// مجاميع القائمة المعروضة نفسها. حسابها في التطبيق كان سيعني رقماً
|
||||
// يختلف عن الملخّص عند اختلاف التقريب — وهو بالضبط نوع التناقض الذي
|
||||
// جاءت هذه النقطة لتنهيه.
|
||||
$sum = [
|
||||
'trips' => count($trips),
|
||||
'gross' => 0.0,
|
||||
'commission' => 0.0,
|
||||
'net' => 0.0,
|
||||
];
|
||||
|
||||
foreach ($trips as $t) {
|
||||
$sum['gross'] += $t['breakdown']['gross'];
|
||||
$sum['commission'] += $t['breakdown']['commission'];
|
||||
$sum['net'] += $t['breakdown']['net'];
|
||||
}
|
||||
|
||||
foreach (['gross', 'commission', 'net'] as $k) {
|
||||
$sum[$k] = round($sum[$k], 2);
|
||||
}
|
||||
|
||||
jsonSuccess([
|
||||
'trips' => $trips,
|
||||
'summary' => $sum,
|
||||
'as_of' => date('c'),
|
||||
], 'success');
|
||||
Reference in New Issue
Block a user