export const dynamic = 'force-dynamic' import Link from 'next/link' import { asAdmin } from '@/lib/db/with-user' import { redirect } from 'next/navigation' import { getSession } from '@/lib/auth/get-session' import { incidents, sites, zones, capaActions, doshReports, investigations } from '@/lib/db/schema' import { eq, gte, isNotNull } from 'drizzle-orm' import { StatCard } from '@/components/dashboard/stat-card' import { bucketIncidentsByMonth, topRootCauses } from '@/lib/dashboard/trends' import { RiskFlagsPanel } from '@/components/dashboard/risk-flags-panel' import { DashboardTabs } from '@/components/dashboard/dashboard-tabs' 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({ searchParams, }: { searchParams: Promise<{ tab?: string }> }) { const session = await getSession() if (!session) redirect('/login') const params = await searchParams const tab = params.tab ?? 'overview' 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 twelveMonthsAgo = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 11, 1)) const [ allIncidents, recentIncidents, zoneIncidents, completedCapas, doshPendingRows, yearIncidents, investigationRows, ] = await Promise.all([ asAdmin(db => db.select({ id: incidents.id, status: incidents.status, incidentType: incidents.incidentType, siteName: sites.name, }) .from(incidents) .leftJoin(sites, eq(incidents.siteId, sites.id)) ), asAdmin(db => db.select({ incidentType: incidents.incidentType }) .from(incidents) .where(gte(incidents.reportedAt, thirtyDaysAgo)) ), asAdmin(db => db.select({ zoneName: zones.name }) .from(incidents) .leftJoin(zones, eq(incidents.zoneId, zones.id)) .where(gte(incidents.reportedAt, ninetyDaysAgo)) ).then(rows => rows.filter(r => r.zoneName !== null)), asAdmin(db => db.select({ dueDate: capaActions.dueDate, completedAt: capaActions.completedAt, verifiedAt: capaActions.verifiedAt, }) .from(capaActions) .where(isNotNull(capaActions.completedAt)) ), asAdmin(db => db.select({ id: doshReports.id }) .from(doshReports) .where(eq(doshReports.status, 'pending')) ), asAdmin(db => db.select({ reportedAt: incidents.reportedAt, incidentType: incidents.incidentType, }) .from(incidents) .where(gte(incidents.reportedAt, twelveMonthsAgo)) ), asAdmin(db => db.select({ rootCauseSummary: investigations.rootCauseSummary }) .from(investigations) .where(isNotNull(investigations.rootCauseSummary)) ), ]) const rows = allIncidents 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.incidentType] = (by_type[r.incidentType] ?? 0) + 1 } const siteMap: Record = {} for (const r of rows) { const name = r.siteName ?? '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) const leadingCount = recentIncidents.filter(r => ['hazard', 'near_miss'].includes(r.incidentType)).length const laggingCount = recentIncidents.filter(r => r.incidentType === 'injury').length const zoneMap: Record = {} for (const r of zoneIncidents) { const name = r.zoneName ?? '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 const capas = completedCapas const onTime = capas.filter(c => { const due = new Date(c.dueDate) const done = c.verifiedAt ? new Date(c.verifiedAt) : c.completedAt ? new Date(c.completedAt) : null return done !== null && done <= due }) const capaOnTimeRate = capas.length > 0 ? Math.round((onTime.length / capas.length) * 100) : null const doshPendingCount = doshPendingRows.length const monthly = bucketIncidentsByMonth( yearIncidents.map(r => ({ reported_at: r.reportedAt.toISOString(), incident_type: r.incidentType })), 12, now ) const monthlyMax = Math.max(1, ...monthly.map(m => m.total)) const rootCauses = topRootCauses( investigationRows.map(r => ({ root_cause_summary: r.rootCauseSummary })) ) return (

Dashboard

Export CSV JKKP 8 All incidents →
{/* Overview tab */} {(tab === 'overview') && ( <> {now.getMonth() === 0 && (

JKKP 8 annual register due: the {now.getFullYear() - 1} register must be submitted to DOSH before 31 January {now.getFullYear()}.{' '} Download {now.getFullYear() - 1} register

)}
0 ? 'yellow' : 'green'} sub="filings outstanding" />

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

)}
)} {/* Trends tab */} {tab === 'trends' && ( <>

Incident Trend — Last 12 Months

Blue: leading (hazard + near miss) · Red: lagging (injury) · Grey: other

{monthly.map(m => { const other = m.total - m.leading - m.lagging return (
{m.label} {m.total || ''}
) })}
{rootCauses.length > 0 && (

Top Root Causes

    {rootCauses.map((rc, i) => (
  1. {i + 1}. {rc.cause} {rc.count}×
  2. ))}
)} )} {/* Zones & Types tab */} {tab === 'zones' && ( <> {by_zone.length > 0 && (

Zone Incident Heatmap — Last 90 Days

{by_zone.map(({ name, count }) => (
{name}
{count}
))}
)}

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.map(({ name, count }) => (
{name} {count}
))} {by_site.length === 0 &&

No data

}
)} {/* AI Insights tab */} {tab === 'ai' && }
) }