From bc1b0129e8320c2d7a8d2fbfc8fd75decef1ba36 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 25 Jul 2026 16:55:58 +0300 Subject: [PATCH] Fix v2 analytics status matching; give Growth and Analytics real charts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight queries across the v2 modules counted only status = 'Finished' and so reported zero on live data, where the current ride pipeline writes 'completed': realtime revenue for today and yesterday, financial stats, settlements, driver scorecard, driver ranking, and both revenue queries. All now match either spelling. Growth and Advanced Analytics rendered through the generic shape-detecting renderer, which produced raw tables that said little. Both now have purpose- built views: - Growth: totals, 30-day joins, and a two-series daily chart. growth.php only returns days that had signups, so the series is expanded to a continuous 30-day axis with explicit zeros — plotting the returned rows directly would hide the gaps and make a quiet month look like steady growth. A caption states how many days actually had a signup. - Analytics: revenue summary tiles, a daily revenue trend, and the captain ranking, with a note explaining that platform share is what remains after the captain's cut. Null aggregates render as "—" rather than 0.00, and markers are drawn only on days with a value so a flat zero line stays readable. Co-Authored-By: Claude Opus 5 --- backend/Admin/v2/analytics/driver_ranking.php | 2 +- backend/Admin/v2/analytics/revenue.php | 4 +- backend/Admin/v2/financial/settlements.php | 2 +- backend/Admin/v2/financial/stats.php | 2 +- backend/Admin/v2/quality/driver_scorecard.php | 2 +- backend/Admin/v2/realtime_dashboard.php | 4 +- dashboard/siro-admin/index.html | 4 +- dashboard/siro-admin/js/app.js | 219 +++++++++++++++++- 8 files changed, 220 insertions(+), 19 deletions(-) diff --git a/backend/Admin/v2/analytics/driver_ranking.php b/backend/Admin/v2/analytics/driver_ranking.php index bc37dfd0..7a6b2e5c 100644 --- a/backend/Admin/v2/analytics/driver_ranking.php +++ b/backend/Admin/v2/analytics/driver_ranking.php @@ -17,7 +17,7 @@ try { SUM(r.price) as total_revenue FROM driver d JOIN ride r ON d.id = r.driver_id - WHERE r.status = 'Finished' + WHERE LOWER(r.status) IN ('finished','completed') GROUP BY d.id, d.first_name, d.last_name, d.phone ORDER BY completed_rides DESC LIMIT 10 diff --git a/backend/Admin/v2/analytics/revenue.php b/backend/Admin/v2/analytics/revenue.php index 62de0dc7..40f98b3c 100644 --- a/backend/Admin/v2/analytics/revenue.php +++ b/backend/Admin/v2/analytics/revenue.php @@ -17,7 +17,7 @@ try { SUM(price - price_for_driver) as company_profit, COUNT(*) as total_rides FROM ride - WHERE status = 'Finished' + WHERE LOWER(status) IN ('finished','completed') AND created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(created_at) ORDER BY date ASC @@ -32,7 +32,7 @@ try { SUM(price - price_for_driver) as total_profit_all, AVG(price) as avg_ride_price FROM ride - WHERE status = 'Finished' + WHERE LOWER(status) IN ('finished','completed') AND created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) "); $stmt->execute(); diff --git a/backend/Admin/v2/financial/settlements.php b/backend/Admin/v2/financial/settlements.php index 44fcd176..9fe905c6 100644 --- a/backend/Admin/v2/financial/settlements.php +++ b/backend/Admin/v2/financial/settlements.php @@ -17,7 +17,7 @@ try { SUM(r.price_for_driver) as total_earned, COUNT(r.id) as total_rides FROM driver d - LEFT JOIN ride r ON d.id = r.driver_id AND r.status = 'Finished' + LEFT JOIN ride r ON d.id = r.driver_id AND LOWER(r.status) IN ('finished','completed') GROUP BY d.id HAVING total_earned > 0 ORDER BY total_earned DESC diff --git a/backend/Admin/v2/financial/stats.php b/backend/Admin/v2/financial/stats.php index 255bd1ee..bc76ce51 100644 --- a/backend/Admin/v2/financial/stats.php +++ b/backend/Admin/v2/financial/stats.php @@ -18,7 +18,7 @@ try { 0 as cash_payments, 0 as digital_payments FROM ride - WHERE status = 'Finished' + WHERE LOWER(status) IN ('finished','completed') "); $stmt->execute(); $stats = $stmt->fetch(PDO::FETCH_ASSOC); diff --git a/backend/Admin/v2/quality/driver_scorecard.php b/backend/Admin/v2/quality/driver_scorecard.php index 3d8b1160..0a2b0312 100644 --- a/backend/Admin/v2/quality/driver_scorecard.php +++ b/backend/Admin/v2/quality/driver_scorecard.php @@ -40,7 +40,7 @@ try { $stmt = $con->prepare(" SELECT COUNT(*) as total_rides, - SUM(CASE WHEN status = 'Finished' THEN 1 ELSE 0 END) as completed_rides, + SUM(CASE WHEN LOWER(status) IN ('finished','completed') THEN 1 ELSE 0 END) as completed_rides, SUM(CASE WHEN status = 'cancel' AND cancel_by = 'driver' THEN 1 ELSE 0 END) as driver_cancellations, SUM(CASE WHEN status = 'cancel' AND cancel_by = 'passenger' THEN 1 ELSE 0 END) as passenger_cancellations FROM ride diff --git a/backend/Admin/v2/realtime_dashboard.php b/backend/Admin/v2/realtime_dashboard.php index 044c515d..400acb59 100644 --- a/backend/Admin/v2/realtime_dashboard.php +++ b/backend/Admin/v2/realtime_dashboard.php @@ -26,12 +26,12 @@ try { $online_drivers = $stmt->fetchColumn(); // 3. إيرادات اليوم - $stmt = $con->prepare("SELECT IFNULL(SUM(price_for_passenger), 0) FROM ride WHERE status = 'Finished' AND DATE(created_at) = CURDATE()"); + $stmt = $con->prepare("SELECT IFNULL(SUM(price_for_passenger), 0) FROM ride WHERE LOWER(status) IN ('finished','completed') AND DATE(created_at) = CURDATE()"); $stmt->execute(); $revenue_today = $stmt->fetchColumn(); // إيرادات الأمس (للمقارنة) - $stmt = $con->prepare("SELECT IFNULL(SUM(price_for_passenger), 0) FROM ride WHERE status = 'Finished' AND DATE(created_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)"); + $stmt = $con->prepare("SELECT IFNULL(SUM(price_for_passenger), 0) FROM ride WHERE LOWER(status) IN ('finished','completed') AND DATE(created_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)"); $stmt->execute(); $revenue_yesterday = $stmt->fetchColumn(); diff --git a/dashboard/siro-admin/index.html b/dashboard/siro-admin/index.html index 1f80c863..281f3770 100644 --- a/dashboard/siro-admin/index.html +++ b/dashboard/siro-admin/index.html @@ -15,7 +15,7 @@ - + @@ -609,6 +609,6 @@ - + diff --git a/dashboard/siro-admin/js/app.js b/dashboard/siro-admin/js/app.js index 955c4222..fc843700 100644 --- a/dashboard/siro-admin/js/app.js +++ b/dashboard/siro-admin/js/app.js @@ -11,7 +11,7 @@ // Bump together with the ?v= query in index.html. Shown in the UI and in the // diagnostics report so "the deploy did nothing" can be answered with a fact // rather than a guess about caching. - const BUILD = '2026-07-25-7'; + const BUILD = '2026-07-25-8'; // ── Localisation ───────────────────────────────────────────────────────── // Arabic is the operators' language; English is kept because several screens @@ -1024,17 +1024,13 @@ }, { id: 'growth', group: 'Realtime & Analytics', icon: 'ph-trend-up', title: 'Growth', - subtitle: 'Daily signups for passengers and captains', - panels: [{ title: 'Growth', path: '/Admin/v2/analytics/growth.php' }], + subtitle: 'How many passengers and captains joined each day over the last 30 days', + custom: renderGrowth, }, { id: 'analyticsV2', group: 'Realtime & Analytics', icon: 'ph-chart-line', title: 'Advanced Analytics', subtitle: 'Revenue, ranking and dashboard aggregates from the v2 engine', - panels: [ - { title: 'Revenue', path: '/Admin/v2/analytics/revenue.php' }, - { title: 'Driver ranking', path: '/Admin/v2/analytics/driver_ranking.php' }, - { title: 'Dashboard data', path: '/Admin/v2/analytics/dashboard_data.php' }, - ], + custom: renderAnalytics, }, { id: 'financeV2', group: 'Finance', icon: 'ph-bank', title: 'Financial V2', @@ -1289,6 +1285,145 @@ }); } + + // ── Growth ─────────────────────────────────────────────────────────────── + // growth.php returns one row per day that had signups — days with none are + // simply absent. Plotting those rows directly would compress gaps and make + // a quiet week look like steady growth, so the series is expanded to a + // continuous 30-day axis with explicit zeros. + function densifyDays(rows, valueKey, days = 30) { + const byDate = new Map(); + (rows || []).forEach((r) => byDate.set(String(r.date).slice(0, 10), Number(r[valueKey]) || 0)); + + const series = []; + const today = new Date(); + for (let i = days - 1; i >= 0; i--) { + const d = new Date(today); + d.setDate(today.getDate() - i); + const iso = d.toISOString().slice(0, 10); + series.push({ + label: `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}`, + value: byDate.get(iso) || 0, + }); + } + return series; + } + + const sumSeries = (series) => series.reduce((a, p) => a + p.value, 0); + + async function renderGrowth(host) { + host.innerHTML = '
Loading growth…
'; + + let payload; + try { + payload = await api('/Admin/v2/analytics/growth.php'); + } catch (err) { + if (handleApiError(err, 'growth')) return; + host.innerHTML = `
${esc(err.message)}
`; + return; + } + + const passengers = densifyDays(payload?.passenger_daily, 'new_passengers'); + const drivers = densifyDays(payload?.driver_daily, 'new_drivers'); + const totals = payload?.totals || {}; + + const joinedP = sumSeries(passengers); + const joinedD = sumSeries(drivers); + const activeDays = passengers.filter((p, i) => p.value || drivers[i].value).length; + + host.innerHTML = ` +
+ ${growthTile(fmtInt(totals.passengers), 'Passengers in total', 'ph-users', 'warning')} + ${growthTile(fmtInt(totals.drivers), 'Captains in total', 'ph-steering-wheel', 'info')} + ${growthTile(`+${fmtInt(joinedP)}`, 'Passengers joined · 30 days', 'ph-user-plus', 'primary')} + ${growthTile(`+${fmtInt(joinedD)}`, 'Captains joined · 30 days', 'ph-user-plus', 'success')} +
+ +
+
+

Signups per day last 30 days

+
+ Passengers + Captains +
+
+
+ ${activeDays === 0 + ? '
Nobody signed up in the last 30 days.
' + : `
+ ${activeDays} of 30 days had at least one signup +
`} +
`; + + drawMultiLine('growthChart', [ + { label: 'Passengers', color: '#6366f1', points: passengers }, + { label: 'Captains', color: '#10b981', points: drivers }, + ]); + } + + function growthTile(value, label, icon, tone) { + return ` +
+
+
+
${esc(value)}
+
${esc(label)}
+
+
+
+
`; + } + + // ── Advanced analytics ─────────────────────────────────────────────────── + async function renderAnalytics(host) { + host.innerHTML = ` +
+ + Revenue is counted from completed rides only. Total is what passengers paid; + platform share is what remains after the captain's cut. +
+
Loading revenue…
+
Loading captain ranking…
`; + + // Revenue + try { + const rev = await api('/Admin/v2/analytics/revenue.php'); + const daily = rev?.daily || []; + const summary = rev?.summary || {}; + const series = densifyDays(daily, 'total_revenue'); + const rides = densifyDays(daily, 'total_rides'); + + $('anRevenue').innerHTML = ` +
+

Revenue last 30 days

+
+
+
${summary.total_revenue_all == null ? '—' : fmtMoney(summary.total_revenue_all)}
Total collected
+
${summary.total_profit_all == null ? '—' : fmtMoney(summary.total_profit_all)}
Platform share
+
${summary.avg_ride_price == null ? '—' : fmtMoney(summary.avg_ride_price)}
Average fare
+
${fmtInt(sumSeries(rides))}
Completed rides
+
+
+ ${sumSeries(series) === 0 ? '
No completed rides carried a fare in this period.
' : ''}`; + + drawMultiLine('revenueTrend', [{ label: 'Revenue', color: '#06b6d4', points: series }]); + } catch (err) { + if (handleApiError(err, 'analytics-revenue')) return; + $('anRevenue').innerHTML = `
${esc(err.message)}
`; + } + + // Captain ranking + try { + const ranking = await api('/Admin/v2/analytics/driver_ranking.php'); + const body = $('anRanking'); + body.innerHTML = '

Captain ranking

'; + renderPayload(body.querySelector('.panel-body'), ranking); + } catch (err) { + if (handleApiError(err, 'analytics-ranking')) return; + $('anRanking').innerHTML = `
${esc(err.message)}
`; + } + } + // ── Blacklist & removal ────────────────────────────────────────────────── // Deletion here is a real DELETE against passengers/driver — the account and // its login are gone. The console therefore demands the phone number be @@ -2708,6 +2843,71 @@ }); } + + // Multiple series on one axis, sharing a scale so the lines are comparable. + function drawMultiLine(id, seriesList) { + chartData.set(id, { type: 'multi', series: seriesList }); + const c = prepareCanvas(id); + if (!c || !seriesList.length || !seriesList[0].points.length) return; + const { ctx, w, h } = c; + + const padL = 38, padR = 14, padTop = 14, padBottom = 26; + const count = seriesList[0].points.length; + const max = Math.max(1, ...seriesList.flatMap((s) => s.points.map((p) => p.value))); + const stepX = count > 1 ? (w - padL - padR) / (count - 1) : 0; + const y = (v) => h - padBottom - (v / max) * (h - padTop - padBottom); + + // grid + scale + ctx.strokeStyle = 'rgba(255,255,255,0.06)'; + ctx.fillStyle = '#64748b'; + ctx.font = '10px Inter, sans-serif'; + const ticks = Math.min(4, max); + for (let i = 0; i <= ticks; i++) { + const gy = padTop + i * (h - padTop - padBottom) / ticks; + ctx.beginPath(); ctx.moveTo(padL, gy); ctx.lineTo(w - padR, gy); ctx.stroke(); + ctx.textAlign = 'right'; + ctx.fillText(String(Math.round(max - i * max / ticks)), padL - 6, gy + 3); + } + + seriesList.forEach((serie) => { + const pts = serie.points.map((p, i) => ({ x: padL + i * stepX, y: y(p.value) })); + + const grad = ctx.createLinearGradient(0, padTop, 0, h - padBottom); + grad.addColorStop(0, serie.color + '55'); + grad.addColorStop(1, serie.color + '00'); + ctx.beginPath(); + pts.forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y))); + ctx.lineTo(pts[pts.length - 1].x, h - padBottom); + ctx.lineTo(pts[0].x, h - padBottom); + ctx.closePath(); + ctx.fillStyle = grad; + ctx.fill(); + + ctx.beginPath(); + pts.forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y))); + ctx.strokeStyle = serie.color; + ctx.lineWidth = 2.2; + ctx.lineJoin = 'round'; + ctx.stroke(); + + // mark only non-zero days: dots on a flat zero line are just noise + serie.points.forEach((p, i) => { + if (!p.value) return; + ctx.beginPath(); + ctx.arc(pts[i].x, pts[i].y, 3.2, 0, Math.PI * 2); + ctx.fillStyle = serie.color; + ctx.fill(); + }); + }); + + ctx.fillStyle = '#64748b'; + ctx.textAlign = 'center'; + const every = Math.ceil(count / 8); + seriesList[0].points.forEach((p, i) => { + if (i % every === 0 || i === count - 1) ctx.fillText(p.label, padL + i * stepX, h - 8); + }); + } + function drawBarChart(id, series) { chartData.set(id, { type: 'bar', series }); const c = prepareCanvas(id); @@ -2796,7 +2996,8 @@ function redrawCharts() { chartData.forEach((cfg, id) => { - if (cfg.type === 'line') drawLineChart(id, cfg.series); + if (cfg.type === 'multi') drawMultiLine(id, cfg.series); + else if (cfg.type === 'line') drawLineChart(id, cfg.series); else if (cfg.type === 'bar') drawBarChart(id, cfg.series); else drawDonut(id, cfg.series, cfg.legendEl); });