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