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,122 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { assessRecordability, capDays, FIRST_AID, incidenceRate, itaSizeCode, severeInjuryReport, summaryChecks, type RecordabilityInput, type SummaryTotals } from './osha.ts'
|
||||
|
||||
const base: RecordabilityInput = {
|
||||
employmentType: 'employee',
|
||||
workRelated: true,
|
||||
newCase: true,
|
||||
died: false,
|
||||
daysAwayFromWork: false,
|
||||
restrictedOrTransfer: false,
|
||||
medicalTreatment: false,
|
||||
lossOfConsciousness: false,
|
||||
significantDiagnosis: false,
|
||||
specialCase: 'none',
|
||||
}
|
||||
const assess = (overrides: Partial<RecordabilityInput>) => assessRecordability({ ...base, ...overrides })
|
||||
|
||||
describe('assessRecordability', () => {
|
||||
it('first aid only is not recordable', () => {
|
||||
expect(assess({})).toMatchObject({ status: 'not_recordable', outcome: null })
|
||||
})
|
||||
|
||||
it('waits for the work-related and new-case answers', () => {
|
||||
expect(assess({ workRelated: null, medicalTreatment: true }).status).toBe('incomplete')
|
||||
expect(assess({ newCase: null, medicalTreatment: true }).status).toBe('incomplete')
|
||||
})
|
||||
|
||||
it('excludes non-work-related cases and recurrences', () => {
|
||||
expect(assess({ workRelated: false, died: true }).status).toBe('not_recordable')
|
||||
expect(assess({ newCase: false, medicalTreatment: true }).reasons[0]).toMatch(/1904\.6/)
|
||||
})
|
||||
|
||||
it('keeps contractors and visitors off your log', () => {
|
||||
expect(assess({ employmentType: 'contractor', daysAwayFromWork: true }).reasons[0]).toMatch(/1904\.31/)
|
||||
expect(assess({ employmentType: 'visitor', died: true }).status).toBe('not_recordable')
|
||||
expect(assess({ employmentType: 'temporary', medicalTreatment: true }).status).toBe('recordable')
|
||||
})
|
||||
|
||||
it('classifies by the most serious outcome', () => {
|
||||
expect(assess({ medicalTreatment: true }).outcome).toBe('other')
|
||||
expect(assess({ medicalTreatment: true, restrictedOrTransfer: true }).outcome).toBe('restricted')
|
||||
expect(assess({ restrictedOrTransfer: true, daysAwayFromWork: true }).outcome).toBe('days_away')
|
||||
expect(assess({ daysAwayFromWork: true, died: true }).outcome).toBe('death')
|
||||
})
|
||||
|
||||
it('records each general criterion on its own', () => {
|
||||
for (const key of ['lossOfConsciousness', 'significantDiagnosis', 'medicalTreatment'] as const) {
|
||||
expect(assess({ [key]: true })).toMatchObject({ status: 'recordable', outcome: 'other' })
|
||||
}
|
||||
})
|
||||
|
||||
it('records special cases even without general criteria', () => {
|
||||
for (const specialCase of ['needlestick', 'medical_removal', 'hearing_loss', 'tuberculosis'] as const) {
|
||||
expect(assess({ specialCase }).status).toBe('recordable')
|
||||
}
|
||||
expect(assess({ specialCase: 'needlestick' }).reasons[0]).toMatch(/1904\.8/)
|
||||
})
|
||||
|
||||
it('lists every reason that applies', () => {
|
||||
expect(assess({ medicalTreatment: true, lossOfConsciousness: true }).reasons).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('severeInjuryReport', () => {
|
||||
it('requires fatalities within 8 hours and other severe injuries within 24', () => {
|
||||
expect(severeInjuryReport({ died: true, inpatientHospitalization: true, amputation: false, lossOfEye: false })).toEqual({ withinHours: 8, reasons: ['work-related fatality'] })
|
||||
expect(severeInjuryReport({ died: false, inpatientHospitalization: false, amputation: true, lossOfEye: true })).toEqual({ withinHours: 24, reasons: ['amputation', 'loss of an eye'] })
|
||||
expect(severeInjuryReport({ died: false, inpatientHospitalization: false, amputation: false, lossOfEye: false })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('capDays', () => {
|
||||
it('stops counting at 180 days in total', () => {
|
||||
expect(capDays(10, 5)).toEqual({ daysAway: 10, daysRestricted: 5 })
|
||||
expect(capDays(150, 60)).toEqual({ daysAway: 150, daysRestricted: 30 })
|
||||
expect(capDays(200, 10)).toEqual({ daysAway: 180, daysRestricted: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('rates and codes', () => {
|
||||
it('computes incidence rates per 200,000 hours', () => {
|
||||
expect(incidenceRate(3, 200_000)).toBe(3)
|
||||
expect(incidenceRate(2, 150_000)).toBe(2.67)
|
||||
expect(incidenceRate(1, 0)).toBeNull()
|
||||
})
|
||||
|
||||
it('maps headcount to ITA size codes', () => {
|
||||
expect([19, 20, 99, 100, 249, 250].map(itaSizeCode)).toEqual([1, 21, 21, 22, 22, 3])
|
||||
})
|
||||
|
||||
it('has the complete 14-item first-aid list', () => {
|
||||
expect(FIRST_AID).toHaveLength(14)
|
||||
})
|
||||
})
|
||||
|
||||
describe('summaryChecks (OSHA ITA validations)', () => {
|
||||
const totals = (overrides: Partial<SummaryTotals> = {}): SummaryTotals => ({
|
||||
deaths: 0,
|
||||
daysAwayCases: 1,
|
||||
restrictedCases: 1,
|
||||
otherCases: 1,
|
||||
daysAway: 5,
|
||||
daysRestricted: 3,
|
||||
byType: { injury: 3, skin_disorder: 0, respiratory: 0, poisoning: 0, hearing_loss: 0, other_illness: 0 },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
it('passes a consistent summary', () => {
|
||||
expect(summaryChecks(totals(), 50, 100_000)).toEqual({ errors: [], warnings: [] })
|
||||
})
|
||||
|
||||
it('catches the cross-field rules OSHA enforces', () => {
|
||||
expect(summaryChecks(totals({ byType: { ...totals().byType, injury: 2 } }), 50, 100_000).errors[0]).toMatch(/add up to 2/)
|
||||
expect(summaryChecks(totals({ daysAway: 0 }), 50, 100_000).errors).toContain('There are days-away cases (H) but no days away (K).')
|
||||
expect(summaryChecks(totals({ restrictedCases: 0, daysAwayCases: 0, otherCases: 3, daysAway: 0 }), 50, 100_000).errors).toContain(
|
||||
'There are restricted days (L) but no days-away or restriction cases (H or I).',
|
||||
)
|
||||
expect(summaryChecks(totals(), 0, 0).errors).toHaveLength(2)
|
||||
expect(summaryChecks(totals(), 10, 100_000).errors[0]).toMatch(/more than the hours in a year/)
|
||||
expect(summaryChecks(totals(), 50, 20_000).warnings[0]).toMatch(/Fewer than 500/)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user