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,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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user