diff --git a/src/app/(app)/invoicing/page.test.tsx b/src/app/(app)/invoicing/page.test.tsx index 73e95e2..d432bb8 100644 --- a/src/app/(app)/invoicing/page.test.tsx +++ b/src/app/(app)/invoicing/page.test.tsx @@ -169,6 +169,7 @@ vi.mock("@/components/auth-provider", () => ({ })); import Page from "./page"; +import { NEW_BOOKING_EVENT } from "@/components/invoicing/stock-booking-section"; describe("InvoicingPage", () => { beforeEach(() => { @@ -1579,6 +1580,42 @@ describe("InvoicingPage", () => { ).toBeInTheDocument(); }); + it("refreshes a previously cached Invoiced list after stock is booked from To Invoice", async () => { + stockBookingsListMock + .mockResolvedValueOnce( + buildPageResult([ + { + id: "sp-old", + purchase_number: "PO-OLD-STK", + booked_at: YESTERDAY_ISO_AT_8AM, + expand: { supplier: { id: "sup-old", suppliername: "Old Supply" } }, + }, + ]), + ) + .mockResolvedValueOnce( + buildPageResult([ + { + id: "sp-new", + purchase_number: "PO-NEW-STK", + booked_at: YESTERDAY_ISO_AT_8AM, + expand: { supplier: { id: "sup-new", suppliername: "New Supply" } }, + }, + ]), + ); + mockedRows.splice(0, mockedRows.length); + + render(); + fireEvent.click(await screen.findByRole("tab", { name: "Invoiced" })); + expect(await screen.findByText("PO-OLD-STK")).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("tab", { name: "To Invoice" })); + window.dispatchEvent(new CustomEvent(NEW_BOOKING_EVENT)); + fireEvent.click(screen.getByRole("tab", { name: "Invoiced" })); + + expect(await screen.findByText("PO-NEW-STK")).toBeInTheDocument(); + expect(stockBookingsListMock).toHaveBeenCalledTimes(2); + }); + it("calls the stock-purchases picking-slip endpoint when the Print picking slip button is clicked", async () => { stockBookingsListMock.mockResolvedValueOnce( buildPageResult([ diff --git a/src/app/(app)/invoicing/page.tsx b/src/app/(app)/invoicing/page.tsx index 15b05e7..59604b0 100644 --- a/src/app/(app)/invoicing/page.tsx +++ b/src/app/(app)/invoicing/page.tsx @@ -1440,9 +1440,10 @@ export default function InvoicingPage() { * * The PB filter is `booked_at != null` — we want every row the * accountant has ever booked, not just today's. The recent-range - * filter is applied client-side via `recentStockBookings` so the - * shared 5-day window (`recentInvoiceRange`) stays consistent with - * the invoices + delivery notes. + * filter is applied client-side so the shared 5-day window + * (`recentInvoiceRange`) stays consistent with invoices and delivery + * notes. Rows are sorted by `booked_at`, never the stock PO's order + * or creation date. */ const loadStockBookings = useCallback(async () => { try { @@ -1473,15 +1474,20 @@ export default function InvoicingPage() { useEffect(() => { if (activeTab !== "invoiced") return; if (stockBookings !== null) return; - void loadStockBookings(); + const handle = window.setTimeout(() => { + void loadStockBookings(); + }, 0); + return () => window.clearTimeout(handle); }, [activeTab, stockBookings, loadStockBookings]); useEffect(() => { const handler = () => { - // Re-fetch only when on the Invoiced tab — if the operator is - // still on To Invoice, the StockBookingSection's own refresh - // path is enough and the next mount of this effect will pick - // up the new row. + // The Invoiced tab may already have a cached list from an earlier + // visit. In that case a booking made while the To Invoice tab is + // active used to be missed: the event was ignored and switching + // back would see a non-null cache and skip its load. Invalidate + // first so the tab always fetches the newly booked PO. + setStockBookings(null); if (activeTab === "invoiced") { void loadStockBookings(); }