Add OSHA Form 301 and Osha page components
- 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.
This commit is contained in:
1 parent
3e964d1176
commit
61c876edec
59 files changed
+9180
-183
No files matched your search
@@ -0,0 +1,442 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user