fix(stock-quick-draft): new-SKU supplier picker reachable, closure guard, submit test

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-07-30 22:57:17 +02:00
parent 75342ed88c
commit 1b7552ffd2
2 changed files with 84 additions and 4 deletions

View file

@ -291,4 +291,48 @@ describe("QuickDraftModal — add & next", () => {
// Search region is hidden in new-SKU mode.
expect(screen.queryByTestId("quick-draft-search")).not.toBeInTheDocument();
});
it("new-SKU path POSTs newStockItem and no stockItemId", async () => {
render(<QuickDraftModal open onOpenChange={() => {}} />);
fireEvent.change(screen.getByTestId("quick-draft-search"), {
target: { value: "cutting" },
});
await act(async () => {
await vi.advanceTimersByTimeAsync(250);
});
fireEvent.click(screen.getByText(/115mm Cutting Disc/));
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
fireEvent.click(screen.getByTestId("quick-draft-toggle-new-sku"));
fireEvent.change(screen.getByTestId("quick-draft-new-sku-name"), {
target: { value: "115mm Cutting Disc" },
});
fireEvent.change(screen.getByTestId("quick-draft-new-sku-sku"), {
target: { value: "STK-CD-115" },
});
fireEvent.click(screen.getByLabelText(/Steel & Pipe Co/));
fireEvent.change(screen.getByTestId("quick-draft-qty"), {
target: { value: "50" },
});
fireEvent.change(screen.getByTestId("quick-draft-cost"), {
target: { value: "20" },
});
fireEvent.click(screen.getByTestId("quick-draft-add"));
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
const addCalls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls.filter(
(c) => typeof c[0] === "string" && c[0].includes("/add-line"),
);
expect(addCalls.length).toBeGreaterThan(0);
const [, init] = addCalls[0];
const body = JSON.parse(init.body);
expect(body.newStockItem).toEqual({ name: "115mm Cutting Disc", sku: "STK-CD-115" });
expect(body.stockItemId).toBeUndefined();
expect(body.supplierId).toBe("sup-1");
expect(body.quantity).toBe(50);
expect(body.costPrice).toBe(20);
});
});

View file

@ -15,9 +15,7 @@ import type {
* Quick-Draft Modal — item-first stock-order entry on /stock-orders.
*
* See docs/superpowers/specs/2026-07-30-stock-quick-draft-design.md
* for the full design. This file is the modal in its entirety; the
* three regions (search, cards, footer) live in sub-components
* further down so the test surface stays manageable.
* for the full design.
*
* Data flow:
* - search & history hit /api/v1/stock-quick-draft/{search,supplier-history}
@ -57,6 +55,7 @@ export function QuickDraftModal({ open, onOpenChange }: QuickDraftModalProps) {
const [supplierChoice, setSupplierChoice] = useState<string | null>(null);
const [qty, setQty] = useState("");
const [cost, setCost] = useState("");
// New-SKU toggle state (named for readability; plan uses isNewSku/newName/newSku)
const [newSkuMode, setNewSkuMode] = useState(false);
const [newSkuName, setNewSkuName] = useState("");
const [newSkuSku, setNewSkuSku] = useState("");
@ -147,6 +146,7 @@ export function QuickDraftModal({ open, onOpenChange }: QuickDraftModalProps) {
};
const onAddAndNext = async () => {
if (submitting) return; // closure-time guard against double-click races
if (!canSubmit) return;
setSubmitting(true);
try {
@ -317,7 +317,7 @@ export function QuickDraftModal({ open, onOpenChange }: QuickDraftModalProps) {
+ New SKU not in stock yet
</button>
) : (
<div className="space-y-2 rounded-md border bg-muted/20 p-3">
<div className="space-y-3 rounded-md border bg-muted/20 p-3">
<div>
<Label htmlFor="quick-draft-new-sku-name">Name</Label>
<Input
@ -336,6 +336,42 @@ export function QuickDraftModal({ open, onOpenChange }: QuickDraftModalProps) {
onChange={(e) => setNewSkuSku(e.target.value)}
/>
</div>
{historyLoading && (
<div className="text-xs text-muted-foreground">
Loading supplier history…
</div>
)}
{!historyLoading && history.length > 0 && (
<div className="text-xs space-y-1" role="radiogroup">
{history.map((entry) => (
<label
key={entry.supplierId}
className="flex cursor-pointer items-center justify-between gap-2"
>
<span className="flex items-center gap-2">
<input
type="radio"
name="quick-draft-supplier"
value={entry.supplierId}
checked={supplierChoice === entry.supplierId}
onChange={() => setSupplierChoice(entry.supplierId)}
className="size-4 accent-primary"
/>
<span>{entry.supplierName}</span>
</span>
<span className="text-muted-foreground">
last paid: <strong>R {entry.lastCost.toFixed(2)}</strong>
</span>
</label>
))}
</div>
)}
{!historyLoading && history.length === 0 && (
<div className="text-xs text-muted-foreground">
Pick a supplier — Task 10 will add a full supplier dropdown
when there&apos;s no history.
</div>
)}
<button
type="button"
className="text-xs underline text-muted-foreground"