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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user