feat: show investigation panel on incident detail page

Fetches investigation record (5-Why steps, findings, root cause,
alcohol test, witness refs) and renders below incident details.
Shows completion status and investigator name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
This commit is contained in:
2026-07-13 10:15:08 +08:00
co-authored by Claude Sonnet 4.6
parent 782a9687e2
commit 5416929cab
2 changed files with 146 additions and 0 deletions
@@ -6,6 +6,7 @@ import { IncidentDetail, type Incident } from '@/components/incidents/incident-d
import { IncidentHeader } from '@/components/incidents/incident-header' import { IncidentHeader } from '@/components/incidents/incident-header'
import { SimilarIncidentsPanel } from '@/components/incidents/similar-incidents-panel' import { SimilarIncidentsPanel } from '@/components/incidents/similar-incidents-panel'
import { ClosurePanel } from '@/components/incidents/closure-panel' import { ClosurePanel } from '@/components/incidents/closure-panel'
import { InvestigationPanel } from '@/components/incidents/investigation-panel'
interface Props { interface Props {
params: Promise<{ id: string }> params: Promise<{ id: string }>
@@ -35,6 +36,17 @@ export default async function HseIncidentDetailPage({ params }: Props) {
const status = (incident as { status: string }).status const status = (incident as { status: string }).status
const { data: investigation } = await supabase
.from('investigations')
.select(`
id, method, five_why_steps, fishbone_categories,
findings_text, root_cause_summary, alcohol_test_result,
witness_statement_refs, completed_at,
investigator:users!investigator_id (name, email)
`)
.eq('incident_id', id)
.maybeSingle()
const needsJkkp = Boolean( const needsJkkp = Boolean(
(incident as { is_fatality?: boolean }).is_fatality || (incident as { is_fatality?: boolean }).is_fatality ||
(incident as { is_serious_bodily_injury?: boolean }).is_serious_bodily_injury || (incident as { is_serious_bodily_injury?: boolean }).is_serious_bodily_injury ||
@@ -56,6 +68,10 @@ export default async function HseIncidentDetailPage({ params }: Props) {
<main className="max-w-3xl mx-auto px-4 py-6"> <main className="max-w-3xl mx-auto px-4 py-6">
<IncidentDetail incident={incident as unknown as Incident} /> <IncidentDetail incident={incident as unknown as Incident} />
{investigation && (
<InvestigationPanel investigation={investigation as never} />
)}
{needsJkkp && ( {needsJkkp && (
<div className="mt-4 flex gap-3 flex-wrap"> <div className="mt-4 flex gap-3 flex-wrap">
<a <a
@@ -0,0 +1,130 @@
type FiveWhyStep = { why: string; answer: string }
interface Investigation {
id: string
method: 'five_why' | 'fishbone' | 'other'
five_why_steps: FiveWhyStep[] | null
fishbone_categories: Record<string, string> | null
findings_text: string | null
root_cause_summary: string | null
alcohol_test_result: string | null
witness_statement_refs: string[]
completed_at: string | null
investigator: { name: string | null; email: string } | null
}
interface Props {
investigation: Investigation
}
const METHOD_LABELS = {
five_why: '5-Why Analysis',
fishbone: 'Fishbone (Ishikawa)',
other: 'Other Method',
}
export function InvestigationPanel({ investigation: inv }: Props) {
const investigatorName = inv.investigator?.name ?? inv.investigator?.email ?? 'Unknown'
return (
<div className="mt-6 bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-100">
<h2 className="text-sm font-semibold text-gray-900">Investigation</h2>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">{METHOD_LABELS[inv.method] ?? inv.method}</span>
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${
inv.completed_at
? 'bg-green-100 text-green-700'
: 'bg-yellow-100 text-yellow-700'
}`}>
{inv.completed_at ? 'Completed' : 'In Progress'}
</span>
</div>
</div>
<div className="px-5 py-4 space-y-5">
<p className="text-xs text-gray-500">
Investigator: <span className="font-medium text-gray-700">{investigatorName}</span>
{inv.completed_at && (
<span className="ml-2">
· Completed {new Date(inv.completed_at).toLocaleDateString('en-MY', { day: 'numeric', month: 'short', year: 'numeric' })}
</span>
)}
</p>
{/* 5-Why steps */}
{inv.method === 'five_why' && inv.five_why_steps && inv.five_why_steps.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">5-Why Steps</h3>
<ol className="space-y-3">
{inv.five_why_steps.map((step, i) => (
<li key={i} className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 text-blue-700 text-xs font-bold flex items-center justify-center">
{i + 1}
</span>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-700">{step.why}</p>
{step.answer && (
<p className="text-sm text-gray-900 mt-0.5">{step.answer}</p>
)}
</div>
</li>
))}
</ol>
</div>
)}
{/* Fishbone categories */}
{inv.method === 'fishbone' && inv.fishbone_categories && Object.keys(inv.fishbone_categories).length > 0 && (
<div>
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Fishbone Categories</h3>
<dl className="space-y-2">
{Object.entries(inv.fishbone_categories).map(([cat, val]) => (
<div key={cat}>
<dt className="text-xs font-medium text-gray-500 capitalize">{cat}</dt>
<dd className="text-sm text-gray-900">{val}</dd>
</div>
))}
</dl>
</div>
)}
{/* Findings */}
{inv.findings_text && (
<div>
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Findings</h3>
<p className="text-sm text-gray-900 whitespace-pre-wrap">{inv.findings_text}</p>
</div>
)}
{/* Root cause */}
{inv.root_cause_summary && (
<div className="bg-red-50 border border-red-100 rounded-lg px-4 py-3">
<h3 className="text-xs font-semibold text-red-600 uppercase tracking-wide mb-1">Root Cause</h3>
<p className="text-sm text-gray-900 whitespace-pre-wrap">{inv.root_cause_summary}</p>
</div>
)}
{/* Alcohol test */}
{inv.alcohol_test_result && (
<div>
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Alcohol Test Result</h3>
<p className="text-sm text-gray-900">{inv.alcohol_test_result}</p>
</div>
)}
{/* Witness refs */}
{inv.witness_statement_refs && inv.witness_statement_refs.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Witness Statements</h3>
<ul className="list-disc list-inside space-y-1">
{inv.witness_statement_refs.map((ref, i) => (
<li key={i} className="text-sm text-gray-900">{ref}</li>
))}
</ul>
</div>
)}
</div>
</div>
)
}