Files
open-safetylog-ehs/src/pages/IncidentDetail.tsx
T
jayjennings 3e964d1176
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s
feat(i18n): add internationalization support with English and Spanish translations
feat(kiosk): implement kiosk report submission functionality with offline support

test(outbox): add tests for outbox functionality including report submission and error handling

feat(outbox): create outbox management for queued reports with IndexedDB persistence

feat(uuid): add UUID generation utility for client IDs

feat(qr): implement QR code generation for reporting links by site and area

feat(kiosk): create Kiosk page for reporting incidents without an account
2026-10-06 11:38:40 -07:00

100 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ArrowLeft, CheckCircle2, MapPin, Printer } from 'lucide-react'
import { Link, useLocation, useParams } from 'react-router-dom'
import { SeverityLabel, StatusBadge } from '../components/Badges'
import { ActionsSection } from '../components/incident/ActionsSection'
import { ActivitySection } from '../components/incident/ActivitySection'
import { AttachmentsSection } from '../components/incident/AttachmentsSection'
import { DetailsSection } from '../components/incident/DetailsSection'
import { InvestigationSection } from '../components/incident/InvestigationSection'
import { WorkflowPanel } from '../components/incident/WorkflowPanel'
import { Page } from '../components/Page'
import { ErrorMessage, Loading } from '../components/QueryState'
import { secondaryButton } from '../components/styles'
import { ApiError } from '../lib/api'
import { formatDateTime } from '../lib/dates'
import { incidentTypeLabel } from '../lib/labels'
import { useIncident } from '../lib/queries'
export function IncidentDetail() {
const number = Number(useParams().number)
const location = useLocation()
const query = useIncident(number)
const incident = query.data
if (query.isPending) return <Loading />
if (!incident) {
return (
<Page>
{query.error instanceof ApiError && query.error.status === 404 ? (
<p className="text-sm text-muted">
This incident doesn’t exist or you don’t have access to it.{' '}
<Link to="/incidents" className="text-coral underline">
Back to incidents
</Link>
</p>
) : (
<ErrorMessage error={query.error} />
)}
</Page>
)
}
const justReported = (location.state as { justReported?: boolean } | null)?.justReported
return (
<Page>
<Link to="/incidents" className="mb-4 inline-flex items-center gap-1 text-2xs font-semibold text-muted hover:text-ink print:hidden">
<ArrowLeft size={14} /> All incidents
</Link>
{justReported && (
<div className="mb-5 flex items-start gap-2 rounded-md bg-mint-soft px-4 py-3 text-xs text-[#2b6e4f] print:hidden" role="status">
<CheckCircle2 size={16} className="mt-px flex-none" />
<span>
<strong>Thanks, {incident.reference} has been reported.</strong> The safety team has been notified{incident.severity === 'high' || incident.severity === 'critical' ? ' straight away' : ''}.
</span>
</div>
)}
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<span className="font-mono text-3xs font-medium tracking-[0.09em] text-[#8da29b] uppercase">
{incident.reference} · {incidentTypeLabel[incident.type]}
</span>
<h1 className="mt-1.5 mb-3 font-display text-[26px] leading-tight font-semibold tracking-[-0.5px] text-ink sm:text-[30px]">{incident.title}</h1>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-2xs text-body">
<StatusBadge status={incident.status} />
<SeverityLabel severity={incident.severity} />
<span className="inline-flex items-center gap-1">
<MapPin size={13} />
{[incident.site.name, incident.area?.name].filter(Boolean).join(' · ')}
</span>
<span>Occurred {formatDateTime(incident.occurredAt)}</span>
<span>
Reported by {incident.anonymous ? 'anonymous' : (incident.reporter?.name ?? incident.reporterName ?? 'former user')}
{incident.kiosk && ` via ${incident.kiosk.name}`} · {formatDateTime(incident.reportedAt)}
</span>
</div>
</div>
<button type="button" className={`${secondaryButton} flex-none print:hidden`} onClick={() => window.print()}>
<Printer size={16} /> Print / PDF
</button>
</div>
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_320px] print:block">
<div className="min-w-0 space-y-5">
<DetailsSection incident={incident} />
<InvestigationSection incident={incident} />
<ActionsSection incident={incident} />
<ActivitySection incident={incident} />
</div>
<div className="space-y-5 print:mt-5">
<div className="print:hidden">
<WorkflowPanel incident={incident} />
</div>
<AttachmentsSection incident={incident} />
</div>
</div>
</Page>
)
}