diff --git a/specs/product/GLOSSARY.md b/specs/product/GLOSSARY.md
index c89fcbcc0..d04b0beef 100644
--- a/specs/product/GLOSSARY.md
+++ b/specs/product/GLOSSARY.md
@@ -434,6 +434,10 @@ Cancelled`): drafts are fully editable (add/edit/remove lines, cancel);
exist in the status enum for later phases; nothing sets them yet, and only
`Confirmed` orders can be voided.)
+Closing the order panel only hides it; it does not cancel the order. The panel
+stays closed even if an earlier open finishes loading. Use **open** again to
+view the order (#712).
+
**Worker sale allocation policy (#612)** — one farm setting deciding how a
restricted plain **Worker's** confirmation may draw stock: `AssignedFlocksOnly`
(default for every farm) tries the worker's own assigned flocks first, and
diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index e8119e9e3..57b8ecd84 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -2790,7 +2790,8 @@ export const en = {
+ "read-only). The quantity counts units, not eggs — the field is labeled with the "
+ "unit and shows the resulting egg count while you type (2 trays = 60 eggs, not 60 trays). Each "
+ "line remembers how many eggs its unit held when it was added, so redefining a carton later never "
- + "changes old orders.",
+ + "changes old orders. Choose close while an order loads to keep the panel closed; "
+ + "choose open again to view it.",
salesConfirming:
"Confirming an order allocates real stock — oldest lots first — and is the point "
+ "where inventory changes hands.",
@@ -3312,7 +3313,7 @@ export const en = {
"Undo of a mistaken confirm — stock returns to the exact lots it came from. Needs a reason.",
glossaryCancelOrderTerm: "Cancel (order)",
- glossaryCancelOrderDef: "Close a draft that never happened. No stock involved.",
+ glossaryCancelOrderDef: "Close a draft that never happened. No stock involved. The close link only hides the panel; use open to view it again.",
glossaryInventoryItemTerm: "Inventory item",
glossaryInventoryItemDef:
diff --git a/web/src/i18n/es.ts b/web/src/i18n/es.ts
index 35495726e..72c5755c6 100644
--- a/web/src/i18n/es.ts
+++ b/web/src/i18n/es.ts
@@ -2264,7 +2264,8 @@ export const es = {
+ "cuenta unidades, no huevos — el campo indica la unidad y muestra el total de huevos "
+ "resultante mientras escribe (2 bandejas = 60 huevos, no 60 bandejas). Cada línea "
+ "recuerda cuántos huevos contenía su unidad cuando se agregó, así que redefinir un cartón más tarde "
- + "nunca cambia pedidos antiguos.",
+ + "nunca cambia pedidos antiguos. Si el pedido aún se está cargando, cerrar mantiene "
+ + "el panel cerrado. Use abrir de nuevo para verlo.",
salesConfirming:
"Confirmar un pedido asigna existencias reales — los lotes más antiguos primero — y "
+ "es el punto donde el inventario cambia de manos.",
@@ -2808,7 +2809,7 @@ export const es = {
+ "vinieron. Requiere un motivo.",
glossaryCancelOrderTerm: "Cancelar (pedido)",
- glossaryCancelOrderDef: "Cerrar un borrador que nunca se concretó. No involucra existencias.",
+ glossaryCancelOrderDef: "Cerrar un borrador que nunca se concretó. No involucra existencias. El enlace cerrar solo oculta el panel; use abrir para verlo de nuevo.",
glossaryInventoryItemTerm: "Artículo de inventario",
glossaryInventoryItemDef:
diff --git a/web/src/i18n/tl.ts b/web/src/i18n/tl.ts
index 8ec8914c0..25cefc737 100644
--- a/web/src/i18n/tl.ts
+++ b/web/src/i18n/tl.ts
@@ -2351,7 +2351,8 @@ export const tl = {
+ "Ang dami ay bilang ng mga unit, hindi mga itlog — nakasaad sa field ang unit at "
+ "ipinapakita nito ang kabuuang itlog habang nagta-type (2 tray = 60 itlog, hindi 60 tray). "
+ "Naaalala ng bawat linya kung ilang itlog ang laman ng unit nito noong idinagdag ito, kaya hindi "
- + "kailanman binabago ng muling pagtukoy sa isang karton ang mga lumang order.",
+ + "kailanman binabago ng muling pagtukoy sa isang karton ang mga lumang order. Kung naglo-load pa ang order, "
+ + "mananatiling sarado ang panel kapag pinili ang isara. Piliin muli ang buksan para tingnan ang order.",
salesConfirming:
"Ang Pagkumpirma ng isang order ay naglalaan ng aktwal na stock — pinakalumang lote "
+ "muna — at ito ang sandali kung saan lumilipat ang inventory.",
@@ -2905,7 +2906,7 @@ export const tl = {
+ "Kailangan ng dahilan.",
glossaryCancelOrderTerm: "Kanselahin (order)",
- glossaryCancelOrderDef: "Isara ang isang draft na hindi natuloy. Walang kinalaman na stock.",
+ glossaryCancelOrderDef: "Isara ang isang draft na hindi natuloy. Walang kinalaman na stock. Itinatago lang ng isara ang panel; piliin ang buksan para tingnan itong muli.",
glossaryInventoryItemTerm: "Item sa imbentaryo",
glossaryInventoryItemDef:
diff --git a/web/src/routes/SalesPage.test.tsx b/web/src/routes/SalesPage.test.tsx
index 468f44d68..97eff2421 100644
--- a/web/src/routes/SalesPage.test.tsx
+++ b/web/src/routes/SalesPage.test.tsx
@@ -2356,3 +2356,76 @@ it.each([false, true])("discards the line editor on Close before same-order relo
expect(within(reopened).getByLabelText(i18n.t("sales:editQuantityAriaLabel"))).toHaveValue(9);
expect(within(reopened).getByLabelText(i18n.t("sales:editUnitPriceAriaLabel"))).toHaveValue(4);
});
+
+// #712 — primary Open reads respect a later panel dismissal.
+describe.each(["Draft", "Confirmed"] as const)("Sales Open dismissal (#712), %s panel", (status) => {
+ it.each([false, true])("stays closed after a held Open settles (different order: %s), then allows a fresh Open", async (different) => {
+ const original: SalesOrder = { ...DRAFT_TWO, status };
+ const target: SalesOrder = different
+ ? { ...original, id: "other-order", referenceNumber: "SO-OTHER" }
+ : original;
+ mockListOrders.mockResolvedValue(different ? [original, target] : [original]);
+ mockGetOrder.mockResolvedValue(original);
+ await renderReady();
+ const openRow = async (order: SalesOrder) => {
+ await act(async () => {
+ fireEvent.click(within(screen.getByRole("row", { name: new RegExp(order.referenceNumber) }))
+ .getByRole("button", { name: i18n.t("sales:open") }));
+ });
+ };
+ await openRow(original);
+ expect(document.querySelector(".order-panel")).not.toBeNull();
+
+ let resolveRead!: (order: SalesOrder) => void;
+ mockGetOrder.mockReturnValueOnce(new Promise((resolve) => { resolveRead = resolve; }));
+ await openRow(target);
+ const close = screen.getByRole("button", { name: i18n.t("sales:close") });
+ expect(close).toBeEnabled();
+ expect(within(screen.getByRole("row", { name: new RegExp(target.referenceNumber) }))
+ .getByRole("button", { name: i18n.t("sales:open") })).toBeDisabled();
+ fireEvent.click(close);
+ expect(document.querySelector(".order-panel")).toBeNull();
+ await act(async () => { resolveRead(target); });
+ expect(document.querySelector(".order-panel")).toBeNull();
+
+ mockGetOrder.mockResolvedValue(target);
+ await openRow(target);
+ expect(document.querySelector(".order-panel")).not.toBeNull();
+ expect(within(document.querySelector(".order-panel")!)
+ .getByText(new RegExp(target.referenceNumber))).toBeInTheDocument();
+ });
+});
+
+describe("Sales primary Open controls (#712)", () => {
+ it("opens from no panel", async () => {
+ mockListOrders.mockResolvedValue([DRAFT_TWO]);
+ mockGetOrder.mockResolvedValue(DRAFT_TWO);
+ await renderReady();
+ expect(document.querySelector(".order-panel")).toBeNull();
+ await act(async () => { fireEvent.click(screen.getByRole("button", { name: i18n.t("sales:open") })); });
+ expect(document.querySelector(".order-panel")).not.toBeNull();
+ });
+
+ it("shows an Open failure and allows retry", async () => {
+ mockListOrders.mockResolvedValue([DRAFT_TWO]);
+ mockGetOrder.mockRejectedValueOnce(new Error("Order read failed")).mockResolvedValue(DRAFT_TWO);
+ await renderReady();
+ await act(async () => { fireEvent.click(screen.getByRole("button", { name: i18n.t("sales:open") })); });
+ expect(document.querySelector(".order-panel")).toBeNull();
+ expect(screen.getByText("Order read failed")).toBeInTheDocument();
+ await act(async () => { fireEvent.click(screen.getByRole("button", { name: i18n.t("sales:open") })); });
+ expect(document.querySelector(".order-panel")).not.toBeNull();
+ expect(screen.queryByText("Order read failed")).not.toBeInTheDocument();
+ });
+
+ it("keeps a dismissed Open failure on the page and the panel closed", async () => {
+ await openOrder(DRAFT_TWO, /Grade A Dozen/);
+ let rejectRead!: (reason: Error) => void;
+ mockGetOrder.mockReturnValueOnce(new Promise((_, reject) => { rejectRead = reject; }));
+ await act(async () => { fireEvent.click(screen.getByRole("button", { name: i18n.t("sales:open") })); });
+ fireEvent.click(screen.getByRole("button", { name: i18n.t("sales:close") }));
+ await act(async () => { rejectRead(new Error("Dismissed order read failed")); });
+ expect(document.querySelector(".order-panel")).toBeNull();
+ expect(screen.getByText("Dismissed order read failed")).toBeInTheDocument();
+ });
+});
diff --git a/web/src/routes/SalesPage.tsx b/web/src/routes/SalesPage.tsx
index d9cf7c90f..d35d5c266 100644
--- a/web/src/routes/SalesPage.tsx
+++ b/web/src/routes/SalesPage.tsx
@@ -228,8 +228,8 @@ export function SalesPage() {
// (#474 → #477 → #479 → #625 → #702); the rules and the incidents that
// earned them live with the hook. Sales-specific is only WHICH scopes own a
// dialog, and that the page-level success message clears on each attempt.
- const { busy, isPending, errors, run, openDialog, dismissDialog } = useDialogAction(
- ["create-order", "record-payment"],
+ const { busy, isPending, errors, run, openDialog, dismissDialog, startLoad } = useDialogAction(
+ ["create-order", "record-payment", "order-panel"],
{ onAttempt: () => setMessage(null) },
);
// Pulled out for the payments effect's dependency list: it is stable, and
@@ -454,6 +454,7 @@ export function SalesPage() {
};
const closeOrderPanel = () => {
+ dismissDialog("order-panel");
setActive(null);
// Dismissal discards the draft now; a later Open must use its fetched line.
setEditItemId(null);
@@ -705,7 +706,9 @@ export function SalesPage() {
// Always fetch fresh on open — the list row may be stale relative to
// mutations made through the panel since the list was loaded.
const onOpen = (id: string) => run(`open:${id}`, async () => {
- setActive(await getOrder(id));
+ const current = startLoad("order-panel");
+ const loaded = await getOrder(id);
+ if (current()) setActive(loaded);
});
// A list failure no longer replaces the workspace: it renders as a banner