diff --git a/app/(protected)/hse/incidents/[id]/page.tsx b/app/(protected)/hse/incidents/[id]/page.tsx
new file mode 100644
index 0000000..1895e0c
--- /dev/null
+++ b/app/(protected)/hse/incidents/[id]/page.tsx
@@ -0,0 +1,41 @@
+export const dynamic = 'force-dynamic'
+
+import { notFound } from 'next/navigation'
+import Link from 'next/link'
+import { createClient } from '@/lib/supabase/server'
+import { IncidentDetail } from '@/components/incidents/incident-detail'
+
+interface Props {
+ params: Promise<{ id: string }>
+}
+
+export default async function HseIncidentDetailPage({ 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()
+
+ if (error || !incident) notFound()
+
+ return (
+
+
+ ← Back to inbox
+
+
+
+ )
+}
diff --git a/app/(protected)/supervisor/incidents/[id]/page.tsx b/app/(protected)/supervisor/incidents/[id]/page.tsx
new file mode 100644
index 0000000..fc6b0e0
--- /dev/null
+++ b/app/(protected)/supervisor/incidents/[id]/page.tsx
@@ -0,0 +1,41 @@
+export const dynamic = 'force-dynamic'
+
+import { notFound } from 'next/navigation'
+import Link from 'next/link'
+import { createClient } from '@/lib/supabase/server'
+import { IncidentDetail } from '@/components/incidents/incident-detail'
+
+interface Props {
+ params: Promise<{ id: string }>
+}
+
+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()
+
+ if (error || !incident) notFound()
+
+ return (
+
+
+ ← Back to inbox
+
+
+
+ )
+}
diff --git a/components/incidents/evidence-gallery.tsx b/components/incidents/evidence-gallery.tsx
new file mode 100644
index 0000000..4887439
--- /dev/null
+++ b/components/incidents/evidence-gallery.tsx
@@ -0,0 +1,46 @@
+'use client'
+
+import type { EvidenceStage } from '@/lib/supabase/storage'
+
+type EvidenceFile = {
+ id: string
+ stage: string
+ file_url: string
+ file_type: string
+ uploaded_at: string
+}
+
+interface Props {
+ files: EvidenceFile[]
+ stage?: EvidenceStage
+}
+
+function isImage(type: string) { return type.startsWith('image/') }
+function isVideo(type: string) { return type.startsWith('video/') }
+
+export function EvidenceGallery({ files, stage }: Props) {
+ const filtered = stage ? files.filter(f => f.stage === stage) : files
+ if (filtered.length === 0) return
No files for this stage
+
+ return (
+
+ )
+}
diff --git a/components/incidents/incident-detail.tsx b/components/incidents/incident-detail.tsx
new file mode 100644
index 0000000..f37aa92
--- /dev/null
+++ b/components/incidents/incident-detail.tsx
@@ -0,0 +1,116 @@
+import { EvidenceGallery } from './evidence-gallery'
+
+const TYPE_LABELS: Record = {
+ injury: 'Injury / Medical',
+ near_miss: 'Near Miss',
+ hazard: 'Hazard',
+ asset_damage: 'Asset Damage',
+ environmental: 'Environmental',
+ security: 'Security',
+ fire: 'Fire / Emergency',
+}
+
+const STATUS_COLORS: Record = {
+ reported: 'bg-yellow-100 text-yellow-800',
+ triaged: 'bg-blue-100 text-blue-800',
+ investigating: 'bg-purple-100 text-purple-800',
+ capa_pending: 'bg-orange-100 text-orange-800',
+ verification: 'bg-indigo-100 text-indigo-800',
+ closed: 'bg-green-100 text-green-800',
+}
+
+const MEDICAL_LABELS: Record = {
+ none: 'None',
+ first_aid: 'First Aid',
+ medical_treatment: 'Medical Treatment',
+ lti: 'Lost Time Injury (LTI)',
+}
+
+type Incident = {
+ id: string
+ reference_no: string | null
+ incident_type: string
+ description: string
+ status: string
+ severity: number | null
+ injury_involved: boolean
+ asset_involved: boolean
+ medical_status: string | null
+ lost_days: number | null
+ reported_at: string
+ closed_at: string | null
+ sites: { id: string; name: string } | null
+ zones: { id: string; name: string } | null
+ reporter: { id: string; name: string; email: string } | null
+ evidence_files: Array<{
+ id: string
+ stage: string
+ file_url: string
+ file_type: string
+ uploaded_at: string
+ }>
+}
+
+interface Props {
+ incident: Incident
+}
+
+function Field({ label, value }: { label: string; value: React.ReactNode }) {
+ return (
+
+
{label}
+ {value ?? '—'}
+
+ )
+}
+
+export function IncidentDetail({ incident }: Props) {
+ const reportStageFiles = incident.evidence_files.filter(f => f.stage === 'report')
+
+ return (
+
+
+
+
+
+ {incident.reference_no ?? 'Pending reference'}
+
+
{TYPE_LABELS[incident.incident_type] ?? incident.incident_type}
+
+
+ {incident.status.replace(/_/g, ' ').toUpperCase()}
+
+
+
+
+
+
+
+
+
+ {incident.injury_involved && (
+
+ )}
+ {incident.injury_involved && incident.lost_days != null && (
+
+ )}
+
+
+
+
+
Description
+
{incident.description}
+
+
+
+
+ Evidence — Report Stage
+
+ {reportStageFiles.length} file{reportStageFiles.length !== 1 ? 's' : ''}
+
+
+
+
+
+ )
+}
diff --git a/tests/components/incidents/incident-detail.test.tsx b/tests/components/incidents/incident-detail.test.tsx
new file mode 100644
index 0000000..e46eb3d
--- /dev/null
+++ b/tests/components/incidents/incident-detail.test.tsx
@@ -0,0 +1,52 @@
+import { describe, it, expect } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { IncidentDetail } from '@/components/incidents/incident-detail'
+
+const mockIncident = {
+ id: 'inc-001',
+ reference_no: 'SCW1-202607-0001',
+ incident_type: 'near_miss',
+ description: 'Forklift nearly hit a pedestrian in Dock A aisle near the charging station.',
+ status: 'reported',
+ severity: null,
+ injury_involved: false,
+ asset_involved: false,
+ medical_status: null,
+ lost_days: null,
+ reported_at: '2026-07-10T09:00:00Z',
+ closed_at: null,
+ sites: { id: 'site-001', name: 'SCW1' },
+ zones: { id: 'zone-001', name: 'Dock A' },
+ reporter: { id: 'user-001', name: 'John Doe', email: 'john@example.com' },
+ evidence_files: [
+ {
+ id: 'ev-001',
+ stage: 'report',
+ file_url: 'https://example.com/photo.jpg',
+ file_type: 'image/jpeg',
+ uploaded_at: '2026-07-10T09:01:00Z',
+ },
+ ],
+}
+
+describe('IncidentDetail', () => {
+ it('renders reference number', () => {
+ render()
+ expect(screen.getByText('SCW1-202607-0001')).toBeTruthy()
+ })
+
+ it('renders description', () => {
+ render()
+ expect(screen.getByText(/Forklift nearly hit/)).toBeTruthy()
+ })
+
+ it('renders reported by', () => {
+ render()
+ expect(screen.getByText(/John Doe/)).toBeTruthy()
+ })
+
+ it('renders evidence count', () => {
+ render()
+ expect(screen.getByText(/1 file/i)).toBeTruthy()
+ })
+})