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
+44 -24
View File
@@ -1,8 +1,10 @@
export const dynamic = 'force-dynamic'
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, capaActions } from '@/lib/db/schema'
import { eq, gte, lt, and } from 'drizzle-orm'
import { StatCard } from '@/components/dashboard/stat-card'
import { RiskFlagsPanel } from '@/components/dashboard/risk-flags-panel'
@@ -11,34 +13,53 @@ export default async function ManagementPage() {
if (!session) redirect('/login')
if (!['management', 'admin'].includes(session.role)) redirect('/')
const supabase = await createClient()
const now = new Date()
const thisMonthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString()
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString()
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString()
const [
{ data: allIncidents },
{ data: thisMonthIncidents },
{ data: lastMonthIncidents },
{ data: recentIncidents },
{ data: overdueCapas },
] = await Promise.all([
supabase.from('incidents').select('id, severity, status, incident_type, medical_status, sites (name)'),
supabase.from('incidents').select('id').gte('reported_at', thisMonthStart),
supabase.from('incidents').select('id').gte('reported_at', lastMonthStart).lt('reported_at', thisMonthStart),
supabase.from('incidents').select('incident_type').gte('reported_at', thirtyDaysAgo),
supabase.from('capa_actions').select('id').eq('status', 'overdue'),
const [allIncidents, thisMonthIncidents, lastMonthIncidents, recentIncidents, overdueCapas] = await Promise.all([
asAdmin(db =>
db.select({
id: incidents.id,
severity: incidents.severity,
status: incidents.status,
incidentType: incidents.incidentType,
medicalStatus: incidents.medicalStatus,
siteName: sites.name,
})
.from(incidents)
.leftJoin(sites, eq(incidents.siteId, sites.id))
),
asAdmin(db =>
db.select({ id: incidents.id })
.from(incidents)
.where(gte(incidents.reportedAt, new Date(thisMonthStart)))
),
asAdmin(db =>
db.select({ id: incidents.id })
.from(incidents)
.where(and(gte(incidents.reportedAt, new Date(lastMonthStart)), lt(incidents.reportedAt, new Date(thisMonthStart))))
),
asAdmin(db =>
db.select({ incidentType: incidents.incidentType })
.from(incidents)
.where(gte(incidents.reportedAt, new Date(thirtyDaysAgo)))
),
asAdmin(db =>
db.select({ id: capaActions.id })
.from(capaActions)
.where(eq(capaActions.status, 'overdue'))
),
])
const rows = allIncidents ?? []
const totalThisMonth = thisMonthIncidents?.length ?? 0
const totalLastMonth = lastMonthIncidents?.length ?? 0
const rows = allIncidents
const totalThisMonth = thisMonthIncidents.length
const totalLastMonth = lastMonthIncidents.length
const monthDelta = totalThisMonth - totalLastMonth
const ltiCount = rows.filter(r => r.medical_status === 'lti').length
const overdueCount = overdueCapas?.length ?? 0
const ltiCount = rows.filter(r => r.medicalStatus === 'lti').length
const overdueCount = overdueCapas.length
// Severity distribution
const severityDist: Record<number, number> = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }
@@ -50,14 +71,13 @@ export default async function ManagementPage() {
const severityMax = Math.max(...Object.values(severityDist), 1)
// Leading vs 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
const leadingCount = recentIncidents.filter(r => ['hazard', 'near_miss'].includes(r.incidentType)).length
const laggingCount = recentIncidents.filter(r => r.incidentType === 'injury').length
// Site comparison
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)