From 52562503c01893e80aecce2c825d3955ec8a70ff Mon Sep 17 00:00:00 2001 From: lorentz Date: Sun, 3 May 2026 16:53:35 -0400 Subject: [PATCH] feat(03-02): replace mobile dashboard page with 3-section layout - Drop legacy recharts/chart sections, priority breakdown, SLA bars, queue list, recent activity - Add 2x2 KPI grid (DASH-01), Needs Attention horizontal strip (DASH-02), Worker/backup status row (DASH-03) - Wire KpiCardMobile, NeedsAttentionStrip, WorkerStatusRow against /api/mobile/dashboard - Inline error state with Retry button, spinner while loading, refresh button in H1 row - Zero recharts imports (DASH-04); 118 lines --- app/mobile/dashboard/page.tsx | 255 ++++++++++++---------------------- 1 file changed, 91 insertions(+), 164 deletions(-) diff --git a/app/mobile/dashboard/page.tsx b/app/mobile/dashboard/page.tsx index ecdaf38..dbef026 100644 --- a/app/mobile/dashboard/page.tsx +++ b/app/mobile/dashboard/page.tsx @@ -1,191 +1,118 @@ '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 Link from 'next/link'; -import { RefreshCw, AlertCircle, Clock, CheckCircle2, ChevronRight } from 'lucide-react'; - -interface DashboardData { - open_total: number; - by_status: { status: number; label: string; count: number }[]; - by_queue: { queue_id: number; label: string; count: number }[]; - by_priority: { priority: number; label: string; count: number }[]; - recent: { - id: number; ticket_number: string; title: string; status: number; priority: number; - last_activity_date: string; company_name: string; queue_label: string; assigned_to: string; - }[]; - sla: { response_met: number; response_total: number; resolution_met: number; resolution_total: number }; -} - -const PRIORITY_COLOR: Record = { - 1: 'bg-slate-300', // Standard - 2: 'bg-slate-300', // Medium - 3: 'bg-slate-300', // Standard - 4: 'bg-red-500', // Critical - 6: 'bg-orange-400', // High - 7: 'bg-purple-500', // Security Event - 8: 'bg-yellow-400', // Minor Service - 9: 'bg-orange-500', // Major Service - 10: 'bg-blue-400', // Installation - 11: 'bg-pink-400', // Fast Track -}; -const PRIORITY_TEXT: Record = { - 1: 'text-slate-500', // Standard - 2: 'text-slate-500', // Medium - 3: 'text-slate-500', // Standard - 4: 'text-red-600', // Critical - 6: 'text-orange-500', // High - 7: 'text-purple-600', // Security Event - 8: 'text-yellow-600', // Minor Service - 9: 'text-orange-600', // Major Service - 10: 'text-blue-600', // Installation - 11: 'text-pink-600', // Fast Track -}; - -function pct(n: number, d: number) { - return d === 0 ? 0 : Math.round((n / d) * 100); -} -function relTime(ts: string) { - const diff = Date.now() - new Date(ts).getTime(); - const m = Math.floor(diff / 60000); - if (m < 60) return `${m}m ago`; - const h = Math.floor(m / 60); - if (h < 24) return `${h}h ago`; - return `${Math.floor(h / 24)}d ago`; -} +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(null); + const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const load = async () => { - setLoading(true); setError(null); + async function load() { + setLoading(true); + setError(null); try { const r = await fetch('/api/mobile/dashboard'); - if (!r.ok) throw new Error('Failed to load'); - setData(await r.json()); - } catch (e) { setError(String(e)); } - finally { setLoading(false); } - }; + 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(() => { load(); }, []); - - if (loading) return ( -
- -
- ); - if (error) return ( -
{error}
- ); - if (!data) return null; - - const respPct = pct(data.sla.response_met, data.sla.response_total); - const resPct = pct(data.sla.resolution_met, data.sla.resolution_total); + useEffect(() => { void load(); }, []); return (
- {/* Header */}
-

Ticket Dashboard

-
- {/* Open total */} -
-
- + {error && !loading && ( +
+

Failed to load

+

{error}

+
-
-

{data.open_total}

-

Open tickets

-
-
+ )} - {/* Priority breakdown */} -
-

By Priority

-
- {data.by_priority.map(p => ( -
-
-

{p.count}

-

{p.label}

-
- ))} + {loading && !data && ( +
+
-
+ )} - {/* SLA */} -
-

SLA — last 30 days

-
- {[ - { label: 'First Response (≤1h)', met: data.sla.response_met, total: data.sla.response_total, p: respPct }, - { label: 'Resolution (≤24h)', met: data.sla.resolution_met, total: data.sla.resolution_total, p: resPct }, - ].map(s => ( -
-

= 80 ? 'text-green-600' : s.p >= 60 ? 'text-yellow-600' : 'text-red-600'}`}> - {s.p}% -

-

{s.label}

-

{s.met} / {s.total}

-
-
= 80 ? 'bg-green-500' : s.p >= 60 ? 'bg-yellow-400' : 'bg-red-500'}`} - style={{ width: `${s.p}%` }} /> -
-
- ))} -
-
+ {data && ( + <> + {/* DASH-01: 2×2 KPI grid */} +
+ {data.kpis.map(kpi => ( + + ))} +
- {/* By queue */} -
-

By Queue

-
- {data.by_queue.map(q => ( -
-

{q.label}

-
-
-
-
- {q.count} -
-
- ))} -
-
+ {/* DASH-02: Needs Attention horizontal strip */} + ({ + id: a.id, + label: a.label, + count: a.count, + href: a.href, + }))} + /> - {/* Recent activity */} -
-
-

Recent Activity

- View all -
-
- {data.recent.map(t => ( - -
-
-

{t.title}

-

{t.company_name} · {t.queue_label ?? '—'}

-
-
-
- - {t.last_activity_date ? relTime(t.last_activity_date) : '—'} -
- -
- - ))} -
-
+ {/* DASH-03: Worker/backup status row */} + ({ + id: w.id, + label: w.label, + value: w.value, + status: w.status, + href: w.href, + }))} + /> + + )}
); }