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). */}
+
+