mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
fix(work-orders): drop slide-over edit actions after complete
Exit edit mode and refuse core Save when status becomes Completed; hide the footer even if unsaved.
This commit is contained in:
parent
18d80a3c47
commit
260fe6c654
5 changed files with 189 additions and 35 deletions
|
|
@ -33,6 +33,27 @@ export function SlideOverFooter({
|
|||
onDiscard,
|
||||
onKeepEditing,
|
||||
}: SlideOverFooterProps) {
|
||||
if (isCancelled) {
|
||||
return (
|
||||
<div
|
||||
className="flex shrink-0 items-center gap-2 px-4 py-2.5"
|
||||
style={{
|
||||
background: "var(--color-bg-muted)",
|
||||
borderTop: "1px solid var(--color-border)",
|
||||
}}
|
||||
>
|
||||
<Ban size={14} style={{ color: "var(--muted-foreground)" }} />
|
||||
<Text as="p" className="text-[12px]" style={{ color: "var(--muted-foreground)" }}>
|
||||
This work order has been cancelled and is read-only.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCompleted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (showUnsaved) {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -70,27 +91,6 @@ export function SlideOverFooter({
|
|||
);
|
||||
}
|
||||
|
||||
if (isCancelled) {
|
||||
return (
|
||||
<div
|
||||
className="flex shrink-0 items-center gap-2 px-4 py-2.5"
|
||||
style={{
|
||||
background: "var(--color-bg-muted)",
|
||||
borderTop: "1px solid var(--color-border)",
|
||||
}}
|
||||
>
|
||||
<Ban size={14} style={{ color: "var(--muted-foreground)" }} />
|
||||
<Text as="p" className="text-[12px]" style={{ color: "var(--muted-foreground)" }}>
|
||||
This work order has been cancelled and is read-only.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCompleted && !editing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex shrink-0 items-center justify-between gap-3 px-5 py-3"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ import {
|
|||
import type { SlideOverSaveOptions } from "./work-order-slide-over";
|
||||
import type { SlideOverTab } from "./use-work-order-slide-over";
|
||||
|
||||
function isSlideOverInfoLocked(status: string | null | undefined): boolean {
|
||||
return isWorkOrderFullyLocked(status) || isWorkOrderCoreLocked(status);
|
||||
}
|
||||
|
||||
type UseSlideOverEditStateArgs = {
|
||||
row: WorkOrderTableRow | null;
|
||||
editMode: boolean;
|
||||
|
|
@ -53,9 +57,7 @@ export function useSlideOverEditState({
|
|||
}
|
||||
setDraft(row);
|
||||
setBaseline(row);
|
||||
setEditing(
|
||||
editMode && !isWorkOrderFullyLocked(row.status) && !isWorkOrderCoreLocked(row.status),
|
||||
);
|
||||
setEditing(editMode && !isSlideOverInfoLocked(row.status));
|
||||
setTab("info");
|
||||
setShowUnsaved(false);
|
||||
setUnsavedIntent(null);
|
||||
|
|
@ -63,6 +65,21 @@ export function useSlideOverEditState({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps -- row.id / editMode gate
|
||||
}, [row?.id, editMode]);
|
||||
|
||||
const lockStatus = infoSource?.status ?? row?.status;
|
||||
const isInfoLocked = isSlideOverInfoLocked(lockStatus);
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<SlideOverFooter
|
||||
isCancelled={false}
|
||||
isCompleted
|
||||
editing
|
||||
isDirty
|
||||
showUnsaved={false}
|
||||
{...footerHandlers}
|
||||
/>,
|
||||
{ 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(
|
||||
<SlideOverFooter
|
||||
isCancelled={false}
|
||||
isCompleted
|
||||
editing
|
||||
isDirty
|
||||
showUnsaved
|
||||
{...footerHandlers}
|
||||
/>,
|
||||
{ withAuth: false },
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(screen.queryByText(/unsaved changes/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /^save$/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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> = {}): 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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue