The soft reset to 77073ba inadvertently staged deletions of all phase 2
and 3 artifacts. This commit restores them from their source commits so
subsequent task commits build on the complete prior-phase foundation:
- components/mobile/{BottomNav,HeaderBar,KpiCardMobile,MoreDrawer,NeedsAttentionStrip,WorkerStatusRow}
- app/mobile/layout.tsx, dashboard/page.tsx, analyzer/page.tsx
- app/api/mobile/dashboard/route.ts
- All .planning/** files from phases 01-04
- CLAUDE.md, app/layout.tsx, app/styles/brand.css, public/manifest.json
118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
'use client';
|
||
|
||
/* /mobile/dashboard — phase 03 (DASH-01..04).
|
||
*
|
||
* Three sections, top-to-bottom:
|
||
* 1. 2×2 KPI grid (DASH-01)
|
||
* 2. Needs Attention (DASH-02)
|
||
* 3. Worker/backup row (DASH-03)
|
||
*
|
||
* No charts on phone widths (DASH-04). Header + bottom nav are provided
|
||
* by app/mobile/layout.tsx; this page only renders the H1 and body. */
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import { RefreshCw } from 'lucide-react';
|
||
import { KpiCardMobile } from '@/components/mobile/KpiCardMobile';
|
||
import { NeedsAttentionStrip } from '@/components/mobile/NeedsAttentionStrip';
|
||
import { WorkerStatusRow } from '@/components/mobile/WorkerStatusRow';
|
||
import type { MobileDashboardResponse } from '@/app/api/mobile/dashboard/route';
|
||
|
||
export default function MobileDashboard() {
|
||
const [data, setData] = useState<MobileDashboardResponse | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
async function load() {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const r = await fetch('/api/mobile/dashboard');
|
||
if (!r.ok) {
|
||
const body = (await r.json().catch(() => ({}))) as { error?: string; message?: string };
|
||
throw new Error(body.message ?? body.error ?? `HTTP ${r.status}`);
|
||
}
|
||
setData((await r.json()) as MobileDashboardResponse);
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : 'Unknown error');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
useEffect(() => { void load(); }, []);
|
||
|
||
return (
|
||
<div className="p-4 space-y-5">
|
||
<div className="flex items-center justify-between">
|
||
<h1 className="text-xl font-bold">Dashboard</h1>
|
||
<button
|
||
type="button"
|
||
onClick={load}
|
||
disabled={loading}
|
||
aria-label="Refresh dashboard"
|
||
className="p-2 rounded-full hover:bg-accent disabled:opacity-40"
|
||
>
|
||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||
</button>
|
||
</div>
|
||
|
||
{error && !loading && (
|
||
<div className="rounded-xl border border-destructive/50 bg-destructive/5 p-4">
|
||
<p className="text-sm font-medium text-destructive">Failed to load</p>
|
||
<p className="text-xs text-muted-foreground mt-1">{error}</p>
|
||
<button
|
||
type="button"
|
||
onClick={load}
|
||
className="mt-3 text-xs font-medium text-primary hover:underline"
|
||
>
|
||
Retry
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{loading && !data && (
|
||
<div className="flex items-center justify-center h-64">
|
||
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
|
||
</div>
|
||
)}
|
||
|
||
{data && (
|
||
<>
|
||
{/* DASH-01: 2×2 KPI grid */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
{data.kpis.map(kpi => (
|
||
<KpiCardMobile
|
||
key={kpi.id}
|
||
label={kpi.label}
|
||
value={kpi.value}
|
||
caption={kpi.caption}
|
||
tone={kpi.tone ?? 'default'}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* DASH-02: Needs Attention horizontal strip */}
|
||
<NeedsAttentionStrip
|
||
items={data.needsAttention.map(a => ({
|
||
id: a.id,
|
||
label: a.label,
|
||
count: a.count,
|
||
href: a.href,
|
||
}))}
|
||
/>
|
||
|
||
{/* DASH-03: Worker/backup status row */}
|
||
<WorkerStatusRow
|
||
entries={data.workers.map(w => ({
|
||
id: w.id,
|
||
label: w.label,
|
||
value: w.value,
|
||
status: w.status,
|
||
href: w.href,
|
||
}))}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|