From ceb5c7994aa1e5d877dfb11c64e7e41781e4e610 Mon Sep 17 00:00:00 2001 From: weeihan Date: Sat, 11 Jul 2026 18:13:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20supervisor=20dashboard=20=E2=80=94=20si?= =?UTF-8?q?te-scoped=20open=20incidents,=20overdue=20CAPAs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(protected)/supervisor/page.tsx | 156 ++++++++++++++++++++++++++-- 1 file changed, 148 insertions(+), 8 deletions(-) diff --git a/app/(protected)/supervisor/page.tsx b/app/(protected)/supervisor/page.tsx index 21f0489..af47a01 100644 --- a/app/(protected)/supervisor/page.tsx +++ b/app/(protected)/supervisor/page.tsx @@ -3,25 +3,165 @@ export const dynamic = 'force-dynamic' import Link from 'next/link' import { createClient } from '@/lib/supabase/server' import { redirect } from 'next/navigation' +import { StatCard } from '@/components/dashboard/stat-card' + +const STATUS_LABELS: Record = { + reported: 'Reported', + triaged: 'Triaged', + investigating: 'Investigating', + capa_pending: 'CAPA Pending', + verification: 'Verification', + closed: 'Closed', +} + +const STATUS_COLORS: Record = { + reported: 'bg-gray-100 text-gray-700', + triaged: 'bg-yellow-100 text-yellow-800', + investigating: 'bg-blue-100 text-blue-700', + capa_pending: 'bg-orange-100 text-orange-700', + verification: 'bg-purple-100 text-purple-700', + closed: 'bg-green-100 text-green-700', +} export default async function SupervisorPage() { const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) redirect('/login') - const { data: profile } = await supabase.from('users').select('name').eq('id', user.id).single() + const { data: profile } = await supabase + .from('users') + .select('name, site_id, role') + .eq('id', user.id) + .single() + if (!profile || !['supervisor', 'admin'].includes(profile.role)) redirect('/') + if (!profile.site_id) { + return ( +
+

Supervisor Portal

+

No site assigned — contact your administrator.

+
+ ) + } + + const { data: siteRow } = await supabase.from('sites').select('name').eq('id', profile.site_id).single() + const siteName = (siteRow as unknown as { name: string } | null)?.name ?? 'Your Site' + + const [ + { data: openIncidents }, + { data: closedIncidents }, + { data: overdueCapas }, + { data: allCapas }, + ] = await Promise.all([ + supabase + .from('incidents') + .select('id, reference_no, incident_type, status, reported_at') + .eq('site_id', profile.site_id) + .neq('status', 'closed') + .order('reported_at', { ascending: false }) + .limit(10), + supabase + .from('incidents') + .select('id') + .eq('site_id', profile.site_id) + .eq('status', 'closed'), + supabase + .from('capa_actions') + .select('id, description, due_date, users (name)') + .eq('status', 'overdue') + .in( + 'incident_id', + (await supabase.from('incidents').select('id').eq('site_id', profile.site_id)).data?.map(r => r.id) ?? [] + ), + supabase + .from('capa_actions') + .select('id, status') + .in( + 'incident_id', + (await supabase.from('incidents').select('id').eq('site_id', profile.site_id)).data?.map(r => r.id) ?? [] + ), + ]) + + const openCount = openIncidents?.length ?? 0 + const closedCount = closedIncidents?.length ?? 0 + const overdueCount = overdueCapas?.length ?? 0 + + const capaRows = allCapas ?? [] + const capaOpenCount = capaRows.filter(c => ['open', 'in_progress'].includes(c.status)).length + const capaVerifiedCount = capaRows.filter(c => c.status === 'verified').length + + const TYPE_LABELS: Record = { + injury: 'Injury', near_miss: 'Near Miss', hazard: 'Hazard', + asset_damage: 'Asset Damage', environmental: 'Environmental', security: 'Security', fire: 'Fire', + } return (
-

Supervisor Portal

-

Welcome, {profile?.name ?? user.email}

-
- -
📋
-
Incident Inbox
-
View site incidents
+
+
+

Supervisor Portal

+

{siteName}

+
+ + All incidents →
+ + {/* Stats */} +
+ 0 ? 'yellow' : 'green'} /> + + 0 ? 'yellow' : 'green'} /> + 0 ? 'red' : 'green'} /> +
+ + {/* Open incidents */} +
+

Open Incidents

+ {(openIncidents ?? []).length === 0 ? ( +

No open incidents.

+ ) : ( +
+ {(openIncidents ?? []).map(inc => ( + +
+

{inc.reference_no ?? inc.id.slice(0, 8)}

+

{TYPE_LABELS[inc.incident_type] ?? inc.incident_type}

+
+ + {STATUS_LABELS[inc.status] ?? inc.status} + + + ))} +
+ )} +
+ + {/* Overdue CAPAs */} + {overdueCount > 0 && ( +
+

Overdue CAPAs

+
+ {(overdueCapas ?? []).map(capa => ( +
+
+

{capa.description}

+

+ Owner: {(capa.users as unknown as { name: string } | null)?.name ?? 'Unassigned'} +

+
+ + Due {new Date(capa.due_date as string).toLocaleDateString('en-MY')} + +
+ ))} +
+

CAPA on-time rate: {capaRows.length > 0 ? Math.round((capaVerifiedCount / capaRows.length) * 100) : 'N/A'}%

+
+ )}
) }