From 7aaed608604e60d003cadc549274fd57451d7972 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 31 Aug 2026 17:14:34 +0200 Subject: [PATCH] feat(stock): persist pricing draft to localStorage --- src/app/(app)/stock/page.test.tsx | 24 ++++++++++++++++++++++++ src/app/(app)/stock/page.tsx | 26 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/app/(app)/stock/page.test.tsx b/src/app/(app)/stock/page.test.tsx index eaf9dd0..0e74b17 100644 --- a/src/app/(app)/stock/page.test.tsx +++ b/src/app/(app)/stock/page.test.tsx @@ -379,6 +379,30 @@ describe("StockListPage", () => { }); }); + describe("Stock list — localStorage draft persistence", () => { + beforeEach(() => { + _resetState(); + _state.items = [ + { id: "aaaaaaaaaaaaaaa", sku: "B-001", name: "M10 Hex Bolt", selling_price: 0, cost_price: 0, markup: 0, current_qty: 5, min_qty: 0, is_active: true, supplier: "" }, + ]; + window.localStorage.clear(); + }); + afterEach(() => cleanup()); + + it("survives a page remount via localStorage", async () => { + const { unmount } = render(); + await screen.findByText("M10 Hex Bolt"); + fireEvent.click(screen.getByRole("button", { name: /Edit pricing/i })); + const costInput = await screen.findByLabelText(/Cost for B-001/i); + fireEvent.change(costInput, { target: { value: "1.23" } }); + unmount(); + render(); + await screen.findByText("M10 Hex Bolt"); + fireEvent.click(screen.getByRole("button", { name: /Edit pricing/i })); + expect(await screen.findByText(/Apply changes/i)).toBeInTheDocument(); + }); + }); + describe("Stock list — missing-pricing-only filter", () => { beforeEach(() => { _resetState(); diff --git a/src/app/(app)/stock/page.tsx b/src/app/(app)/stock/page.tsx index 3c8ad60..6debc83 100644 --- a/src/app/(app)/stock/page.tsx +++ b/src/app/(app)/stock/page.tsx @@ -156,6 +156,27 @@ export default function StockListPage() { window.localStorage.setItem("wfdash.stock.showPricing", String(showPricingColumns)); }, [showPricingColumns]); + // Rehydrate draft from localStorage on mount. + useEffect(() => { + if (typeof window === "undefined") return; + try { + const raw = window.localStorage.getItem("wfdash.stock.pricingDraft"); + if (raw) setPricingDraft(JSON.parse(raw)); + } catch { + // ignore corrupt draft + } + }, []); + + // Persist draft every change. + useEffect(() => { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem("wfdash.stock.pricingDraft", JSON.stringify(pricingDraft)); + } catch { + // localStorage quota — best-effort + } + }, [pricingDraft]); + // Debounce the search input → 250ms after the last keystroke. Same // pattern as /orders and /quotes — the operator is hunting for a // match, not paging through. @@ -250,6 +271,11 @@ export default function StockListPage() { Object.entries(pricingDraft).filter(([id]) => failedIds.has(id)), ); setPricingDraft(failedDraftEntries); + if (failedIds.size === 0) { + window.localStorage.removeItem("wfdash.stock.pricingDraft"); + } else { + window.localStorage.setItem("wfdash.stock.pricingDraft", JSON.stringify(failedDraftEntries)); + } setPricingApplyState(failedIds.size > 0 ? "partial" : "applied"); toast.success( failedIds.size > 0