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

+105
View File
@@ -0,0 +1,105 @@
import { Table2 } from 'lucide-react'
import { useState, type ReactNode } from 'react'
import { panel } from '../styles'
export interface LegendItem {
label: string
color: string
shape?: 'rect' | 'line'
}
export interface TableData {
columns: string[]
rows: (string | number)[][]
}
/**
* Card for one chart: title, legend (2+ series), and a table view that shows every value, so
* nothing depends on hovering or on telling colors apart.
*/
export function ChartFrame({ title, subtitle, legend, table, children, className = '' }: { title: string; subtitle?: string; legend?: LegendItem[]; table: TableData; children: ReactNode; className?: string }) {
const [showTable, setShowTable] = useState(false)
return (
<figure className={`${panel} m-0 flex flex-col p-5 ${className}`}>
<div className="mb-3 flex items-start justify-between gap-3">
<div>
<figcaption className="font-display text-[17px] font-semibold text-ink">{title}</figcaption>
{subtitle && <p className="mt-0.5 text-2xs text-muted">{subtitle}</p>}
</div>
<button
type="button"
className={`inline-flex flex-none items-center gap-1 rounded-[5px] border px-2 py-1 text-3xs font-semibold print:hidden ${showTable ? 'border-ink bg-ink text-white' : 'border-line text-muted hover:text-ink'}`}
aria-pressed={showTable}
onClick={() => setShowTable(!showTable)}
>
<Table2 size={12} /> Table
</button>
</div>
{legend && legend.length > 1 && !showTable && (
<ul className="mb-3 flex flex-wrap gap-x-4 gap-y-1 text-2xs text-body" aria-label="Legend">
{legend.map((item) => (
<li key={item.label} className="inline-flex items-center gap-1.5">
{item.shape === 'line' ? <i className="h-0.5 w-3.5 rounded-full" style={{ background: item.color }} /> : <i className="size-2.5 rounded-[2px]" style={{ background: item.color }} />}
{item.label}
</li>
))}
</ul>
)}
{showTable ? <DataTable data={table} /> : children}
</figure>
)
}
export function DataTable({ data }: { data: TableData }) {
return (
<div className="max-h-[320px] overflow-auto">
<table className="w-full border-collapse text-2xs">
<thead className="sticky top-0 bg-white">
<tr>
{data.columns.map((column, i) => (
<th key={column} className={`border-b border-line px-2 py-1.5 font-semibold text-[#586762] ${i ? 'text-right' : 'text-left'}`}>
{column}
</th>
))}
</tr>
</thead>
<tbody>
{data.rows.map((row, r) => (
<tr key={r}>
{row.map((cell, i) => (
<td key={i} className={`border-b border-line-soft px-2 py-1.5 text-body ${i ? 'text-right tabular-nums' : ''}`}>
{typeof cell === 'number' ? cell.toLocaleString('en-US', { maximumFractionDigits: 2 }) : cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
/** Tooltip: value first (strong), series second, keyed with a short line in the series color. */
export function Tooltip({ x, y, title, rows, containerWidth }: { x: number; y: number; title: string; rows: { label: string; value: string; color?: string }[]; containerWidth: number }) {
const flip = x > containerWidth - 180
return (
<div
className="pointer-events-none absolute z-10 min-w-[140px] rounded-md border border-line bg-white px-3 py-2 text-2xs shadow-[0_6px_20px_#1b252322]"
style={{ left: flip ? undefined : x + 12, right: flip ? containerWidth - x + 12 : undefined, top: Math.max(0, y - 10) }}
role="status"
>
<p className="mb-1 text-3xs text-muted">{title}</p>
{rows.map((row) => (
<p key={row.label} className="flex items-center gap-2">
{row.color && <i className="h-0.5 w-3 flex-none rounded-full" style={{ background: row.color }} />}
<strong className="font-semibold text-ink tabular-nums">{row.value}</strong>
<span className="text-body">{row.label}</span>
</p>
))}
</div>
)
}
export function ChartEmpty({ children }: { children: ReactNode }) {
return <div className="grid min-h-[160px] place-items-center px-4 text-center text-2xs text-muted">{children}</div>
}