Files
Verde-Web/resources/views/admin/dashboard.blade.php
admin c2115b767e feat(web): replace remaining "Soon" stubs with real pages
- 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>
2026-05-01 14:07:59 +08:00

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