feat(db): phase 4 group 6 — server component pages to Drizzle

Converts all 18 server component page files from Supabase client queries
to Drizzle ORM using asAdmin. Adds getSession() + redirect to the three
pages (hse/incidents, hse/incidents/[id], hse/dashboard) that lacked it.
Maps snake_case component prop shapes explicitly where required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 17:29:22 +08:00
co-authored by Claude Sonnet 4.6
parent c2db693d9f
commit f591c0be18
18 changed files with 932 additions and 450 deletions
+85 -52
View File
@@ -1,7 +1,11 @@
export const dynamic = 'force-dynamic'
import Link from 'next/link'
import { createClient } from '@/lib/supabase/server'
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'
@@ -22,7 +26,9 @@ export default async function HseDashboardPage({
}: {
searchParams: Promise<{ tab?: string }>
}) {
const supabase = await createClient()
const session = await getSession()
if (!session) redirect('/login')
const params = await searchParams
const tab = params.tab ?? 'overview'
@@ -34,68 +40,89 @@ export default async function HseDashboardPage({
const twelveMonthsAgo = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 11, 1))
const [
{ data: incidents },
{ data: recentIncidents },
{ data: zoneIncidents },
{ data: completedCapas },
{ data: doshPendingRows },
{ data: yearIncidents },
{ data: investigations },
allIncidents,
recentIncidents,
zoneIncidents,
completedCapas,
doshPendingRows,
yearIncidents,
investigationRows,
] = 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'),
supabase
.from('incidents')
.select('reported_at, incident_type')
.gte('reported_at', twelveMonthsAgo.toISOString()),
supabase
.from('investigations')
.select('root_cause_summary')
.not('root_cause_summary', 'is', null),
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 = incidents ?? []
const rows = allIncidents
const total = rows.length
const closed = rows.filter(r => r.status === 'closed').length
const open = total - closed
const by_type: Record<string, number> = {}
for (const r of rows) {
by_type[r.incident_type] = (by_type[r.incident_type] ?? 0) + 1
by_type[r.incidentType] = (by_type[r.incidentType] ?? 0) + 1
}
const siteMap: Record<string, number> = {}
for (const r of rows) {
const name = (r.sites as unknown as { name: string } | null)?.name ?? 'Unknown'
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 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
const leadingCount = recentIncidents.filter(r => ['hazard', 'near_miss'].includes(r.incidentType)).length
const laggingCount = recentIncidents.filter(r => r.incidentType === 'injury').length
const zoneMap: Record<string, number> = {}
for (const r of zoneIncidents ?? []) {
const name = (r.zones as unknown as { name: string } | null)?.name ?? 'Unknown'
for (const r of zoneIncidents) {
const name = r.zoneName ?? 'Unknown'
zoneMap[name] = (zoneMap[name] ?? 0) + 1
}
const by_zone = Object.entries(zoneMap)
@@ -104,13 +131,13 @@ export default async function HseDashboardPage({
.slice(0, 10)
const zoneMax = by_zone[0]?.count ?? 1
const capas = completedCapas ?? []
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)
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
})
@@ -118,11 +145,17 @@ export default async function HseDashboardPage({
? Math.round((onTime.length / capas.length) * 100)
: null
const doshPendingCount = doshPendingRows?.length ?? 0
const doshPendingCount = doshPendingRows.length
const monthly = bucketIncidentsByMonth(yearIncidents ?? [], 12, now)
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(investigations ?? [])
const rootCauses = topRootCauses(
investigationRows.map(r => ({ root_cause_summary: r.rootCauseSummary }))
)
return (
<main className="max-w-4xl mx-auto px-4 py-6">