export const dynamic = 'force-dynamic' import Link from 'next/link' import { createClient } from '@/lib/supabase/server' import { StatCard } from '@/components/dashboard/stat-card' const TYPE_LABELS: Record = { injury: 'Injury', near_miss: 'Near Miss', hazard: 'Hazard', asset_damage: 'Asset Damage', environmental: 'Environmental', security: 'Security', fire: 'Fire', } export default async function HseDashboardPage() { const supabase = await createClient() const now = new Date() const thirtyDaysAgo = new Date(now) thirtyDaysAgo.setDate(now.getDate() - 30) const ninetyDaysAgo = new Date(now) ninetyDaysAgo.setDate(now.getDate() - 90) const [ { data: incidents }, { data: recentIncidents }, { data: zoneIncidents }, { data: completedCapas }, { data: doshPendingRows }, ] = await Promise.all([ supabase.from('incidents').select('id, status, incident_type, sites (name)'), supabase .from('incidents') .select('incident_type') .gte('reported_at', thirtyDaysAgo.toISOString()), supabase .from('incidents') .select('zones (name)') .gte('reported_at', ninetyDaysAgo.toISOString()) .not('zone_id', 'is', null), supabase .from('capa_actions') .select('due_date, completed_at, verified_at') .not('completed_at', 'is', null), supabase .from('dosh_reports') .select('id') .eq('status', 'pending'), ]) // --- Existing metrics --- const rows = incidents ?? [] const total = rows.length const closed = rows.filter(r => r.status === 'closed').length const open = total - closed const by_type: Record = {} for (const r of rows) { by_type[r.incident_type] = (by_type[r.incident_type] ?? 0) + 1 } const siteMap: Record = {} for (const r of rows) { const name = (r.sites as unknown as { name: string } | null)?.name ?? 'Unknown' siteMap[name] = (siteMap[name] ?? 0) + 1 } const by_site = Object.entries(siteMap) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count) // --- Leading / lagging (last 30 days) --- const recent = recentIncidents ?? [] const leadingCount = recent.filter(r => ['hazard', 'near_miss'].includes(r.incident_type)).length const laggingCount = recent.filter(r => r.incident_type === 'injury').length // --- Zone heatmap (last 90 days) --- const zoneMap: Record = {} for (const r of zoneIncidents ?? []) { const name = (r.zones as unknown as { name: string } | null)?.name ?? 'Unknown' zoneMap[name] = (zoneMap[name] ?? 0) + 1 } const by_zone = Object.entries(zoneMap) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count) .slice(0, 10) const zoneMax = by_zone[0]?.count ?? 1 // --- CAPA on-time rate --- const capas = completedCapas ?? [] const onTime = capas.filter(c => { const due = new Date(c.due_date) const done = c.verified_at ? new Date(c.verified_at as string) : c.completed_at ? new Date(c.completed_at as string) : null return done !== null && done <= due }) const capaOnTimeRate = capas.length > 0 ? Math.round((onTime.length / capas.length) * 100) : null // --- DOSH pending filings --- const doshPendingCount = doshPendingRows?.length ?? 0 return (

Dashboard

Export CSV View all incidents →
{/* Summary stats */}
0 ? 'yellow' : 'green'} sub="filings outstanding" />
{/* Leading vs lagging — last 30 days */}

Leading vs Lagging — Last 30 Days

Leading: hazard + near-miss reports (predict risk) · Lagging: injuries (past harm)

{leadingCount}

Leading (Hazards + Near Misses)

{laggingCount}

Lagging (Injuries)

{capaOnTimeRate !== null && (

{capaOnTimeRate}%

CAPA On-Time Rate

)}
{/* Zone heatmap — last 90 days */} {by_zone.length > 0 && (

Zone Incident Heatmap — Last 90 Days

{by_zone.map(({ name, count }) => (
{name}
{count}
))}
)} {/* Incident type breakdown */}

By Incident Type

{Object.entries(by_type).sort((a, b) => b[1] - a[1]).map(([type, count]) => (
{TYPE_LABELS[type] ?? type}
0 ? `${(count / total) * 100}%` : '0%' }} />
{count}
))} {Object.keys(by_type).length === 0 && (

No incidents yet

)}
{/* By site */}

By Site

{by_site.map(({ name, count }) => (
{name} {count}
))} {by_site.length === 0 &&

No data

}
) }