Add OSHA Form 301 and Osha page components
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s

- 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:
jayjennings committed 2026-10-06 13:09:57 -07:00
1 parent 3e964d1176
commit 61c876edec
59 files changed
+9180 -183

No files matched your search

+26 -1
View File
@@ -138,12 +138,37 @@ The token goes in the `X-Kiosk-Token` header. Browsers can't send custom headers
The web app opens links at `/k/<token>`, optionally with `?site=<id>&area=<id>` to pre-fill the location (as QR codes do).
## OSHA recordkeeping
All endpoints need **safety_manager**: they hold confidential medical information. Other roles only see `injuredCount` on an incident. Rules are in [`shared/osha.ts`](../shared/osha.ts); section numbers refer to 29 CFR 1904.
| Method | Path | Notes |
| --- | --- | --- |
| GET | `/api/osha/incidents/:number/cases` | Injured or ill people on an incident |
| POST | `/api/osha/incidents/:number/cases` | `{ name, ...answers }`. Case number is the incident reference, with `-2`, `-3`… for further people |
| PATCH | `/api/osha/cases/:id` | Any case field. `recordable` and `outcome` are always recalculated from the answers; days away + restricted may not exceed 180 |
| DELETE | `/api/osha/cases/:id` | |
| GET | `/api/osha/cases/:id/301` | Data for Form 301 |
| GET | `/api/osha/log?siteId=&year=` | 300 Log rows, 300A totals, monthly hours, annual average employees, TRIR/DART/LTIR, ITA checks, missing establishment fields |
| GET/PUT | `/api/osha/sites/:id/hours?year=` | Monthly `{ month, hours, averageEmployees }`; `null` for both clears a month |
| GET | `/api/osha/ita/summary.csv?year=` | ITA Establishment and 300A file, one row per active site |
| GET | `/api/osha/ita/cases.csv?year=` | ITA case data (Forms 300/301) file, without names |
**Case answers:** `employmentType`, `workRelated`, `newCase`, `died`, `dateOfDeath`, `daysAwayFromWork`, `daysAway`, `restrictedOrTransfer`, `daysRestricted`, `medicalTreatment`, `lossOfConsciousness`, `significantDiagnosis`, `specialCase` (`none`, `needlestick`, `medical_removal`, `hearing_loss`, `tuberculosis`), `caseType` (300 column M), `privacyCase`, severe-injury flags (`inpatientHospitalization`, `amputation`, `lossOfEye`, `reportedToOshaAt`) and the Form 301 fields (`jobTitle`, `natureOfInjury`, `bodyPart`, `objectSubstance`, `activityBefore`, `whatHappened`, `dateOfBirth`, `dateOfHire`, `sex`, `employeeAddress`, `physicianName`, `treatmentFacility`, `treatedInEmergencyRoom`, `timeStartedWork`).
Responses include `severeReport` (`{ withinHours, reasons, dueBy }`) when 1904.39 requires a direct report, and `missingFields` listing what Form 301 and the ITA still need. A case newly needing a direct report emails safety managers and admins.
Site establishment fields (`streetAddress`, `city`, `state`, `zip`, `naicsCode`, `industryDescription`, `ein`, `establishmentType`) are set with `PATCH /api/sites/:id`.
## Stats
| Method | Path | Who | Notes |
| --- | --- | --- | --- |
| GET | `/api/stats/overview` | signed in | Open incidents, actions due/overdue, average close time, status counts, weekly trend (12 weeks), incidents by site this year |
| GET | `/api/stats/analytics` | signed in | `?days=` (7–730, default 90): counts by type and severity, top root-cause category, on-time action rate, monthly completions |
| GET | `/api/stats/analytics` | signed in | `?from=YYYY-MM-DD&to=YYYY-MM-DD` (up to 3 years), optional `siteId`, `type`. Leading/lagging KPIs with the previous period, trend (weekly up to ~3 months, else monthly), locations, root causes, weekday × hour heatmap, actions on time vs late, and for supervisors+ rolling 12-month TRIR/DART and yearly rates |
| GET | `/api/views?page=analytics\|incidents` | signed in | Your saved views plus shared ones |
| POST | `/api/views` | signed in | `{ page, name, filters, shared }`. Sharing needs supervisor |
| DELETE | `/api/views/:id` | owner or admin | |
Stats only count what the signed-in user can see.
+18 -6
View File
@@ -20,6 +20,9 @@ This is a living document. Update it as decisions are made or scope changes.
| Reporting without accounts | "Reporting links": revocable, rate-limited tokens that can only file reports; used for kiosk tablets and QR codes | Decided |
| Languages | English + Spanish with typed message files; worker-facing screens first, manager screens later (see TODO) | Decided |
| Offline | Installable PWA; reports queued in IndexedDB and retried with a client-generated id so retries never duplicate | Decided |
| OSHA rules | One shared rules module (`shared/osha.ts`) used by API and UI; printable forms are OSHA-permitted equivalents (1904.29(b)(4)); ITA exports follow OSHA's published CSV specs | Decided |
| Medical privacy | Injured-person and medical details visible to safety managers and admins only; timeline and case CSV carry no names | Decided |
| Charts | Hand-built SVG components (no chart library) following the dataviz method: validated color slots, one axis, hairline grid, hover tooltips, a table view on every chart | Decided |
| CI | Gitea Actions (`.gitea/workflows`): lint, typecheck, unit tests, build, e2e | Decided |
| Product name | Open SafetyLog EHS (replaced placeholder "IA Tracker" and the prototype's "Safesite" branding) | Decided |
@@ -74,15 +77,24 @@ This is a living document. Update it as decisions are made or scope changes.
## Phase 4: Compliance (US OSHA)
- Recordability wizard; days away / restricted / transferred tracking
- OSHA 300, 300A and 301 form generation; privacy-case handling
- ITA (Injury Tracking Application) CSV export
- Hours worked per site per month; TRIR, DART and lost-time rates
- [x] Injured/ill people per incident with a live recordability check (1904.4–1904.11): covered worker, work-related, new case, general criteria, special cases; most-serious-outcome classification; 14-item first-aid list and privacy-case rules shown in context
- [x] Day counts with the 180-day cap; days only counted when the matching outcome applies
- [x] Severe injury reporting (1904.39): 8-hour/24-hour deadline banner, email alert to safety managers, "reported to OSHA at" record
- [x] Form 300 Log (landscape print), Form 300A Summary with establishment details and certification block, printable Form 301 per case
- [x] Establishment details per site (address, NAICS, EIN, type) with validation
- [x] Monthly hours worked and headcount; TRIR, DART and LTIR
- [x] ITA CSV exports for the 300A summary and 300/301 case data (no names), plus OSHA's own upload checks run in the app
- [x] Tests: rules module unit tests, API tests incl. exact CSV output, browser test of the full recordkeeping flow
## Phase 5: Analytics
- Trends, location breakdown, top root causes, corrective-action closure time
- Leading vs lagging indicators; filters and saved dashboards
- [x] One filter row (date range presets or custom, site, incident type) scoping every number and chart on the page
- [x] Leading indicators: hazards & near misses reported, near misses per injury, triaged within 24 hours, actions completed on time; each compared with the previous period
- [x] Lagging indicators: injuries, OSHA-recordable cases, rolling 12-month TRIR/DART, average days to close; OSHA rates by calendar year
- [x] Charts: reports per week/month by kind, rolling OSHA rates, locations (sites, or areas within a site), root causes, weekday × hour heatmap, actions completed on time vs late
- [x] Saved views (private, or shared by supervisors and above) on Analytics and the Incidents list
- [x] Demo seed with a year of realistic history (deterministic)
- [x] Tests: analytics computations (periods, groupings, rates, scoping), saved views, chart scale helper, browser tests
## Phase 6: Release v1.0
+19
View File
@@ -31,6 +31,25 @@ Open items that aren't tied to a single roadmap phase. Tick them off (or delete
- [ ] **Have Spanish reviewed by a native speaker** familiar with plant safety terms (e.g. "casi accidente" for near miss, "planta" for site).
- [ ] **Offline incident list.** Only reporting works offline; viewing incidents and actions needs a connection.
## Phase 4 follow-ups
- [ ] **Expert review of the OSHA rules.** `shared/osha.ts` encodes 29 CFR 1904 as plainly as possible with section references, but it should be reviewed by a recordkeeping professional before anyone relies on it. The app should say clearly that it assists, and the employer remains responsible.
- [ ] **Deadline reminders.** Email safety managers in January to certify and post the 300A (Feb 1–Apr 30) and before the ITA submission deadline (March 2).
- [ ] **Direct ITA submission.** OSHA offers an API for the Injury Tracking Application; submitting directly would remove the CSV download/upload step.
- [ ] **Day counting from dates.** Calculate days away/restricted from start and return-to-work dates instead of typed counts.
- [ ] **Employee access (1904.35).** Employees and their representatives may request the 300 Log (and their own 301). Add a way to produce those copies with privacy cases removed.
- [ ] **OSHA rates on the dashboard.** Show TRIR/DART on Overview and Analytics, and trends over years.
- [ ] **Translate the OSHA pages** along with the other manager screens.
- [ ] **Other regions.** The data model keeps outcomes and case types separate from US-specific forms; UK RIDDOR or Canadian provincial rules could be added as further rule sets.
## Phase 5 follow-ups
- [ ] **Industry benchmarks.** Show the BLS average TRIR/DART for the site's NAICS code next to its own rates.
- [ ] **Scheduled reports.** A weekly or monthly email digest of the indicators for safety managers.
- [ ] **Chart export.** Download a chart as an image, or print the whole Analytics page as a report (print styles are in place for most of it).
- [ ] **Dark mode.** The app is light-only; if a dark theme is added, the chart colors need their dark-mode steps validated against the dark surface.
- [ ] **Translate Analytics** with the other manager screens.
## Done
- [x] Docker verified 2026-10-06 (Docker 29.8, Compose v5.5): image builds (~315 MB), `docker compose up` with PostgreSQL 18 passes setup → sign-in → report smoke test; data and sessions survive `down`/`up`; clean shutdown; healthcheck healthy; runs as non-root; standalone PGlite container works; `Secure` cookie with an `https://` PUBLIC_URL; web app loads with no errors.