import { useRef } from "react"; import { XIcon } from "lucide-react"; import { useDataset } from "../hooks/useDataset.ts"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useMoneyFormat } from "../hooks/useMoneyFormat.ts"; import { bulkTotal, isBlankLine, withTrailingBlank, type BulkDraft, type BulkLine, } from "../bulkEntry.ts"; interface Props { draft: BulkDraft; onChange: (next: BulkDraft) => void; } /** * The row list for fast entry: description, amount, note — and nothing else in * the tab order, so Tab walks the fields and only the fields. * * The list always ends in one blank row, appended as soon as the last row is * filled, so Tab flows into a new line with no special case and new rows appear * on touch devices too. That is why there is no "Add line" button. */ export function BulkLines({ draft, onChange }: Props) { const digits = useDataset().settings.digits; const fmt = useMoneyFormat(); const descriptionRefs = useRef>([]); const noteRefs = useRef>([]); function update(index: number, changes: Partial) { onChange( withTrailingBlank({ ...draft, lines: draft.lines.map((line, i) => (i === index ? { ...line, ...changes } : line)), }), ); } function removeLine(index: number) { const remaining = draft.lines.filter((_, i) => i !== index); onChange(withTrailingBlank({ ...draft, lines: remaining })); // Land on the previous row's last field, which is where the user came from. if (index > 0) requestAnimationFrame(() => noteRefs.current[index - 1]?.focus()); } const filledCount = draft.lines.filter((line) => !isBlankLine(line)).length; return (
Tab through the fields; a new line opens as you fill the last one
{draft.lines.map((line, index) => (
{index + 1} { descriptionRefs.current[index] = el; }} className="h-8 flex-1" placeholder="What was it?" value={line.description} onChange={(event) => { const description = event.target.value; update(index, { description }); }} onKeyDown={(event) => { // Backspace in an already-empty description removes the row. if ( event.key === "Backspace" && line.description === "" && draft.lines.length > 1 ) { event.preventDefault(); removeLine(index); } }} /> { const amount = Number(event.target.value) || 0; update(index, { amount }); }} /> { noteRefs.current[index] = el; }} className="h-8 w-40" placeholder="Note (optional)" value={line.note} onChange={(event) => { const note = event.target.value; update(index, { note }); }} /> {/* Was a bare `×` in a 20px-wide box: a text glyph doing an icon's job, at a size nothing could hit. lucide's X is the icon family the dialog's own close button already uses. */}
))}

{filledCount} line{filledCount === 1 ? "" : "s"} ·{" "} {fmt.money(bulkTotal(draft, digits), draft.currency)}

); }