feat(stock): beforeunload warning when pricing draft is dirty

This commit is contained in:
Dave 2026-08-31 17:15:42 +02:00
parent 7aaed60860
commit 9f27e9d4f1
2 changed files with 38 additions and 0 deletions

View file

@ -428,4 +428,25 @@ describe("StockListPage", () => {
expect(await screen.findByText("Missing both")).toBeInTheDocument(); expect(await screen.findByText("Missing both")).toBeInTheDocument();
}); });
}); });
describe("Stock list — beforeunload while dirty", () => {
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("registers a beforeunload listener when there are dirty rows", async () => {
const addSpy = vi.spyOn(window, "addEventListener");
render(<Page />);
await screen.findByText("M10 Hex Bolt"); // wait for items
fireEvent.click(screen.getByRole("button", { name: /Edit pricing/i }));
const costInput = await screen.findByLabelText(/Cost for B-001/i);
fireEvent.change(costInput, { target: { value: "5" } });
expect(addSpy).toHaveBeenCalledWith("beforeunload", expect.any(Function));
});
});
}); });

View file

@ -177,6 +177,23 @@ export default function StockListPage() {
} }
}, [pricingDraft]); }, [pricingDraft]);
// Warn the operator before they navigate away with unsaved pricing
// changes. Only registered when edit mode is on AND there's actually
// something in the draft — the bar already gates the apply button
// on the same condition, so a Cmd-W / tab close here would silently
// lose the work otherwise.
useEffect(() => {
if (typeof window === "undefined") return;
const isDirty = Object.keys(pricingDraft).length > 0 && editPricing;
if (!isDirty) return;
const handler = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue = "";
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, [pricingDraft, editPricing]);
// Debounce the search input → 250ms after the last keystroke. Same // Debounce the search input → 250ms after the last keystroke. Same
// pattern as /orders and /quotes — the operator is hunting for a // pattern as /orders and /quotes — the operator is hunting for a
// match, not paging through. // match, not paging through.