diff --git a/src/app/(protected)/workorders/_components/detail/slide-over-footer.tsx b/src/app/(protected)/workorders/_components/detail/slide-over-footer.tsx
index 6792121e..cc623902 100644
--- a/src/app/(protected)/workorders/_components/detail/slide-over-footer.tsx
+++ b/src/app/(protected)/workorders/_components/detail/slide-over-footer.tsx
@@ -33,6 +33,27 @@ export function SlideOverFooter({
onDiscard,
onKeepEditing,
}: SlideOverFooterProps) {
+ if (isCancelled) {
+ return (
+
+
+
+ This work order has been cancelled and is read-only.
+
+
+ );
+ }
+
+ if (isCompleted) {
+ return null;
+ }
+
if (showUnsaved) {
return (
-
-
- This work order has been cancelled and is read-only.
-
-
- );
- }
-
- if (isCompleted && !editing) {
- return null;
- }
-
return (
{
+ if (!isInfoLocked) return;
+ setEditing(false);
+ setShowUnsaved(false);
+ setUnsavedIntent(null);
+ const source = infoSource ?? row;
+ if (source) {
+ setDraft(source);
+ setBaseline(source);
+ }
+ }, [isInfoLocked, infoSource, row]);
+
useEffect(() => {
if (!infoSource) return;
if (!editing) {
@@ -90,6 +107,7 @@ export function useSlideOverEditState({
const commit = () => {
if (!row || !activeDraft || !dirtyBaseline) return;
+ if (isSlideOverInfoLocked(infoSource?.status ?? row.status)) return;
const patch = buildSlideOverPatch(activeDraft, dirtyBaseline);
if (Object.keys(patch).length === 0) {
setEditing(false);
@@ -145,7 +163,7 @@ export function useSlideOverEditState({
const startEditing = () => {
if (!infoSource) return;
- if (isWorkOrderFullyLocked(infoSource.status) || isWorkOrderCoreLocked(infoSource.status)) {
+ if (isSlideOverInfoLocked(infoSource.status)) {
return;
}
setDraft(infoSource);
diff --git a/src/domain/work-orders/utils/work-order-lock.ts b/src/domain/work-orders/utils/work-order-lock.ts
index 96e99ae9..83d2e74c 100644
--- a/src/domain/work-orders/utils/work-order-lock.ts
+++ b/src/domain/work-orders/utils/work-order-lock.ts
@@ -29,9 +29,9 @@ export function canMutateWorkOrderExtraDocs(status: string | null | undefined):
const COMPLETED_BOARD_PATCH_KEYS = new Set(["flagColor"]);
/**
- * Board PATCH policy for SH-115. Comments and Extra Docs use dedicated endpoints
- * (`canMutateWorkOrderComments` / `canMutateWorkOrderExtraDocs`). Those requests
- * may still fail with 422 `ReadOnly` if the backend lock remains total.
+ * Board PATCH policy for Completed: only `flagColor`. Comments and Extra Docs use
+ * dedicated endpoints (`canMutateWorkOrderComments` / `canMutateWorkOrderExtraDocs`).
+ * Those requests may still fail with 422 `ReadOnly` if the backend lock remains total.
*/
export function canApplyWorkOrderTablePatch(
status: string | null | undefined,
diff --git a/src/test/app/(protected)/workorders/slide-over-footer-completed.test.tsx b/src/test/app/(protected)/workorders/slide-over-footer-completed.test.tsx
index 8b58ebbe..348fbf48 100644
--- a/src/test/app/(protected)/workorders/slide-over-footer-completed.test.tsx
+++ b/src/test/app/(protected)/workorders/slide-over-footer-completed.test.tsx
@@ -3,6 +3,16 @@ import { describe, expect, it, vi } from "vitest";
import { SlideOverFooter } from "@/app/(protected)/workorders/_components/detail/slide-over-footer";
import { renderWithProviders } from "@/test/test-utils";
+const footerHandlers = {
+ onCancelWO: vi.fn(),
+ onCancelEdit: vi.fn(),
+ onClose: vi.fn(),
+ onEdit: vi.fn(),
+ onSave: vi.fn(),
+ onDiscard: vi.fn(),
+ onKeepEditing: vi.fn(),
+};
+
describe("SlideOverFooter completed lock", () => {
it("hides Edit and Cancel WO when the work order is completed", () => {
const { container } = renderWithProviders(
@@ -12,13 +22,7 @@ describe("SlideOverFooter completed lock", () => {
editing={false}
isDirty={false}
showUnsaved={false}
- onCancelWO={vi.fn()}
- onCancelEdit={vi.fn()}
- onClose={vi.fn()}
- onEdit={vi.fn()}
- onSave={vi.fn()}
- onDiscard={vi.fn()}
- onKeepEditing={vi.fn()}
+ {...footerHandlers}
/>,
{ withAuth: false },
);
@@ -27,4 +31,40 @@ describe("SlideOverFooter completed lock", () => {
expect(screen.queryByRole("button", { name: /edit wo/i })).not.toBeInTheDocument();
expect(screen.queryByText(/cancel wo/i)).not.toBeInTheDocument();
});
+
+ it("hides Cancel WO and Save when completed while still in edit mode", () => {
+ const { container } = renderWithProviders(
+ ,
+ { withAuth: false },
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ expect(screen.queryByText(/cancel wo/i)).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: /^save$/i })).not.toBeInTheDocument();
+ });
+
+ it("hides the unsaved Save bar when the work order is completed", () => {
+ const { container } = renderWithProviders(
+ ,
+ { withAuth: false },
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ expect(screen.queryByText(/unsaved changes/i)).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: /^save$/i })).not.toBeInTheDocument();
+ });
});
diff --git a/src/test/app/(protected)/workorders/use-slide-over-edit-state-completed.test.tsx b/src/test/app/(protected)/workorders/use-slide-over-edit-state-completed.test.tsx
new file mode 100644
index 00000000..bde4cad9
--- /dev/null
+++ b/src/test/app/(protected)/workorders/use-slide-over-edit-state-completed.test.tsx
@@ -0,0 +1,96 @@
+import { act, renderHook } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { useSlideOverEditState } from "@/app/(protected)/workorders/_components/detail/use-slide-over-edit-state";
+import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
+
+function baseRow(overrides: Partial = {}): WorkOrderTableRow {
+ return {
+ id: 100,
+ woNumber: "100",
+ site: "SITE",
+ type: "Emergency",
+ severity: null,
+ dispatcherId: "d1",
+ dispatcherName: "Alex",
+ scheduledOn: "2026-07-16",
+ completedDate: "",
+ targetWeek: "",
+ dayGroup: "2026-07-16",
+ dayLabel: "Thu",
+ location: "Main",
+ locationId: 5,
+ pm: "",
+ serviceNotes: "",
+ extraServices: [],
+ vendorId: "v1",
+ company: "Acme",
+ tech: "Sam",
+ techPhone: "",
+ apptStart: "",
+ apptEnd: "",
+ apptTime: "",
+ status: "Incomplete",
+ docStatus: "",
+ pocName: "",
+ pocPhone: "",
+ pocNotes: "",
+ additionalContacts: [],
+ dueDate: "",
+ rescheduleCount: 0,
+ carriedOver: 0,
+ originalWeek: "",
+ originalDate: "",
+ isPastDue: false,
+ scheduleWeekOnly: false,
+ mediaCount: 0,
+ title: "",
+ priority: "",
+ primaryDispatchId: null,
+ rowVersion: "1",
+ dispatchRowVersion: "",
+ flagColor: null,
+ isAddOn: false,
+ upliftSummary: { hasUplift: false, pendingCount: 0 },
+ ...overrides,
+ };
+}
+
+describe("useSlideOverEditState completed lock", () => {
+ it("leaves edit mode and refuses commit when the same work order becomes completed", () => {
+ const onSave = vi.fn();
+ const onClose = vi.fn();
+ const setTab = vi.fn();
+ const open = baseRow();
+
+ const { result, rerender } = renderHook(
+ ({ row, infoSource }) =>
+ useSlideOverEditState({
+ row,
+ editMode: true,
+ infoSource,
+ onSave,
+ onClose,
+ closeDisabled: false,
+ setTab,
+ }),
+ { initialProps: { row: open, infoSource: open } },
+ );
+
+ expect(result.current.editing).toBe(true);
+
+ act(() => {
+ result.current.handleDraftChange({ woNumber: "WO-LOCKED" });
+ });
+
+ const completed = baseRow({ status: "Completed", woNumber: "100" });
+ rerender({ row: completed, infoSource: completed });
+
+ expect(result.current.editing).toBe(false);
+
+ act(() => {
+ result.current.commit();
+ });
+
+ expect(onSave).not.toHaveBeenCalled();
+ });
+});