feat(sp6): Lane component with multi-select
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user