feat(sp6): Lane component with multi-select

This commit is contained in:
Tyler
2026-06-20 18:40:34 -06:00
parent 713f0932b2
commit 5f3339ac1f
2 changed files with 159 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen, fireEvent } from "@testing-library/react";
import { Lane } from "./Lane";
import type { InboxClaimRow } from "@/lib/inbox-api";
afterEach(() => cleanup());
const sampleRows: InboxClaimRow[] = [
{
id: "CLP-1",
kind: "claim",
patient_control_number: "PCN-1",
charge_amount: 100,
payer_id: "PAYER-A",
provider_npi: "1234567890",
state: "rejected",
rejection_reason: "R69",
rejected_at: "2026-06-20T09:00:00Z",
service_date_from: null,
score: null,
score_tier: null,
score_breakdown: null,
},
];
describe("Lane", () => {
it("renders the lane name and row count", () => {
const { container } = render(
<Lane name="REJECTED" accent="oxblood" rows={sampleRows} onRowClick={() => {}} />,
);
expect(container.textContent).toContain("REJECTED");
expect(container.textContent).toContain("1");
});
it("renders an empty state when no rows", () => {
const { container } = render(
<Lane name="DONE" accent="muted" rows={[]} onRowClick={() => {}} />,
);
expect(container.textContent).toMatch(/nothing here/i);
});
it("supports multi-select via row checkbox toggle", () => {
const onSelectionChange = vi.fn();
render(
<Lane
name="REJECTED"
accent="oxblood"
rows={sampleRows}
onRowClick={() => {}}
onSelectionChange={onSelectionChange}
/>,
);
const cb = screen.getByRole("checkbox");
fireEvent.click(cb);
expect(onSelectionChange).toHaveBeenCalled();
});
});
+101
View File
@@ -0,0 +1,101 @@
import { useState } from "react";
import { InboxRow, type Accent } from "./InboxRow";
import type { InboxClaimRow, InboxCandidateRow } from "@/lib/inbox-api";
export type LaneRow = InboxClaimRow | InboxCandidateRow;
export function Lane({
name,
accent,
rows,
onRowClick,
onSelectionChange,
}: {
name: string;
accent: Accent;
rows: LaneRow[];
onRowClick: (row: LaneRow) => void;
onSelectionChange?: (ids: string[]) => void;
}) {
const [selected, setSelected] = useState<Set<string>>(new Set());
function rowId(row: LaneRow): string {
return row.kind === "remit" ? (row as InboxCandidateRow).payer_claim_control_number : row.id;
}
function toggle(id: string) {
const next = new Set(selected);
if (next.has(id)) next.delete(id);
else next.add(id);
setSelected(next);
onSelectionChange?.(Array.from(next));
}
return (
<section
className="flex-1 min-w-[280px] flex flex-col"
style={{
background: "var(--tt-bg-elev)",
border: "1px solid var(--tt-bg)",
borderRadius: 4,
}}
data-lane={name.toLowerCase()}
>
<header
className="px-3 py-2 flex items-baseline justify-between"
style={{ borderBottom: "1px solid var(--tt-bg)" }}
>
<h2
className="font-mono uppercase tracking-widest"
style={{ color: "var(--tt-ink)", fontSize: 11, letterSpacing: "0.12em" }}
>
{name}
</h2>
<span
className="font-mono tabular-nums"
style={{ color: "var(--tt-amber)", fontSize: 18 }}
>
{rows.length}
</span>
</header>
{rows.length === 0 ? (
<p
className="p-4 font-mono"
style={{ color: "var(--tt-muted)", fontSize: 11 }}
>
Nothing here.
</p>
) : (
<div className="w-full" style={{ fontSize: 12 }}>
{rows.map((row) => {
const id = rowId(row);
return (
<div
key={id}
className="flex items-stretch"
style={{ borderBottom: "1px solid var(--tt-bg)" }}
>
<label
className="flex items-center justify-center"
style={{ width: 28, padding: "0 4px 0 8px" }}
>
<input
type="checkbox"
aria-label={`Select ${id}`}
checked={selected.has(id)}
onChange={() => toggle(id)}
onClick={(e) => e.stopPropagation()}
/>
</label>
<div style={{ width: "100%" }}>
<InboxRow row={row} accent={accent} onClick={() => onRowClick(row)} />
</div>
</div>
);
})}
</div>
)}
</section>
);
}