- Live Tracking: Leaflet map (CDN) showing active truck positions,
auto-refresh every 10s, sidebar list with speed + last update.
- Finance / Payments: filterable list of payments with mark-paid
action that runs admin fulfillment.
- Settings: notification-preferences form bound to /me/notification-
preferences.
Backend:
- New admin payments index endpoint
GET /admin/payments + POST /admin/payments/{uuid}/mark-paid (moved
out of /admin/live/* for clarity).
Sidebar groups now reflect what's live: Operations (Trips, Live
Tracking), Planning (Routes, Teams, Trucks, DOPs, Dumpsites), People
(Users, Households, Partner Stores), QR, Areas, Finance, Reports,
Settings. Trip Calendar + Incidents remain "Soon" — backends ready,
UI work pending. Dashboard backend-status copy updated to 13/13
modules / 171 tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
131 lines
6.4 KiB
PHP
131 lines
6.4 KiB
PHP
@extends('admin.layouts.app', ['pageTitle' => 'Dashboard'])
|
|
|
|
@section('page')
|
|
<div class="mx-auto max-w-7xl">
|
|
<header class="mb-8">
|
|
<h2 class="text-2xl font-semibold tracking-tight text-neutral-900">
|
|
Welcome <span id="welcome-first" class="text-verde-700"></span>
|
|
</h2>
|
|
<p class="mt-1 text-sm text-neutral-500">Operations overview for your service area.</p>
|
|
</header>
|
|
|
|
{{-- Stat cards --}}
|
|
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
|
@foreach ([
|
|
['key' => 'pending_households', 'label' => 'Pending households', 'href' => '/admin/households'],
|
|
['key' => 'verified_households', 'label' => 'Verified households', 'href' => '/admin/households'],
|
|
['key' => 'service_areas', 'label' => 'Service areas', 'href' => '/admin/service-areas'],
|
|
['key' => 'drop_off_points', 'label' => 'Drop-off points', 'href' => '/admin/drop-off-points'],
|
|
['key' => 'dumpsites', 'label' => 'Dumpsites', 'href' => '/admin/dumpsites'],
|
|
['key' => 'qr_batches', 'label' => 'QR batches', 'href' => '/admin/qr-batches'],
|
|
] as $s)
|
|
<a href="{{ $s['href'] }}" class="card p-5 transition hover:border-verde-300 hover:shadow-sm">
|
|
<div class="text-xs font-medium uppercase tracking-wider text-neutral-500">{{ $s['label'] }}</div>
|
|
<div class="mt-2 text-2xl font-semibold tracking-tight text-neutral-900" data-stat="{{ $s['key'] }}">—</div>
|
|
</a>
|
|
@endforeach
|
|
</div>
|
|
|
|
{{-- Recent activity --}}
|
|
<div class="mt-8 grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
<div class="card-padded lg:col-span-2">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-neutral-900">Recent verification queue</h3>
|
|
<a href="/admin/households?verification_status=pending" class="text-xs font-medium text-verde-700 hover:text-verde-800">View all →</a>
|
|
</div>
|
|
<div class="mt-4 divide-y divide-neutral-100" id="recent-households">
|
|
<div class="py-4 text-sm text-neutral-400">Loading…</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-padded">
|
|
<h3 class="text-sm font-semibold text-neutral-900">System status</h3>
|
|
<ul class="mt-4 space-y-2 text-sm text-neutral-700">
|
|
<li class="flex items-center gap-2">
|
|
<span class="h-1.5 w-1.5 rounded-full bg-verde-500"></span>
|
|
API <code class="ml-auto font-mono text-xs text-neutral-500" id="api-status">checking…</code>
|
|
</li>
|
|
<li class="flex items-center gap-2">
|
|
<span class="h-1.5 w-1.5 rounded-full bg-verde-500"></span>
|
|
Backend modules <span class="ml-auto text-xs text-neutral-500">13 / 13</span>
|
|
</li>
|
|
<li class="flex items-center gap-2">
|
|
<span class="h-1.5 w-1.5 rounded-full bg-verde-500"></span>
|
|
Tests passing <span class="ml-auto text-xs text-neutral-500">171</span>
|
|
</li>
|
|
</ul>
|
|
<div class="mt-5 border-t border-neutral-100 pt-4 text-xs text-neutral-500">
|
|
Reverb live broadcast and FCM push are deferred — polling drives the live map for now.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
const user = window.Verde.getUser();
|
|
document.getElementById('welcome-first').textContent = (user?.first_name ?? 'admin') + '.';
|
|
|
|
function setStat(key, value) {
|
|
const el = document.querySelector(`[data-stat="${key}"]`);
|
|
if (el) el.textContent = value;
|
|
}
|
|
|
|
async function loadStats() {
|
|
// Pending households
|
|
const ph = await window.Verde.apiFetch('/api/v1/admin/households?verification_status=pending&per_page=1');
|
|
if (ph.ok) setStat('pending_households', ph.body.meta?.total ?? 0);
|
|
|
|
const vh = await window.Verde.apiFetch('/api/v1/admin/households?verification_status=approved&per_page=1');
|
|
if (vh.ok) setStat('verified_households', vh.body.meta?.total ?? 0);
|
|
|
|
const sa = await window.Verde.apiFetch('/api/v1/service-areas');
|
|
if (sa.ok) setStat('service_areas', (sa.body.data ?? []).length);
|
|
|
|
const dop = await window.Verde.apiFetch('/api/v1/admin/drop-off-points?per_page=1');
|
|
if (dop.ok) setStat('drop_off_points', dop.body.meta?.total ?? 0);
|
|
|
|
const ds = await window.Verde.apiFetch('/api/v1/admin/dumpsites?per_page=1');
|
|
if (ds.ok) setStat('dumpsites', ds.body.meta?.total ?? 0);
|
|
|
|
const qb = await window.Verde.apiFetch('/api/v1/admin/qr-batches?per_page=1');
|
|
if (qb.ok) setStat('qr_batches', qb.body.meta?.total ?? 0);
|
|
}
|
|
|
|
async function loadRecentHouseholds() {
|
|
const res = await window.Verde.apiFetch('/api/v1/admin/households?verification_status=pending&per_page=5');
|
|
const list = document.getElementById('recent-households');
|
|
if (!res.ok) {
|
|
list.innerHTML = '<div class="py-4 text-sm text-red-500">Failed to load.</div>';
|
|
return;
|
|
}
|
|
const items = res.body.data ?? [];
|
|
if (items.length === 0) {
|
|
list.innerHTML = '<div class="py-4 text-sm text-neutral-400">No pending households. Nice.</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = items.map(h => `
|
|
<a href="/admin/households" class="block py-3 transition hover:bg-neutral-50 -mx-2 px-2 rounded">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="text-sm font-medium text-neutral-900">${window.Verde.escapeHtml(h.head?.full_name ?? '—')}</div>
|
|
<div class="text-xs text-neutral-500">${window.Verde.escapeHtml(h.address_line)}</div>
|
|
</div>
|
|
<span class="badge ${h.proof_of_residency_uploaded ? 'badge-pending' : 'badge-inactive'}">
|
|
${h.proof_of_residency_uploaded ? 'Has proof' : 'No proof'}
|
|
</span>
|
|
</div>
|
|
</a>
|
|
`).join('');
|
|
}
|
|
|
|
async function checkApi() {
|
|
const r = await fetch('/api/v1/health').then(x => x.json()).catch(() => null);
|
|
document.getElementById('api-status').textContent = r?.success ? 'ok' : 'error';
|
|
}
|
|
|
|
loadStats();
|
|
loadRecentHouseholds();
|
|
checkApi();
|
|
</script>
|
|
@endsection
|