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

+101
View File
@@ -0,0 +1,101 @@
import { useState } from 'react'
import { Tooltip } from './ChartFrame'
import { formatNumber, niceTicks } from './scale'
import { useWidth } from './useWidth'
export interface Series {
key: string
label: string
color: string
}
interface Props {
data: { label: string; values: Record<string, number> }[]
series: Series[]
height?: number
ariaLabel: string
}
const PAD = { top: 8, right: 8, bottom: 26, left: 32 }
const GAP = 2 // surface gap between stacked segments
const RADIUS = 4
/** Rounded top on the data end, square at the baseline. */
function columnPath(x: number, y: number, w: number, h: number, roundTop: boolean) {
const r = roundTop ? Math.min(RADIUS, h, w / 2) : 0
return `M${x},${y + h}V${y + r}${r ? `Q${x},${y} ${x + r},${y}` : ''}H${x + w - r}${r ? `Q${x + w},${y} ${x + w},${y + r}` : ''}V${y + h}Z`
}
export function StackedColumns({ data, series, height = 220, ariaLabel }: Props) {
const [ref, width] = useWidth<HTMLDivElement>()
const [hover, setHover] = useState<number | null>(null)
const plotW = width - PAD.left - PAD.right
const plotH = height - PAD.top - PAD.bottom
const totals = data.map((d) => series.reduce((sum, s) => sum + (d.values[s.key] ?? 0), 0))
const ticks = niceTicks(Math.max(...totals, 1))
const max = ticks.at(-1)!
const band = plotW / Math.max(1, data.length)
const barW = Math.min(24, band * 0.6)
const y = (v: number) => PAD.top + plotH - (v / max) * plotH
const labelEvery = Math.ceil(data.length / Math.max(1, Math.floor(plotW / 56)))
return (
<div ref={ref} className="relative" onPointerLeave={() => setHover(null)}>
<svg width={width} height={height} role="img" aria-label={ariaLabel} className="block">
{ticks.map((t) => (
<g key={t}>
<line x1={PAD.left} x2={width - PAD.right} y1={y(t)} y2={y(t)} stroke={t === 0 ? 'var(--color-chart-axis)' : 'var(--color-chart-grid)'} strokeWidth={1} shapeRendering="crispEdges" />
<text x={PAD.left - 6} y={y(t)} dy="0.32em" textAnchor="end" fontSize={10} fill="var(--color-chart-label)" className="tabular-nums">
{formatNumber(t)}
</text>
</g>
))}
{data.map((d, i) => {
const x = PAD.left + i * band + (band - barW) / 2
let base = 0
const visible = series.filter((s) => (d.values[s.key] ?? 0) > 0)
return (
<g key={d.label} opacity={hover === null || hover === i ? 1 : 0.55}>
{visible.map((s, j) => {
const v = d.values[s.key]
const top = y(base + v)
const bottom = y(base)
base += v
const isTop = j === visible.length - 1
const h = Math.max(0, bottom - top - (j > 0 ? GAP : 0))
return <path key={s.key} d={columnPath(x, top, barW, h, isTop)} fill={s.color} />
})}
{(i % labelEvery === 0 || i === data.length - 1) && (
<text x={x + barW / 2} y={height - 8} textAnchor="middle" fontSize={10} fill="var(--color-chart-label)">
{d.label}
</text>
)}
<rect
x={PAD.left + i * band}
y={PAD.top}
width={band}
height={plotH}
fill="transparent"
tabIndex={0}
aria-label={`${d.label}: ${series.map((s) => `${d.values[s.key] ?? 0} ${s.label}`).join(', ')}`}
onPointerEnter={() => setHover(i)}
onFocus={() => setHover(i)}
onBlur={() => setHover(null)}
className="outline-none"
/>
</g>
)
})}
</svg>
{hover !== null && (
<Tooltip
x={PAD.left + hover * band + band / 2}
y={y(totals[hover])}
containerWidth={width}
title={`${data[hover].label} · ${formatNumber(totals[hover])} total`}
rows={[...series].reverse().map((s) => ({ label: s.label, value: formatNumber(data[hover].values[s.key] ?? 0), color: s.color }))}
/>
)}
</div>
)
}