- {rows.map(inc => {
- const siteName = (inc.sites as unknown as { name: string } | null)?.name
- return (
-
-
-
-
- {inc.reference_no ?? inc.id.slice(0, 8)}
+ {rows.map(inc => (
+
+
+
+
+ {inc.referenceNo ?? inc.id.slice(0, 8)}
+
+
+ {TYPE_LABELS[inc.incidentType] ?? inc.incidentType}
+ {inc.siteName ? ` · ${inc.siteName}` : ''}
+ {inc.severity ? ` · Severity ${inc.severity}` : ''}
+
+ {inc.reportedAt && (
+
+ {new Date(inc.reportedAt).toLocaleDateString('en-MY', {
+ day: 'numeric', month: 'short', year: 'numeric',
+ })}
-
- {TYPE_LABELS[inc.incident_type] ?? inc.incident_type}
- {siteName ? ` · ${siteName}` : ''}
- {inc.severity ? ` · Severity ${inc.severity}` : ''}
-
- {inc.reported_at && (
-
- {new Date(inc.reported_at as string).toLocaleDateString('en-MY', {
- day: 'numeric', month: 'short', year: 'numeric',
- })}
-
- )}
-
-
- {STATUS_LABELS[inc.status] ?? inc.status}
-
+ )}
-
- )
- })}
+
+ {STATUS_LABELS[inc.status] ?? inc.status}
+
+
+
+ ))}
)}
diff --git a/app/(protected)/supervisor/incidents/[id]/page.tsx b/app/(protected)/supervisor/incidents/[id]/page.tsx
index ccab99f..cc44806 100644
--- a/app/(protected)/supervisor/incidents/[id]/page.tsx
+++ b/app/(protected)/supervisor/incidents/[id]/page.tsx
@@ -2,7 +2,10 @@ export const dynamic = 'force-dynamic'
import { notFound } from 'next/navigation'
import Link from 'next/link'
-import { createClient } from '@/lib/supabase/server'
+import { asAdmin } from '@/lib/db/with-user'
+import { incidents, sites, zones, trucks, users, evidenceFiles, investigations } from '@/lib/db/schema'
+import { eq, and } from 'drizzle-orm'
+import { aliasedTable } from 'drizzle-orm'
import { IncidentDetail, type Incident } from '@/components/incidents/incident-detail'
import { InvestigationPanel } from '@/components/incidents/investigation-panel'
@@ -12,44 +15,129 @@ interface Props {
export default async function SupervisorIncidentDetailPage({ params }: Props) {
const { id } = await params
- const supabase = await createClient()
- const { data: incident, error } = await supabase
- .from('incidents')
- .select(`
- id, reference_no, incident_type, description, severity, status,
- injury_involved, asset_involved, medical_status, lost_days,
- reported_at, closed_at,
- sites (id, name),
- zones (id, name),
- reporter:users!reported_by (id, name, email),
- evidence_files (id, stage, file_url, file_type, uploaded_at)
- `)
- .eq('id', id)
- .eq('evidence_files.deleted', false)
- .single()
+ const reporterAlias = aliasedTable(users, 'reporter')
+ const investigatorAlias = aliasedTable(users, 'investigator')
- if (error || !incident) notFound()
+ const [incident] = await asAdmin(db =>
+ db.select({
+ id: incidents.id,
+ referenceNo: incidents.referenceNo,
+ incidentType: incidents.incidentType,
+ description: incidents.description,
+ severity: incidents.severity,
+ status: incidents.status,
+ injuryInvolved: incidents.injuryInvolved,
+ assetInvolved: incidents.assetInvolved,
+ medicalStatus: incidents.medicalStatus,
+ lostDays: incidents.lostDays,
+ reportedAt: incidents.reportedAt,
+ closedAt: incidents.closedAt,
+ siteId: sites.id,
+ siteName: sites.name,
+ zoneId: zones.id,
+ zoneName: zones.name,
+ truckId: trucks.id,
+ truckNo: trucks.truckNo,
+ truckCarrier: trucks.carrier,
+ reporterId: reporterAlias.id,
+ reporterName: reporterAlias.name,
+ reporterEmail: reporterAlias.email,
+ })
+ .from(incidents)
+ .leftJoin(sites, eq(incidents.siteId, sites.id))
+ .leftJoin(zones, eq(incidents.zoneId, zones.id))
+ .leftJoin(trucks, eq(incidents.truckId, trucks.id))
+ .leftJoin(reporterAlias, eq(incidents.reportedBy, reporterAlias.id))
+ .where(eq(incidents.id, id))
+ .limit(1)
+ )
- const { data: investigation } = await supabase
- .from('investigations')
- .select(`
- id, method, five_why_steps, fishbone_categories,
- findings_text, root_cause_summary, alcohol_test_result, urine_test_result,
- witness_statement_refs, completed_at,
- investigator:users!investigator_id (name, email)
- `)
- .eq('incident_id', id)
- .maybeSingle()
+ if (!incident) notFound()
+
+ const evidenceRows = await asAdmin(db =>
+ db.select({
+ id: evidenceFiles.id,
+ stage: evidenceFiles.stage,
+ fileUrl: evidenceFiles.fileUrl,
+ fileType: evidenceFiles.fileType,
+ uploadedAt: evidenceFiles.uploadedAt,
+ })
+ .from(evidenceFiles)
+ .where(and(eq(evidenceFiles.incidentId, id), eq(evidenceFiles.deleted, false)))
+ )
+
+ const [inv] = await asAdmin(db =>
+ db.select({
+ id: investigations.id,
+ method: investigations.method,
+ fiveWhySteps: investigations.fiveWhySteps,
+ fishboneCategories: investigations.fishboneCategories,
+ findingsText: investigations.findingsText,
+ rootCauseSummary: investigations.rootCauseSummary,
+ alcoholTestResult: investigations.alcoholTestResult,
+ urineTestResult: investigations.urineTestResult,
+ witnessStatementRefs: investigations.witnessStatementRefs,
+ completedAt: investigations.completedAt,
+ investigatorName: investigatorAlias.name,
+ investigatorEmail: investigatorAlias.email,
+ })
+ .from(investigations)
+ .leftJoin(investigatorAlias, eq(investigations.investigatorId, investigatorAlias.id))
+ .where(eq(investigations.incidentId, id))
+ .limit(1)
+ )
+
+ const incidentProp: Incident = {
+ id: incident.id,
+ reference_no: incident.referenceNo,
+ incident_type: incident.incidentType,
+ description: incident.description,
+ status: incident.status,
+ severity: incident.severity,
+ injury_involved: incident.injuryInvolved,
+ asset_involved: incident.assetInvolved,
+ medical_status: incident.medicalStatus,
+ lost_days: incident.lostDays,
+ reported_at: incident.reportedAt.toISOString(),
+ closed_at: incident.closedAt?.toISOString() ?? null,
+ sites: incident.siteId ? { id: incident.siteId, name: incident.siteName! } : null,
+ zones: incident.zoneId ? { id: incident.zoneId, name: incident.zoneName! } : null,
+ trucks: incident.truckId ? { id: incident.truckId, truck_no: incident.truckNo!, carrier: incident.truckCarrier ?? null } : null,
+ reporter: incident.reporterId ? { id: incident.reporterId, name: incident.reporterName!, email: incident.reporterEmail! } : null,
+ evidence_files: evidenceRows.map(e => ({
+ id: e.id,
+ stage: e.stage,
+ file_url: e.fileUrl,
+ file_type: e.fileType,
+ uploaded_at: e.uploadedAt.toISOString(),
+ })),
+ }
+
+ const investigationProp = inv ? {
+ id: inv.id,
+ method: inv.method,
+ five_why_steps: inv.fiveWhySteps as { why: string; answer: string }[] | null,
+ fishbone_categories: inv.fishboneCategories as Record
| null,
+ findings_text: inv.findingsText,
+ root_cause_summary: inv.rootCauseSummary,
+ alcohol_test_result: inv.alcoholTestResult,
+ urine_test_result: inv.urineTestResult,
+ witness_statement_refs: inv.witnessStatementRefs ?? [],
+ completed_at: inv.completedAt?.toISOString() ?? null,
+ investigator: inv.investigatorEmail
+ ? { name: inv.investigatorName ?? null, email: inv.investigatorEmail }
+ : null,
+ } : null
return (
← Back to inbox
-
- {investigation && (
-
+
+ {investigationProp && (
+
)}
)
diff --git a/app/(protected)/supervisor/incidents/page.tsx b/app/(protected)/supervisor/incidents/page.tsx
index 2bcdb81..37a161f 100644
--- a/app/(protected)/supervisor/incidents/page.tsx
+++ b/app/(protected)/supervisor/incidents/page.tsx
@@ -1,6 +1,11 @@
export const dynamic = 'force-dynamic'
-import { createClient } from '@/lib/supabase/server'
+import { redirect } from 'next/navigation'
+import { asAdmin } from '@/lib/db/with-user'
+import { getSession } from '@/lib/auth/get-session'
+import { incidents, sites, zones, trucks, users } from '@/lib/db/schema'
+import { eq, and, or, ilike, desc, asc, sql } from 'drizzle-orm'
+import { aliasedTable } from 'drizzle-orm'
import { IncidentList, type Incident } from '@/components/incidents/incident-list'
import { Pagination } from '@/components/incidents/pagination'
import { IncidentFilters, type SiteOption, type TruckOption } from '@/components/incidents/incident-filters'
@@ -12,48 +17,90 @@ export default async function SupervisorInboxPage({
}: {
searchParams: Promise<{ page?: string; q?: string; status?: string; type?: string; site_id?: string; truck_id?: string }>
}) {
- const supabase = await createClient()
+ const session = await getSession()
+ if (!session) redirect('/login')
+
const params = await searchParams
const page = Math.max(1, Number.parseInt(params.page ?? '1', 10) || 1)
const from = (page - 1) * PAGE_SIZE
- let query = supabase
- .from('incidents')
- .select(`
- id, reference_no, incident_type, status, severity, reported_at,
- sites (name),
- zones (name),
- trucks (truck_no),
- reporter:users!reported_by (name)
- `, { count: 'exact' })
- .order('reported_at', { ascending: false })
+ const reporterAlias = aliasedTable(users, 'reporter')
- if (params.q) {
- query = query.or(`reference_no.ilike.%${params.q}%,description.ilike.%${params.q}%`)
- }
- if (params.status) query = query.eq('status', params.status)
- if (params.type) query = query.eq('incident_type', params.type)
- if (params.site_id) query = query.eq('site_id', params.site_id)
- if (params.truck_id) query = query.eq('truck_id', params.truck_id)
+ const conditions = []
+ if (params.q) conditions.push(or(ilike(incidents.referenceNo, `%${params.q}%`), ilike(incidents.description, `%${params.q}%`)))
+ if (params.status) conditions.push(eq(incidents.status, params.status as never))
+ if (params.type) conditions.push(eq(incidents.incidentType, params.type as never))
+ if (params.site_id) conditions.push(eq(incidents.siteId, params.site_id))
+ if (params.truck_id) conditions.push(eq(incidents.truckId, params.truck_id))
+ const where = conditions.length ? and(...conditions) : undefined
- const [{ data: incidents, count }, { data: sites }, { data: trucks }] = await Promise.all([
- query.range(from, from + PAGE_SIZE - 1),
- supabase.from('sites').select('id, name').order('name'),
- supabase.from('trucks').select('id, truck_no').eq('active', true).order('truck_no'),
+ const [[countRow], incidentRows, siteRows, truckRows] = await Promise.all([
+ asAdmin(db =>
+ db.select({ count: sql`count(*)` })
+ .from(incidents)
+ .where(where)
+ ),
+ asAdmin(db =>
+ db.select({
+ id: incidents.id,
+ referenceNo: incidents.referenceNo,
+ incidentType: incidents.incidentType,
+ status: incidents.status,
+ severity: incidents.severity,
+ reportedAt: incidents.reportedAt,
+ siteName: sites.name,
+ zoneName: zones.name,
+ reporterName: reporterAlias.name,
+ truckNo: trucks.truckNo,
+ })
+ .from(incidents)
+ .leftJoin(sites, eq(incidents.siteId, sites.id))
+ .leftJoin(zones, eq(incidents.zoneId, zones.id))
+ .leftJoin(reporterAlias, eq(incidents.reportedBy, reporterAlias.id))
+ .leftJoin(trucks, eq(incidents.truckId, trucks.id))
+ .where(where)
+ .orderBy(desc(incidents.reportedAt))
+ .limit(PAGE_SIZE)
+ .offset(from)
+ ),
+ asAdmin(db =>
+ db.select({ id: sites.id, name: sites.name }).from(sites).orderBy(asc(sites.name))
+ ),
+ asAdmin(db =>
+ db.select({ id: trucks.id, truckNo: trucks.truckNo })
+ .from(trucks)
+ .where(eq(trucks.active, true))
+ .orderBy(asc(trucks.truckNo))
+ ),
])
- const siteOptions: SiteOption[] = (sites ?? []).map(s => ({ id: s.id, name: s.name }))
- const truckOptions: TruckOption[] = (trucks ?? []).map(tk => ({ id: tk.id, truck_no: tk.truck_no }))
+ const total = Number(countRow?.count ?? 0)
+
+ const incidentList: Incident[] = incidentRows.map(r => ({
+ id: r.id,
+ reference_no: r.referenceNo,
+ incident_type: r.incidentType,
+ status: r.status,
+ severity: r.severity,
+ reported_at: r.reportedAt.toISOString(),
+ sites: r.siteName ? { name: r.siteName } : null,
+ zones: r.zoneName ? { name: r.zoneName } : null,
+ trucks: r.truckNo ? { truck_no: r.truckNo } : null,
+ reporter: r.reporterName ? { name: r.reporterName } : null,
+ }))
+
+ const siteOptions: SiteOption[] = siteRows.map(s => ({ id: s.id, name: s.name }))
+ const truckOptions: TruckOption[] = truckRows.map(t => ({ id: t.id, truck_no: t.truckNo }))
return (
Incident Inbox
- {count ?? incidents?.length ?? 0} incidents
+ {total} incidents
-
-
+
+
)
}
diff --git a/app/(protected)/supervisor/page.tsx b/app/(protected)/supervisor/page.tsx
index dee0b30..afaf1ee 100644
--- a/app/(protected)/supervisor/page.tsx
+++ b/app/(protected)/supervisor/page.tsx
@@ -1,9 +1,12 @@
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, capaActions, users } from '@/lib/db/schema'
+import { eq, not, and, inArray, sql } from 'drizzle-orm'
+import { aliasedTable } from 'drizzle-orm'
import { StatCard } from '@/components/dashboard/stat-card'
const STATUS_LABELS: Record = {
@@ -29,8 +32,6 @@ export default async function SupervisorPage() {
if (!session) redirect('/login')
if (!['supervisor', 'admin'].includes(session.role)) redirect('/')
- const supabase = await createClient()
-
if (!session.siteId) {
return (
@@ -40,56 +41,69 @@ export default async function SupervisorPage() {
)
}
- const { data: siteRow } = await supabase.from('sites').select('name').eq('id', session.siteId).single()
- const siteName = (siteRow as unknown as { name: string } | null)?.name ?? 'Your Site'
+ const [siteRow] = await asAdmin(db =>
+ db.select({ name: sites.name }).from(sites).where(eq(sites.id, session.siteId!)).limit(1)
+ )
+ const siteName = siteRow?.name ?? 'Your Site'
- // Resolve incident IDs once to avoid duplicate queries inside Promise.all
- const { data: siteIncidents } = await supabase
- .from('incidents')
- .select('id')
- .eq('site_id', session.siteId)
- const incidentIds = siteIncidents?.map(r => r.id) ?? []
+ // Get all incident IDs for this site
+ const siteIncidents = await asAdmin(db =>
+ db.select({ id: incidents.id }).from(incidents).where(eq(incidents.siteId, session.siteId!))
+ )
+ const incidentIds = siteIncidents.map(r => r.id)
- const [
- { data: openIncidents },
- { count: closedCount },
- { count: openCount },
- { data: overdueCapas },
- { data: allCapas },
- ] = await Promise.all([
- supabase
- .from('incidents')
- .select('id, reference_no, incident_type, status, reported_at')
- .eq('site_id', session.siteId)
- .neq('status', 'closed')
- .order('reported_at', { ascending: false })
- .limit(10),
- supabase
- .from('incidents')
- .select('*', { count: 'exact', head: true })
- .eq('site_id', session.siteId)
- .eq('status', 'closed'),
- supabase
- .from('incidents')
- .select('*', { count: 'exact', head: true })
- .eq('site_id', session.siteId)
- .neq('status', 'closed'),
- supabase
- .from('capa_actions')
- .select('id, description, due_date, users (name)')
- .eq('status', 'overdue')
- .in('incident_id', incidentIds),
- supabase
- .from('capa_actions')
- .select('id, status')
- .in('incident_id', incidentIds),
+ const ownerAlias = aliasedTable(users, 'owner')
+
+ const [openIncidents, closedCountRow, openCountRow, overdueCapas, allCapas] = await Promise.all([
+ asAdmin(db =>
+ db.select({
+ id: incidents.id,
+ referenceNo: incidents.referenceNo,
+ incidentType: incidents.incidentType,
+ status: incidents.status,
+ reportedAt: incidents.reportedAt,
+ })
+ .from(incidents)
+ .where(and(eq(incidents.siteId, session.siteId!), not(eq(incidents.status, 'closed'))))
+ .orderBy(sql`${incidents.reportedAt} desc`)
+ .limit(10)
+ ),
+ asAdmin(db =>
+ db.select({ count: sql`count(*)` })
+ .from(incidents)
+ .where(and(eq(incidents.siteId, session.siteId!), eq(incidents.status, 'closed')))
+ ),
+ asAdmin(db =>
+ db.select({ count: sql`count(*)` })
+ .from(incidents)
+ .where(and(eq(incidents.siteId, session.siteId!), not(eq(incidents.status, 'closed'))))
+ ),
+ incidentIds.length > 0
+ ? asAdmin(db =>
+ db.select({
+ id: capaActions.id,
+ description: capaActions.description,
+ dueDate: capaActions.dueDate,
+ ownerName: ownerAlias.name,
+ })
+ .from(capaActions)
+ .leftJoin(ownerAlias, eq(capaActions.ownerUserId, ownerAlias.id))
+ .where(and(eq(capaActions.status, 'overdue'), inArray(capaActions.incidentId, incidentIds)))
+ )
+ : Promise.resolve([]),
+ incidentIds.length > 0
+ ? asAdmin(db =>
+ db.select({ id: capaActions.id, status: capaActions.status })
+ .from(capaActions)
+ .where(inArray(capaActions.incidentId, incidentIds))
+ )
+ : Promise.resolve([]),
])
- const resolvedOpenCount = openCount ?? 0
- const resolvedClosedCount = closedCount ?? 0
- const overdueCount = overdueCapas?.length ?? 0
-
- const capaRows = allCapas ?? []
+ const resolvedOpenCount = Number(openCountRow[0]?.count ?? 0)
+ const resolvedClosedCount = Number(closedCountRow[0]?.count ?? 0)
+ const overdueCount = overdueCapas.length
+ const capaRows = allCapas
const capaOpenCount = capaRows.filter(c => ['open', 'in_progress'].includes(c.status)).length
const capaVerifiedCount = capaRows.filter(c => c.status === 'verified').length
@@ -121,19 +135,19 @@ export default async function SupervisorPage() {
{/* Open incidents */}
Open Incidents
- {(openIncidents ?? []).length === 0 ? (
+ {openIncidents.length === 0 ? (
No open incidents.
) : (
- {(openIncidents ?? []).map(inc => (
+ {openIncidents.map(inc => (
-
{inc.reference_no ?? inc.id.slice(0, 8)}
-
{TYPE_LABELS[inc.incident_type] ?? inc.incident_type}
+
{inc.referenceNo ?? inc.id.slice(0, 8)}
+
{TYPE_LABELS[inc.incidentType] ?? inc.incidentType}
{STATUS_LABELS[inc.status] ?? inc.status}
@@ -149,16 +163,16 @@ export default async function SupervisorPage() {
Overdue CAPAs
- {(overdueCapas ?? []).map(capa => (
+ {overdueCapas.map(capa => (
{capa.description}
- Owner: {(capa.users as unknown as { name: string } | null)?.name ?? 'Unassigned'}
+ Owner: {capa.ownerName ?? 'Unassigned'}
- Due {capa.due_date ? new Date(capa.due_date as string).toLocaleDateString('en-MY') : '—'}
+ Due {capa.dueDate ? new Date(capa.dueDate).toLocaleDateString('en-MY') : '—'}
))}
diff --git a/app/report/page.tsx b/app/report/page.tsx
index 52e914c..f2ce9a5 100644
--- a/app/report/page.tsx
+++ b/app/report/page.tsx
@@ -1,8 +1,10 @@
export const dynamic = 'force-dynamic'
import { redirect } from 'next/navigation'
-import { createClient } from '@/lib/supabase/server'
+import { asAdmin } from '@/lib/db/with-user'
import { getSession } from '@/lib/auth/get-session'
+import { zones, sites, trucks } from '@/lib/db/schema'
+import { eq, asc } from 'drizzle-orm'
import { ReportForm } from '@/components/incidents/report-form'
import { LanguageSwitcher } from '@/components/language-switcher'
import { OfflineSync } from '@/components/incidents/offline-sync'
@@ -16,28 +18,42 @@ export default async function ReportPage({ searchParams }: Props) {
const session = await getSession()
if (!session) redirect(`/login?redirect=/report${zone ? `?zone=${zone}` : ''}`)
- const supabase = await createClient()
-
let zoneName: string | null = null
let siteName: string | null = null
if (zone) {
- const { data: zd } = await supabase
- .from('zones')
- .select('id, name, site_id, sites (name)')
- .eq('qr_code_token', zone)
- .single()
+ const [zd] = await asAdmin(db =>
+ db.select({
+ name: zones.name,
+ siteName: sites.name,
+ })
+ .from(zones)
+ .leftJoin(sites, eq(zones.siteId, sites.id))
+ .where(eq(zones.qrCodeToken, zone))
+ .limit(1)
+ )
if (zd) {
- zoneName = (zd as { name: string }).name ?? null
- siteName = (zd.sites as unknown as { name: string } | null)?.name ?? null
+ zoneName = zd.name
+ siteName = zd.siteName ?? null
}
}
- const { data: trucks } = await supabase
- .from('trucks')
- .select('id, truck_no, carrier')
- .eq('active', true)
- .order('truck_no')
+ const truckRows = await asAdmin(db =>
+ db.select({
+ id: trucks.id,
+ truckNo: trucks.truckNo,
+ carrier: trucks.carrier,
+ })
+ .from(trucks)
+ .where(eq(trucks.active, true))
+ .orderBy(asc(trucks.truckNo))
+ )
+
+ const truckList = truckRows.map(t => ({
+ id: t.id,
+ truck_no: t.truckNo,
+ carrier: t.carrier ?? null,
+ }))
return (
@@ -58,7 +74,7 @@ export default async function ReportPage({ searchParams }: Props) {
zoneToken={zone ?? null}
zoneName={zoneName}
siteName={siteName}
- trucks={(trucks ?? []) as Array<{ id: string; truck_no: string; carrier: string | null }>}
+ trucks={truckList}
initialTruckId={truck_id ?? null}
/>