- 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.
106 lines
4.3 KiB
TypeScript
106 lines
4.3 KiB
TypeScript
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>
|
|
}
|