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,65 @@
|
||||
import { useState } from 'react'
|
||||
import { Tooltip } from './ChartFrame'
|
||||
import { useWidth } from './useWidth'
|
||||
|
||||
const STEPS = ['var(--color-seq-100)', 'var(--color-seq-200)', 'var(--color-seq-300)', 'var(--color-seq-400)', 'var(--color-seq-500)', 'var(--color-seq-600)', 'var(--color-seq-700)']
|
||||
|
||||
/** Grid of counts in one hue, light to dark; empty cells stay near the surface. */
|
||||
export function Heatmap({ grid, rowLabels, colLabels, ariaLabel, describe }: { grid: number[][]; rowLabels: string[]; colLabels: string[]; ariaLabel: string; describe: (row: number, col: number, value: number) => string }) {
|
||||
const [ref, width] = useWidth<HTMLDivElement>()
|
||||
const [hover, setHover] = useState<{ r: number; c: number } | null>(null)
|
||||
const max = Math.max(1, ...grid.flat())
|
||||
const step = (value: number) => (value === 0 ? 'var(--color-line-soft)' : STEPS[Math.min(STEPS.length - 1, Math.floor((value / max) * (STEPS.length - 1)))])
|
||||
const labelW = 34
|
||||
const cell = Math.max(10, Math.floor((width - labelW) / colLabels.length))
|
||||
const height = rowLabels.length * cell + 18
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative" onPointerLeave={() => setHover(null)}>
|
||||
<svg width={width} height={height} role="img" aria-label={ariaLabel} className="block">
|
||||
{rowLabels.map((label, r) => (
|
||||
<text key={label} x={labelW - 6} y={r * cell + cell / 2} dy="0.32em" textAnchor="end" fontSize={10} fill="var(--color-chart-label)">
|
||||
{label}
|
||||
</text>
|
||||
))}
|
||||
{colLabels.map((label, c) =>
|
||||
c % 3 === 0 ? (
|
||||
<text key={c} x={labelW + c * cell + cell / 2} y={rowLabels.length * cell + 12} textAnchor="middle" fontSize={10} fill="var(--color-chart-label)">
|
||||
{label}
|
||||
</text>
|
||||
) : null,
|
||||
)}
|
||||
{grid.map((row, r) =>
|
||||
row.map((value, c) => (
|
||||
<rect
|
||||
key={`${r}-${c}`}
|
||||
x={labelW + c * cell + 1}
|
||||
y={r * cell + 1}
|
||||
width={cell - 2}
|
||||
height={cell - 2}
|
||||
rx={2}
|
||||
fill={step(value)}
|
||||
stroke={hover?.r === r && hover.c === c ? 'var(--color-ink)' : 'none'}
|
||||
strokeWidth={1.5}
|
||||
tabIndex={value ? 0 : -1}
|
||||
aria-label={describe(r, c, value)}
|
||||
onPointerEnter={() => setHover({ r, c })}
|
||||
onFocus={() => setHover({ r, c })}
|
||||
className="outline-none"
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</svg>
|
||||
<div className="mt-2 flex items-center gap-2 text-3xs text-muted" aria-hidden>
|
||||
<span>Fewer</span>
|
||||
{STEPS.map((color) => (
|
||||
<i key={color} className="h-2.5 w-5 rounded-[2px]" style={{ background: color }} />
|
||||
))}
|
||||
<span>More ({max} max)</span>
|
||||
</div>
|
||||
{hover && (
|
||||
<Tooltip x={labelW + hover.c * cell + cell} y={hover.r * cell} containerWidth={width} title={describe(hover.r, hover.c, grid[hover.r][hover.c]).split(':')[0]} rows={[{ label: grid[hover.r][hover.c] === 1 ? 'incident' : 'incidents', value: String(grid[hover.r][hover.c]) }]} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user