From 7801b602f50e528079c06ec9ccfdcb57cdb8412e Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Wed, 29 Jul 2026 17:41:21 +0200 Subject: [PATCH] feat(orders): add edit-mode confirmation for items table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Items in the orders review modal are read-only by default. A new 'Edit items' button (Pencil icon, top right of the items list) opens a confirmation dialog before the inputs become editable. - State: editingItems (default false), confirmingEdit (default false). - Both reset to false whenever the modal re-opens (useEffect on [open, order.id]) so re-opening always starts read-only. - All 4 per-row inputs (qty, cost, markup, selling) carry disabled={!editingItems || saving}. - Visual hint: when editing, the items list gets a subtle warn ring + tint (ring-1 ring-warn/40 bg-warn/5). - data-testid hooks: review-edit-items-btn on the button, review-edit-confirm-dialog on the dialog, review-items-list on the ul with data-editing attribute. - 4 new tests cover: read-only default, button opens dialog, confirm enables editing, cancel keeps read-only. Also adds order_attachements?: string[] to the Order type — the PB file field on the orders collection used by the modal's source- of-truth panel (separate commit wires the data). Co-authored-by: Orca --- src/components/orders-review-modal.test.tsx | 70 +++++++++++++ src/components/orders-review-modal.tsx | 110 +++++++++++++++++++- src/lib/types.ts | 12 +++ 3 files changed, 188 insertions(+), 4 deletions(-) diff --git a/src/components/orders-review-modal.test.tsx b/src/components/orders-review-modal.test.tsx index 72ac6de..ffc0d49 100644 --- a/src/components/orders-review-modal.test.tsx +++ b/src/components/orders-review-modal.test.tsx @@ -426,4 +426,74 @@ describe("OrdersReviewModal", () => { ); expect(screen.getByText(/unpreviewable attachment/i)).toBeInTheDocument(); }); + + describe("edit-mode confirmation", () => { + // All 4 per-row inputs (qty, cost, markup, selling) should be + // disabled until the operator clicks "Edit items" → confirms. + // This is the safety gate — items are read-only by default. + it("items are read-only by default", () => { + render(); + const itemRow = screen.getByText("HSS drill 6mm").closest("li")!; + const rowInputs = itemRow.querySelectorAll("input"); + expect(rowInputs).toHaveLength(4); + for (const input of Array.from(rowInputs)) { + expect(input).toBeDisabled(); + } + // The list itself should NOT carry the editing visual hint yet. + const list = screen.getByTestId("review-items-list"); + expect(list.getAttribute("data-editing")).toBe("false"); + }); + + it("clicking Edit items opens a confirmation dialog", () => { + render(); + fireEvent.click(screen.getByTestId("review-edit-items-btn")); + // Dialog surfaces the confirmation copy. + expect( + screen.getByText(/Enable item editing\?/i), + ).toBeInTheDocument(); + // Inputs are STILL disabled — confirming is required to flip the gate. + const itemRow = screen.getByText("HSS drill 6mm").closest("li")!; + for (const input of Array.from(itemRow.querySelectorAll("input"))) { + expect(input).toBeDisabled(); + } + }); + + it("confirming the dialog enables editing", () => { + render(); + fireEvent.click(screen.getByTestId("review-edit-items-btn")); + fireEvent.click(screen.getByTestId("review-edit-confirm-enable")); + + // Dialog should be closed and items enabled. + const itemRow = screen.getByText("HSS drill 6mm").closest("li")!; + for (const input of Array.from(itemRow.querySelectorAll("input"))) { + expect(input).not.toBeDisabled(); + } + // The visual edit-mode hint should be applied. + const list = screen.getByTestId("review-items-list"); + expect(list.getAttribute("data-editing")).toBe("true"); + // Button label flips to "Stop editing" so the operator has an + // obvious exit without re-confirming. + expect( + screen.getByRole("button", { name: /Stop editing/i }), + ).toBeInTheDocument(); + }); + + it("canceling the dialog keeps items read-only", () => { + render(); + fireEvent.click(screen.getByTestId("review-edit-items-btn")); + // The Cancel button is the close in the dialog footer. + fireEvent.click(screen.getByRole("button", { name: /^Cancel$/i })); + + // Dialog gone, items still disabled, no edit hint. + expect( + screen.queryByText(/Enable item editing\?/i), + ).not.toBeInTheDocument(); + const itemRow = screen.getByText("HSS drill 6mm").closest("li")!; + for (const input of Array.from(itemRow.querySelectorAll("input"))) { + expect(input).toBeDisabled(); + } + const list = screen.getByTestId("review-items-list"); + expect(list.getAttribute("data-editing")).toBe("false"); + }); + }); }); diff --git a/src/components/orders-review-modal.tsx b/src/components/orders-review-modal.tsx index 53a03a5..9473379 100644 --- a/src/components/orders-review-modal.tsx +++ b/src/components/orders-review-modal.tsx @@ -8,10 +8,20 @@ import { Loader2, Mail, Paperclip, + Pencil, X, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { CustomerSearch } from "@/components/ui/customer-search"; @@ -110,6 +120,16 @@ export function OrdersReviewModal({ const [partialFailures, setPartialFailures] = useState< Array<{ id: string; error: string }> >([]); + // Edit-mode gate for the items table. Items are read-only by default; + // clicking the "Edit items" button pops a confirmation dialog, and on + // confirm we flip this on. Reset to false on modal close so re-opening + // the modal always starts in read-only mode (defence in depth — parent + // already remounts via `key={order.id}`, but we don't want stale + // editing state to leak between orders if the key is ever forgotten). + const [editingItems, setEditingItems] = useState(false); + // Whether the confirmation dialog is currently shown. Distinct from + // `editingItems` because the dialog is its own modal surface. + const [confirmingEdit, setConfirmingEdit] = useState(false); // Re-seed local state when the modal opens for a different order. // Parent already remounts via `key={order.id}`, but defensively @@ -126,6 +146,8 @@ export function OrdersReviewModal({ setNumberError(null); setCollidingOrderId(null); setPartialFailures([]); + setEditingItems(false); + setConfirmingEdit(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, order.id]); @@ -446,15 +468,48 @@ export function OrdersReviewModal({ The preview region is filled in by Task 5. */}
-

- Order items -

+
+

+ Order items +

+ {/* Edit gate — items are read-only until the operator + confirms via the dialog. The button label flips + between "Edit items" and "Stop editing" so the + operator has a clear exit. Clicking while in edit + mode skips the confirmation (no data loss to + confirm against; save still has its own gate). */} + +
{persistedItems.length === 0 ? (

No line items on this order.

) : ( -
    +
      {persistedItems.map((it) => { const d = itemDrafts[it.id]; if (!d) return null; @@ -474,6 +529,7 @@ export function OrdersReviewModal({ onChange={(e) => onQuantityChange(it.id, e.target.value) } + disabled={!editingItems || saving} className="h-8 text-xs tabular-nums" />
@@ -485,6 +541,7 @@ export function OrdersReviewModal({ type="number" value={d.cost_price ?? ""} onChange={(e) => onCostChange(it.id, e.target.value)} + disabled={!editingItems || saving} className={cn( "h-8 text-xs tabular-nums", d.lock === "cost" && "border-warn/40 bg-warn/10", @@ -501,6 +558,7 @@ export function OrdersReviewModal({ onChange={(e) => onMarkupChange(it.id, e.target.value) } + disabled={!editingItems || saving} className="h-8 text-xs tabular-nums" />
@@ -514,6 +572,7 @@ export function OrdersReviewModal({ onChange={(e) => onSellingChange(it.id, e.target.value) } + disabled={!editingItems || saving} className={cn( "h-8 text-xs tabular-nums", d.lock === "selling" && "border-warn/40 bg-warn/10", @@ -571,6 +630,49 @@ export function OrdersReviewModal({ + {/* Edit-mode confirmation dialog. Pops when the operator clicks + "Edit items" so the input fields aren't immediately + editable — the operator has to confirm intent. Cancel just + closes the dialog; Enable editing flips `editingItems` and + also closes the dialog. */} + { + // Only allow the dialog to be dismissed via Cancel / X — + // don't auto-flip editingItems from the open-change handler + // (the only writers are the two buttons below). + if (!open) setConfirmingEdit(false); + }} + > + + + Enable item editing? + + Are you sure you want to edit the fields? Changes will be + saved when you click Save. + + + + }> + Cancel + + + + + +