- Implemented Form301 component for displaying OSHA Form 301 equivalent, including employee and case information. - Created Osha component to manage OSHA recordkeeping, including tabs for 300 Log, 300A Summary, Hours & rates, and Submit to OSHA. - Added functionality for printing and downloading CSV files for OSHA submissions. - Integrated loading and error handling states for data fetching.
443 lines
22 KiB
TypeScript
443 lines
22 KiB
TypeScript
import { AlertOctagon, CheckCircle2, CircleDashed, Pencil, Printer, Trash2, UserPlus, XCircle } from 'lucide-react'
|
|
import { useState, type ChangeEvent, type FormEvent, type ReactNode } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import type { IncidentDetail, OshaCase } from '../../../shared/api-types.ts'
|
|
import { employmentTypes, oshaCaseTypes, oshaSpecialCases } from '../../../shared/enums.ts'
|
|
import { assessRecordability, FIRST_AID, OSHA_REPORTING_CONTACT, PRIVACY_CASES, severeInjuryReport, type EmploymentType } from '../../../shared/osha.ts'
|
|
import type { OshaCaseInput } from '../../../shared/schemas.ts'
|
|
import { formatDateTime, toDateTimeLocal } from '../../lib/dates'
|
|
import { employmentTypeLabel, oshaCaseTypeLabel, oshaOutcomeLabel, oshaSpecialCaseLabel } from '../../lib/labels'
|
|
import { useOshaCaseMutations, useOshaCases } from '../../lib/queries'
|
|
import { Field } from '../FormSection'
|
|
import { ErrorMessage, Loading } from '../QueryState'
|
|
import { field, iconButton, primaryButton, secondaryButton, smallButton, textButton } from '../styles'
|
|
import { Section } from './Section'
|
|
|
|
/** Injured or ill people and their OSHA recordkeeping. Medical details are only shown to safety managers. */
|
|
export function OshaSection({ incident }: { incident: IncidentDetail }) {
|
|
const cases = useOshaCases(incident.number, incident.can.manageOsha)
|
|
const [editing, setEditing] = useState<OshaCase | 'new' | null>(null)
|
|
const { remove } = useOshaCaseMutations(incident.number)
|
|
|
|
if (!incident.can.manageOsha) {
|
|
if (incident.injuredCount === 0) return null
|
|
return (
|
|
<Section title="Injured or ill people">
|
|
<p className="text-xs text-muted">
|
|
{incident.injuredCount} {incident.injuredCount === 1 ? 'person' : 'people'} recorded. Injury and medical details are confidential and only visible to safety managers.
|
|
</p>
|
|
</Section>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Section
|
|
title="Injured or ill people"
|
|
action={
|
|
!editing && (
|
|
<button type="button" className={smallButton} onClick={() => setEditing('new')}>
|
|
<UserPlus size={13} /> Add person
|
|
</button>
|
|
)
|
|
}
|
|
>
|
|
{editing ? (
|
|
<CaseForm incident={incident} existing={editing === 'new' ? null : editing} onDone={() => setEditing(null)} />
|
|
) : cases.isPending ? (
|
|
<Loading />
|
|
) : cases.data?.length ? (
|
|
<ul className="space-y-3">
|
|
{cases.data.map((c) => (
|
|
<CaseSummary
|
|
key={c.id}
|
|
osha={c}
|
|
onEdit={() => setEditing(c)}
|
|
onRemove={() => confirm(`Remove ${c.name} (${c.caseNumber}) from this incident? This deletes their OSHA case.`) && remove.mutate(c.id)}
|
|
/>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="text-xs text-faint">Nobody recorded. Add anyone who was hurt or made ill to decide whether it goes on the OSHA log.</p>
|
|
)}
|
|
<ErrorMessage error={cases.error ?? remove.error} className="mt-3" />
|
|
</Section>
|
|
)
|
|
}
|
|
|
|
function Verdict({ recordable, status, outcome }: { recordable: boolean; status?: 'incomplete'; outcome: OshaCase['outcome'] }) {
|
|
if (status === 'incomplete') {
|
|
return (
|
|
<span className="inline-flex items-center gap-1 rounded bg-line-soft px-2 py-1 text-3xs font-semibold text-muted">
|
|
<CircleDashed size={12} /> Not assessed
|
|
</span>
|
|
)
|
|
}
|
|
return recordable ? (
|
|
<span className="inline-flex items-center gap-1 rounded bg-coral-soft px-2 py-1 text-3xs font-semibold text-coral-strong">
|
|
<CheckCircle2 size={12} /> OSHA recordable · {oshaOutcomeLabel[outcome ?? 'other']}
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1 rounded bg-mint-soft px-2 py-1 text-3xs font-semibold text-[#2b6e4f]">
|
|
<XCircle size={12} /> Not recordable
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function SevereAlert({ osha }: { osha: OshaCase }) {
|
|
if (!osha.severeReport || osha.reportedToOshaAt) return null
|
|
return (
|
|
<div className="mt-2 flex gap-2 rounded-md border border-coral bg-[#fff2ee] px-3 py-2 text-2xs text-coral-strong" role="alert">
|
|
<AlertOctagon size={15} className="mt-px flex-none" />
|
|
<span>
|
|
<strong>Report to OSHA within {osha.severeReport.withinHours} hours</strong> ({osha.severeReport.reasons.join(', ')}), by {formatDateTime(osha.severeReport.dueBy)}. {OSHA_REPORTING_CONTACT}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CaseSummary({ osha, onEdit, onRemove }: { osha: OshaCase; onEdit: () => void; onRemove: () => void }) {
|
|
const assessed = assessRecordability(osha)
|
|
return (
|
|
<li className="rounded-md border border-line bg-field p-3">
|
|
<div className="flex flex-wrap items-start gap-2">
|
|
<div className="min-w-[200px] flex-1">
|
|
<p className="text-xs font-semibold text-ink">
|
|
{osha.name}
|
|
{osha.privacyCase && <span className="ml-2 font-mono text-3xs font-normal text-faint uppercase">Privacy case</span>}
|
|
</p>
|
|
<p className="mt-0.5 text-3xs text-muted">
|
|
Case {osha.caseNumber} · {[osha.jobTitle, osha.natureOfInjury, osha.bodyPart].filter(Boolean).join(' · ') || 'No details yet'}
|
|
</p>
|
|
</div>
|
|
<Verdict recordable={osha.recordable} status={assessed.status === 'incomplete' ? 'incomplete' : undefined} outcome={osha.outcome} />
|
|
<div className="flex items-center gap-1 print:hidden">
|
|
<button type="button" className={iconButton} aria-label={`Edit case ${osha.caseNumber}`} onClick={onEdit}>
|
|
<Pencil size={14} />
|
|
</button>
|
|
{osha.recordable && (
|
|
<Link to={`/osha/cases/${osha.id}/301`} className={iconButton} aria-label={`Form 301 for case ${osha.caseNumber}`} title="Form 301">
|
|
<Printer size={14} />
|
|
</Link>
|
|
)}
|
|
<button type="button" className={iconButton} aria-label={`Remove case ${osha.caseNumber}`} onClick={onRemove}>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{osha.reportedToOshaAt && <p className="mt-2 text-3xs text-[#2b6e4f]">Reported to OSHA {formatDateTime(osha.reportedToOshaAt)}</p>}
|
|
<SevereAlert osha={osha} />
|
|
{osha.missingFields.length > 0 && <p className="mt-2 text-3xs text-amber-strong">Still needed for Form 301 / OSHA submission: {osha.missingFields.join(', ')}.</p>}
|
|
</li>
|
|
)
|
|
}
|
|
|
|
type FormState = Required<Omit<OshaCaseInput, 'reportedToOshaAt' | 'sex'>> & { reportedToOshaAt: string; sex: '' | 'M' | 'F' }
|
|
|
|
function initialState(existing: OshaCase | null, incident: IncidentDetail): FormState {
|
|
const c = existing
|
|
return {
|
|
name: c?.name ?? '',
|
|
jobTitle: c?.jobTitle ?? '',
|
|
employmentType: c?.employmentType ?? 'employee',
|
|
bodyPart: c?.bodyPart ?? '',
|
|
natureOfInjury: c?.natureOfInjury ?? '',
|
|
caseType: c?.caseType ?? (incident.type === 'illness' ? 'other_illness' : 'injury'),
|
|
workRelated: c?.workRelated ?? null,
|
|
newCase: c?.newCase ?? null,
|
|
died: c?.died ?? false,
|
|
dateOfDeath: c?.dateOfDeath ?? '',
|
|
daysAwayFromWork: c?.daysAwayFromWork ?? false,
|
|
daysAway: c?.daysAway ?? 0,
|
|
restrictedOrTransfer: c?.restrictedOrTransfer ?? false,
|
|
daysRestricted: c?.daysRestricted ?? 0,
|
|
medicalTreatment: c?.medicalTreatment ?? false,
|
|
lossOfConsciousness: c?.lossOfConsciousness ?? false,
|
|
significantDiagnosis: c?.significantDiagnosis ?? false,
|
|
specialCase: c?.specialCase ?? 'none',
|
|
privacyCase: c?.privacyCase ?? false,
|
|
inpatientHospitalization: c?.inpatientHospitalization ?? false,
|
|
amputation: c?.amputation ?? false,
|
|
lossOfEye: c?.lossOfEye ?? false,
|
|
reportedToOshaAt: c?.reportedToOshaAt ? toDateTimeLocal(new Date(c.reportedToOshaAt)) : '',
|
|
employeeAddress: c?.employeeAddress ?? '',
|
|
dateOfBirth: c?.dateOfBirth ?? '',
|
|
dateOfHire: c?.dateOfHire ?? '',
|
|
sex: c?.sex ?? '',
|
|
physicianName: c?.physicianName ?? '',
|
|
treatmentFacility: c?.treatmentFacility ?? '',
|
|
treatedInEmergencyRoom: c?.treatedInEmergencyRoom ?? false,
|
|
timeStartedWork: c?.timeStartedWork ?? '',
|
|
activityBefore: c?.activityBefore ?? '',
|
|
whatHappened: c?.whatHappened ?? incident.description ?? '',
|
|
objectSubstance: c?.objectSubstance ?? '',
|
|
}
|
|
}
|
|
|
|
const empty = (value: string) => (value.trim() === '' ? null : value.trim())
|
|
|
|
function Group({ title, help, children }: { title: string; help?: ReactNode; children: ReactNode }) {
|
|
return (
|
|
<fieldset className="border-t border-line pt-4 first:border-0 first:pt-0">
|
|
<legend className="mb-1 text-xs font-semibold text-ink">{title}</legend>
|
|
{help && <div className="mb-3 text-2xs leading-[1.5] text-muted">{help}</div>}
|
|
{children}
|
|
</fieldset>
|
|
)
|
|
}
|
|
|
|
function YesNo({ label, value, onChange }: { label: string; value: boolean | null; onChange: (value: boolean) => void }) {
|
|
return (
|
|
<div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-2xs text-body">
|
|
<span className="min-w-[220px] flex-1 font-semibold text-[#586762]">{label}</span>
|
|
{[true, false].map((option) => (
|
|
<label key={String(option)} className="inline-flex items-center gap-1.5">
|
|
<input type="radio" className="accent-coral" checked={value === option} onChange={() => onChange(option)} />
|
|
{option ? 'Yes' : 'No'}
|
|
</label>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Check({ label, checked, onChange, children }: { label: ReactNode; checked: boolean; onChange: (checked: boolean) => void; children?: ReactNode }) {
|
|
return (
|
|
<div className="mb-2.5">
|
|
<label className="flex items-start gap-2 text-2xs text-body">
|
|
<input type="checkbox" className="mt-0.5 accent-coral" checked={checked} onChange={(event) => onChange(event.target.checked)} />
|
|
<span>{label}</span>
|
|
</label>
|
|
{checked && children && <div className="mt-2 ml-6">{children}</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CaseForm({ incident, existing, onDone }: { incident: IncidentDetail; existing: OshaCase | null; onDone: () => void }) {
|
|
const { create, update } = useOshaCaseMutations(incident.number)
|
|
const [form, setForm] = useState<FormState>(() => initialState(existing, incident))
|
|
const set = <K extends keyof FormState>(key: K, value: FormState[K]) => setForm((current) => ({ ...current, [key]: value }))
|
|
const text = (key: keyof FormState) => (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => set(key, event.target.value as never)
|
|
|
|
const verdict = assessRecordability({ ...form, employmentType: form.employmentType as EmploymentType })
|
|
const severe = severeInjuryReport(form)
|
|
const totalDays = (form.daysAwayFromWork ? Number(form.daysAway) : 0) + (form.restrictedOrTransfer ? Number(form.daysRestricted) : 0)
|
|
const mutation = existing ? update : create
|
|
|
|
const submit = (event: FormEvent) => {
|
|
event.preventDefault()
|
|
const payload: OshaCaseInput & { name: string } = {
|
|
...form,
|
|
name: form.name.trim(),
|
|
daysAway: Number(form.daysAway) || 0,
|
|
daysRestricted: Number(form.daysRestricted) || 0,
|
|
sex: form.sex || null,
|
|
reportedToOshaAt: form.reportedToOshaAt ? new Date(form.reportedToOshaAt).toISOString() : null,
|
|
...Object.fromEntries(
|
|
(['jobTitle', 'bodyPart', 'natureOfInjury', 'employeeAddress', 'physicianName', 'treatmentFacility', 'activityBefore', 'whatHappened', 'objectSubstance', 'dateOfDeath', 'dateOfBirth', 'dateOfHire', 'timeStartedWork'] as const).map((key) => [
|
|
key,
|
|
empty(String(form[key] ?? '')),
|
|
]),
|
|
),
|
|
}
|
|
if (existing) update.mutate({ id: existing.id, ...payload }, { onSuccess: onDone })
|
|
else create.mutate(payload, { onSuccess: onDone })
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={submit} className="space-y-4">
|
|
<Group title="Who">
|
|
<div className="grid gap-x-4 sm:grid-cols-2">
|
|
<Field label="Name">
|
|
<input className={field} required value={form.name} onChange={text('name')} />
|
|
</Field>
|
|
<Field label="Job title" optional>
|
|
<input className={field} value={form.jobTitle ?? ''} onChange={text('jobTitle')} placeholder="e.g. Press operator" />
|
|
</Field>
|
|
<Field label="Worker type">
|
|
<select className={field} value={form.employmentType} onChange={text('employmentType')}>
|
|
{employmentTypes.map((type) => (
|
|
<option key={type} value={type}>
|
|
{employmentTypeLabel[type]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<Field label="Injury or illness type (300 Log column M)">
|
|
<select className={field} value={form.caseType} onChange={text('caseType')}>
|
|
{oshaCaseTypes.map((type) => (
|
|
<option key={type} value={type}>
|
|
{oshaCaseTypeLabel[type]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
</div>
|
|
</Group>
|
|
|
|
<Group title="Does it go on the OSHA log?" help="Answer from what you know now; you can update the case as things change (for example if days away turn into restricted work).">
|
|
<YesNo label="Is it work-related? (an event or exposure at work caused or worsened it; 1904.5)" value={form.workRelated} onChange={(v) => set('workRelated', v)} />
|
|
<YesNo label="Is it a new case? (not a continuation of an injury already on the log; 1904.6)" value={form.newCase} onChange={(v) => set('newCase', v)} />
|
|
<p className="mt-4 mb-2 text-2xs font-semibold text-[#586762]">Did it result in any of these? (tick all that apply)</p>
|
|
<Check label="Death" checked={form.died} onChange={(v) => set('died', v)}>
|
|
<Field label="Date of death">
|
|
<input className={field} type="date" value={form.dateOfDeath ?? ''} onChange={text('dateOfDeath')} />
|
|
</Field>
|
|
</Check>
|
|
<Check label="Days away from work (count calendar days after the day of injury, including weekends)" checked={form.daysAwayFromWork} onChange={(v) => set('daysAwayFromWork', v)}>
|
|
<Field label="Days away so far">
|
|
<input className={`${field} max-w-[140px]`} type="number" min={0} max={180} value={form.daysAway} onChange={text('daysAway')} />
|
|
</Field>
|
|
</Check>
|
|
<Check label="Restricted work or transfer to another job" checked={form.restrictedOrTransfer} onChange={(v) => set('restrictedOrTransfer', v)}>
|
|
<Field label="Days restricted or transferred so far">
|
|
<input className={`${field} max-w-[140px]`} type="number" min={0} max={180} value={form.daysRestricted} onChange={text('daysRestricted')} />
|
|
</Field>
|
|
</Check>
|
|
{totalDays > 180 && <p className="mb-2 ml-6 text-3xs text-coral-strong">Stop counting once days away and restricted days reach 180 in total.</p>}
|
|
<Check
|
|
label={
|
|
<>
|
|
Medical treatment beyond first aid{' '}
|
|
<details className="mt-1 inline-block w-full text-3xs text-muted">
|
|
<summary className="cursor-pointer text-coral">What counts as first aid? (the complete list)</summary>
|
|
<ul className="mt-1 list-disc space-y-0.5 pl-4">
|
|
{FIRST_AID.map((item) => (
|
|
<li key={item}>{item}</li>
|
|
))}
|
|
</ul>
|
|
<p className="mt-1">Anything not on this list, such as prescription medication, stitches, rigid splints or physical therapy, is medical treatment.</p>
|
|
</details>
|
|
</>
|
|
}
|
|
checked={form.medicalTreatment}
|
|
onChange={(v) => set('medicalTreatment', v)}
|
|
/>
|
|
<Check label="Loss of consciousness" checked={form.lossOfConsciousness} onChange={(v) => set('lossOfConsciousness', v)} />
|
|
<Check
|
|
label="Significant injury or illness diagnosed by a doctor or licensed health care professional (e.g. fractured bone, punctured eardrum, cancer, chronic irreversible disease)"
|
|
checked={form.significantDiagnosis}
|
|
onChange={(v) => set('significantDiagnosis', v)}
|
|
/>
|
|
<Field label="Special case (recordable even without the above)">
|
|
<select className={field} value={form.specialCase} onChange={text('specialCase')}>
|
|
{oshaSpecialCases.map((value) => (
|
|
<option key={value} value={value}>
|
|
{oshaSpecialCaseLabel[value]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
|
|
<div
|
|
className={`rounded-md border px-4 py-3 text-xs ${verdict.status === 'recordable' ? 'border-coral-soft bg-[#fff6f3]' : verdict.status === 'incomplete' ? 'border-line bg-canvas' : 'border-mint-soft bg-[#f3faf6]'}`}
|
|
aria-live="polite"
|
|
>
|
|
<strong className="text-ink">
|
|
{verdict.status === 'recordable' ? `Recordable: ${oshaOutcomeLabel[verdict.outcome!]}` : verdict.status === 'incomplete' ? 'Answer the questions above' : 'Not recordable'}
|
|
</strong>
|
|
<ul className="mt-1 list-disc space-y-0.5 pl-4 text-2xs text-body">
|
|
{verdict.reasons.map((reason) => (
|
|
<li key={reason}>{reason}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</Group>
|
|
|
|
<Group title="Serious injuries: report to OSHA directly (1904.39)" help="Fatalities must be reported within 8 hours; in-patient hospitalization, amputation or loss of an eye within 24 hours of learning about it.">
|
|
<Check label="Hospitalized as an in-patient (formally admitted)" checked={form.inpatientHospitalization} onChange={(v) => set('inpatientHospitalization', v)} />
|
|
<Check label="Amputation" checked={form.amputation} onChange={(v) => set('amputation', v)} />
|
|
<Check label="Loss of an eye" checked={form.lossOfEye} onChange={(v) => set('lossOfEye', v)} />
|
|
{severe && (
|
|
<div className="rounded-md border border-coral bg-[#fff2ee] px-3 py-2 text-2xs text-coral-strong">
|
|
<strong>Must be reported to OSHA within {severe.withinHours} hours.</strong> {OSHA_REPORTING_CONTACT}
|
|
<Field label="Reported to OSHA at" optional>
|
|
<input className={`${field} max-w-[240px]`} type="datetime-local" value={form.reportedToOshaAt} onChange={text('reportedToOshaAt')} />
|
|
</Field>
|
|
</div>
|
|
)}
|
|
</Group>
|
|
|
|
<Group title="Injury details (Forms 300 and 301)">
|
|
<div className="grid gap-x-4 sm:grid-cols-2">
|
|
<Field label="Injury or illness" optional>
|
|
<input className={field} value={form.natureOfInjury ?? ''} onChange={text('natureOfInjury')} placeholder="e.g. Laceration, sprain, burn" />
|
|
</Field>
|
|
<Field label="Part of body" optional>
|
|
<input className={field} value={form.bodyPart ?? ''} onChange={text('bodyPart')} placeholder="e.g. Left index finger" />
|
|
</Field>
|
|
</div>
|
|
<Field label="Object or substance that directly harmed them" optional>
|
|
<input className={field} value={form.objectSubstance ?? ''} onChange={text('objectSubstance')} placeholder="e.g. Press die, chlorine, concrete floor" />
|
|
</Field>
|
|
<Field label="What were they doing just before?" optional>
|
|
<textarea className={`${field} resize-y`} rows={2} value={form.activityBefore ?? ''} onChange={text('activityBefore')} placeholder="e.g. Clearing a jam on press 4 with the guard open" />
|
|
</Field>
|
|
<Field label="What happened? How did the injury occur?" optional>
|
|
<textarea className={`${field} resize-y`} rows={3} value={form.whatHappened ?? ''} onChange={text('whatHappened')} />
|
|
</Field>
|
|
<div className="grid gap-x-4 sm:grid-cols-2">
|
|
<Field label="Physician or health care professional" optional>
|
|
<input className={field} value={form.physicianName ?? ''} onChange={text('physicianName')} />
|
|
</Field>
|
|
<Field label="Treatment facility (name and address)" optional>
|
|
<input className={field} value={form.treatmentFacility ?? ''} onChange={text('treatmentFacility')} />
|
|
</Field>
|
|
</div>
|
|
<Check label="Treated in an emergency room" checked={form.treatedInEmergencyRoom} onChange={(v) => set('treatedInEmergencyRoom', v)} />
|
|
</Group>
|
|
|
|
<Group title="Employee details (Form 301)" help="Kept confidential. Never sent to OSHA with names; the online case submission uses dates only.">
|
|
<div className="grid gap-x-4 sm:grid-cols-3">
|
|
<Field label="Date of birth" optional>
|
|
<input className={field} type="date" value={form.dateOfBirth ?? ''} onChange={text('dateOfBirth')} />
|
|
</Field>
|
|
<Field label="Date hired" optional>
|
|
<input className={field} type="date" value={form.dateOfHire ?? ''} onChange={text('dateOfHire')} />
|
|
</Field>
|
|
<Field label="Sex" optional>
|
|
<select className={field} value={form.sex} onChange={text('sex')}>
|
|
<option value="">Not recorded</option>
|
|
<option value="M">Male</option>
|
|
<option value="F">Female</option>
|
|
</select>
|
|
</Field>
|
|
</div>
|
|
<div className="grid gap-x-4 sm:grid-cols-[1fr_180px]">
|
|
<Field label="Home address" optional>
|
|
<input className={field} value={form.employeeAddress ?? ''} onChange={text('employeeAddress')} />
|
|
</Field>
|
|
<Field label="Time they began work" optional>
|
|
<input className={field} type="time" value={form.timeStartedWork ?? ''} onChange={text('timeStartedWork')} />
|
|
</Field>
|
|
</div>
|
|
</Group>
|
|
|
|
<Group title="Privacy (1904.29)">
|
|
<Check label="Privacy case: show “Privacy case” instead of the name on the 300 Log" checked={form.privacyCase} onChange={(v) => set('privacyCase', v)} />
|
|
<details className="text-3xs text-muted">
|
|
<summary className="cursor-pointer text-coral">When is a case a privacy case?</summary>
|
|
<ul className="mt-1 list-disc space-y-0.5 pl-4">
|
|
{PRIVACY_CASES.map((item) => (
|
|
<li key={item}>{item}</li>
|
|
))}
|
|
</ul>
|
|
</details>
|
|
</Group>
|
|
|
|
<ErrorMessage error={mutation.error} />
|
|
<div className="flex justify-end gap-2">
|
|
<button type="button" className={secondaryButton} onClick={onDone}>
|
|
Cancel
|
|
</button>
|
|
<button type="submit" className={primaryButton} disabled={mutation.isPending}>
|
|
{existing ? 'Save case' : 'Add person'}
|
|
</button>
|
|
</div>
|
|
{existing?.recordable && (
|
|
<Link to={`/osha/cases/${existing.id}/301`} className={textButton}>
|
|
<Printer size={13} /> Form 301
|
|
</Link>
|
|
)}
|
|
</form>
|
|
)
|
|
}
|