Add fleet, invoice, promo and lookup-driven admin modules

Brings the console to 18 modules across 26 navigation entries.

- Fleet performance (best captains, per-captain card charges), invoice
  totals and promo codes as read-only panels.
- Ride monitor: monitorRide.php and getDriverGiftPayment.php answer only for
  a given phone number, so they get an input rather than a panel that would
  render an error on load.
- Status-like values in generated tiles use the same label mapping as the
  tables, so raw values such as cancelled_by_passenger no longer leak into
  the UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-25 02:02:08 +03:00
co-authored by Claude Opus 5
parent db4ca7dd7a
commit 41a06bba0c
+72 -1
View File
@@ -862,6 +862,19 @@
subtitle: 'Geofenced demand density',
panels: [{ title: 'Heatmap', path: '/Admin/geofence/get_heatmap.php' }],
},
{
id: 'fleet', group: 'Quality', icon: 'ph-steering-wheel', title: 'Fleet Performance',
subtitle: 'Top captains, gift eligibility payouts and per-captain card charges',
panels: [
{ title: 'Best captains', path: '/Admin/driver/getBestDriver.php' },
{ title: 'Card charges per captain', path: '/Admin/getVisaForEachDriver.php' },
],
},
{
id: 'invoices', group: 'Finance', icon: 'ph-receipt', title: 'Invoices',
subtitle: 'Invoice totals recorded against admin accounts',
panels: [{ title: 'Invoice totals', path: '/Admin/adminUser/invoice_total.php' }],
},
{
id: 'quality', group: 'Quality', icon: 'ph-prohibit', title: 'Blacklist',
subtitle: 'Blocked captains and passengers',
@@ -872,6 +885,24 @@
subtitle: 'Behaviour and reliability scoring per captain',
panels: [{ title: 'Scorecard', path: '/Admin/v2/quality/driver_scorecard.php' }],
},
{
id: 'monitor', group: 'Realtime & Analytics', icon: 'ph-crosshair', title: 'Ride Monitor',
subtitle: 'Look up the active ride and gift eligibility for a specific phone number',
custom: (host) => renderLookupTools(host, [
{
title: 'Active ride for a phone number',
path: '/Admin/rides/monitorRide.php',
field: 'phone',
placeholder: 'Passenger or captain phone, e.g. 962798583052',
},
{
title: 'Gift payment eligibility',
path: '/Admin/driver/getDriverGiftPayment.php',
field: 'phone',
placeholder: 'Captain phone',
},
]),
},
{
id: 'transit', group: 'Transit', icon: 'ph-bus', title: 'Mawasalati Organisations',
subtitle: 'Registered transit organisations and their pending routes',
@@ -974,6 +1005,46 @@
}));
}
// Endpoints that answer only for a specific record get an input rather than
// an empty panel: they require a parameter, so eagerly calling them would
// just render an error.
function renderLookupTools(host, tools) {
host.innerHTML = tools.map((tool, i) => `
<div class="card">
<div class="card-header"><h3 class="card-title">${esc(tool.title)}</h3></div>
<div class="api-base-row">
<input type="text" class="form-input" data-lookup-input="${i}" placeholder="${esc(tool.placeholder)}">
<button class="btn btn-secondary btn-sm" data-lookup-run="${i}"><i class="ph ph-magnifying-glass"></i> <span>Look up</span></button>
</div>
<div class="panel-body" data-lookup-body="${i}">
<div class="table-msg">Enter a value above to query this endpoint.</div>
</div>
</div>`).join('');
host.querySelectorAll('[data-lookup-run]').forEach((btn) => {
const index = Number(btn.dataset.lookupRun);
const tool = tools[index];
const input = host.querySelector(`[data-lookup-input="${index}"]`);
const body = host.querySelector(`[data-lookup-body="${index}"]`);
const run = async () => {
const value = input.value.trim();
if (!value) return;
body.innerHTML = '<div class="table-msg">Querying…</div>';
try {
const payload = await api(tool.path, { params: { [tool.field]: value } });
renderPayload(body, payload);
} catch (err) {
if (handleApiError(err, 'lookup')) return;
body.innerHTML = `<div class="table-msg is-error">${esc(err.message)}</div>`;
}
};
btn.addEventListener('click', run);
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') run(); });
});
}
// ── Kazan tariff editor ──────────────────────────────────────────────────
// Only these columns are accepted by ride/kazan/update.php; anything else
// sent would be silently dropped, so the form mirrors that list exactly.
@@ -1151,7 +1222,7 @@
grid.className = 'kpi-tiles';
grid.innerHTML = scalars.map(([k, v]) => `
<div class="kpi-tile">
<div class="kpi-tile-value">${esc(formatValue(v, k))}</div>
<div class="kpi-tile-value">${esc(/status|state/i.test(k) ? labelStatus(v) : formatValue(v, k))}</div>
<div class="kpi-tile-label">${esc(humanize(k))}</div>
</div>`).join('');
frag.appendChild(grid);