mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
fix(work-orders): persist type via PATCH and lock completed rows
This commit is contained in:
parent
f51f6bd493
commit
ccdad96170
8 changed files with 205 additions and 28 deletions
|
|
@ -28,7 +28,7 @@ export function AssignedToCell({ dispatcherId, users, highlight, onSave }: Assig
|
|||
e.stopPropagation();
|
||||
setAnchorEl(e.currentTarget);
|
||||
}}
|
||||
className="group/cell flex w-full items-center gap-1.5 rounded transition-opacity hover:opacity-80"
|
||||
className="group/cell flex w-full min-w-0 items-center gap-1.5 rounded transition-opacity hover:opacity-80"
|
||||
style={
|
||||
highlight
|
||||
? {
|
||||
|
|
@ -43,13 +43,13 @@ export function AssignedToCell({ dispatcherId, users, highlight, onSave }: Assig
|
|||
{selected ? (
|
||||
<DispatcherPill user={selected} />
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[13px] text-[var(--color-text-muted)]">
|
||||
<span className="flex min-w-0 items-center gap-1.5 text-[13px] text-[var(--color-text-muted)]">
|
||||
<Users
|
||||
size={13}
|
||||
className="shrink-0"
|
||||
style={{ color: highlight ? "var(--primary)" : "var(--color-text-muted)" }}
|
||||
/>
|
||||
Select dispatcher
|
||||
<span className="truncate whitespace-nowrap">Select dispatcher</span>
|
||||
</span>
|
||||
)}
|
||||
<HoverChevron />
|
||||
|
|
|
|||
|
|
@ -18,14 +18,32 @@ type TypeCellProps = {
|
|||
value: WOType | "";
|
||||
severity?: WorkOrderSeverity | null;
|
||||
q?: string;
|
||||
viewOnly?: boolean;
|
||||
onSave: (type: WOType, severity: WorkOrderSeverity | null) => void;
|
||||
};
|
||||
|
||||
export function TypeCell({ value, severity = null, q, onSave }: TypeCellProps) {
|
||||
function TypeCellValue({ value, q }: { value: WOType | ""; q?: string }) {
|
||||
if (value) {
|
||||
return <WoTableTypeLabel type={value} q={q} />;
|
||||
}
|
||||
return <span className="text-[13px] text-[var(--color-text-muted)]">Select type of WO</span>;
|
||||
}
|
||||
|
||||
export function TypeCell({ value, severity = null, q, viewOnly = false, onSave }: TypeCellProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalSeed, setModalSeed] = useState<WOType | "">(value);
|
||||
const [resetSeverity, setResetSeverity] = useState(false);
|
||||
const showSeverityBadge = Boolean(value !== "" && typeNeedsSeverity(value) && severity != null);
|
||||
|
||||
if (viewOnly) {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-1">
|
||||
<TypeCellValue value={value} q={q} />
|
||||
{Boolean(showSeverityBadge && severity != null) && <SeverityBadge severity={severity!} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function pick(t: WOType) {
|
||||
setAnchorEl(null);
|
||||
|
|
@ -43,8 +61,6 @@ export function TypeCell({ value, severity = null, q, onSave }: TypeCellProps) {
|
|||
onSave(t, needsSeverity ? severity : null);
|
||||
}
|
||||
|
||||
const showSeverityBadge = Boolean(value !== "" && typeNeedsSeverity(value) && severity != null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
|
|
@ -57,11 +73,7 @@ export function TypeCell({ value, severity = null, q, onSave }: TypeCellProps) {
|
|||
}}
|
||||
className="group/cell flex w-full items-center gap-1 transition-opacity hover:opacity-80"
|
||||
>
|
||||
{value ? (
|
||||
<WoTableTypeLabel type={value} q={q} />
|
||||
) : (
|
||||
<span className="text-[13px] text-[var(--color-text-muted)]">Select type of WO</span>
|
||||
)}
|
||||
<TypeCellValue value={value} q={q} />
|
||||
{Boolean(showSeverityBadge && severity != null) && (
|
||||
<SeverityBadge
|
||||
severity={severity!}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,33 @@
|
|||
import { toast } from "react-toastify";
|
||||
import type { WorkOrderSeverity } from "@/domain/work-orders/types/work-order-severity";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type {
|
||||
WorkOrderTablePatch,
|
||||
WorkOrderTableRow,
|
||||
} from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
|
||||
import {
|
||||
formatSeverityEscalationToast,
|
||||
shouldAnnounceSeverityEscalation,
|
||||
} from "@/domain/work-orders/utils/severity-escalation";
|
||||
|
||||
type PersistPatchFn = (
|
||||
id: string | number,
|
||||
patch: WorkOrderTablePatch,
|
||||
options?: { onSuccess?: () => void },
|
||||
) => void;
|
||||
|
||||
export function saveExistingRowType(
|
||||
row: Pick<WorkOrderTableRow, "woNumber" | "severity">,
|
||||
onPatch: PersistPatchFn,
|
||||
row: Pick<WorkOrderTableRow, "id" | "woNumber" | "severity">,
|
||||
type: WOType,
|
||||
severity: WorkOrderSeverity | null,
|
||||
onPatchRow: (patch: Partial<WorkOrderTableRow>) => void,
|
||||
) {
|
||||
if (shouldAnnounceSeverityEscalation(row.severity, severity)) {
|
||||
toast.warn(formatSeverityEscalationToast(row.woNumber, severity));
|
||||
const patch = { type, severity };
|
||||
if (!shouldAnnounceSeverityEscalation(row.severity, severity)) {
|
||||
onPatch(row.id, patch);
|
||||
return;
|
||||
}
|
||||
onPatchRow({ type, severity });
|
||||
onPatch(row.id, patch, {
|
||||
onSuccess: () => toast.warn(formatSeverityEscalationToast(row.woNumber, severity)),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { CSSProperties } from "react";
|
|||
import type { LocationOption, WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { applyAssignedToTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts";
|
||||
import { isWorkOrderCompletionSurfaceReadOnly } from "@/domain/work-orders/utils/work-order-lock";
|
||||
import { saveExistingRowType } from "./save-existing-row-type";
|
||||
import { AssignedToCell } from "./cells/assigned-to-cell";
|
||||
import { DateCell } from "./cells/date-cell";
|
||||
|
|
@ -104,7 +105,8 @@ export function WoTableRowCells({
|
|||
value={row.type}
|
||||
severity={row.severity}
|
||||
q={search}
|
||||
onSave={(t, sev) => saveExistingRowType(row, t, sev, onPatchRow)}
|
||||
viewOnly={isWorkOrderCompletionSurfaceReadOnly(row.status)}
|
||||
onSave={(t, sev) => saveExistingRowType(handlers.onPatch, row, t, sev)}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
|
|
|
|||
|
|
@ -8,35 +8,47 @@ vi.mock("react-toastify", () => ({
|
|||
|
||||
import { saveExistingRowType } from "@/app/(protected)/workorders/_components/list/table/save-existing-row-type";
|
||||
|
||||
const row = { id: 7, woNumber: "00000024965", severity: 3 as const };
|
||||
|
||||
describe("saveExistingRowType", () => {
|
||||
beforeEach(() => {
|
||||
toastWarn.mockReset();
|
||||
});
|
||||
|
||||
it("warns when severity newly enters SEV 1 and still patches the row", () => {
|
||||
const onPatchRow = vi.fn();
|
||||
it("warns when severity newly enters SEV 1 only after persist onSuccess", () => {
|
||||
const onPatch = vi.fn();
|
||||
|
||||
saveExistingRowType({ woNumber: "00000024965", severity: 3 }, "Emergency", 1, onPatchRow);
|
||||
saveExistingRowType(onPatch, row, "Emergency", 1);
|
||||
|
||||
expect(toastWarn).not.toHaveBeenCalled();
|
||||
expect(onPatch).toHaveBeenCalledWith(
|
||||
7,
|
||||
{ type: "Emergency", severity: 1 },
|
||||
expect.objectContaining({ onSuccess: expect.any(Function) }),
|
||||
);
|
||||
|
||||
const { onSuccess } = onPatch.mock.calls[0]![2] as { onSuccess: () => void };
|
||||
onSuccess();
|
||||
expect(toastWarn).toHaveBeenCalledWith("WO #00000024965 escalated to SEV 1.");
|
||||
expect(onPatchRow).toHaveBeenCalledWith({ type: "Emergency", severity: 1 });
|
||||
});
|
||||
|
||||
it("does not warn when staying on a critical severity", () => {
|
||||
const onPatchRow = vi.fn();
|
||||
const onPatch = vi.fn();
|
||||
|
||||
saveExistingRowType({ woNumber: "00000024965", severity: 1 }, "Emergency", 1, onPatchRow);
|
||||
saveExistingRowType(onPatch, { ...row, severity: 1 }, "Emergency", 1);
|
||||
|
||||
expect(toastWarn).not.toHaveBeenCalled();
|
||||
expect(onPatchRow).toHaveBeenCalledWith({ type: "Emergency", severity: 1 });
|
||||
expect(onPatch).toHaveBeenCalledWith(7, { type: "Emergency", severity: 1 });
|
||||
expect(onPatch.mock.calls[0]![2]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not warn when clearing severity on a PM correction", () => {
|
||||
const onPatchRow = vi.fn();
|
||||
const onPatch = vi.fn();
|
||||
|
||||
saveExistingRowType({ woNumber: "00000024965", severity: 2 }, "PM", null, onPatchRow);
|
||||
saveExistingRowType(onPatch, { ...row, severity: 2 }, "PM", null);
|
||||
|
||||
expect(toastWarn).not.toHaveBeenCalled();
|
||||
expect(onPatchRow).toHaveBeenCalledWith({ type: "PM", severity: null });
|
||||
expect(onPatch).toHaveBeenCalledWith(7, { type: "PM", severity: null });
|
||||
expect(onPatch.mock.calls[0]![2]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -83,6 +83,23 @@ describe("TypeCell", () => {
|
|||
expect(onSave).toHaveBeenCalledWith("Reactive", 4);
|
||||
});
|
||||
|
||||
it("does not open the picker or severity modal when viewOnly", async () => {
|
||||
const onSave = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<TypeCell value="Emergency" severity={2} viewOnly onSave={onSave} />);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Type of WO, Emergency" })).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Emergency")).toBeInTheDocument();
|
||||
expect(screen.getByText("SEV 2")).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByText("Emergency"));
|
||||
await user.click(screen.getByText("SEV 2"));
|
||||
|
||||
expect(screen.queryByRole("button", { name: /^Reactive$/ })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens the modal from the severity badge", async () => {
|
||||
const onSave = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { WoTableRowCells } from "@/app/(protected)/workorders/_components/list/table/wo-table-row-cells";
|
||||
import type { WoTableRowHandlers } from "@/app/(protected)/workorders/_components/list/table/wo-table-row";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { renderWithProviders } from "@/test/test-utils";
|
||||
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/site-cell", () => ({
|
||||
SiteCell: () => null,
|
||||
}));
|
||||
vi.mock(
|
||||
"@/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell",
|
||||
() => ({
|
||||
EditableWONumberCell: () => null,
|
||||
}),
|
||||
);
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/type-cell", () => ({
|
||||
TypeCell: ({ onSave }: { onSave: (type: string, severity: number) => void }) => (
|
||||
<button type="button" onClick={() => onSave("Emergency", 1)}>
|
||||
persist-type
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/date-cell", () => ({
|
||||
DateCell: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/due-date-cell", () => ({
|
||||
DueDateCell: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({
|
||||
AssignedToCell: () => null,
|
||||
}));
|
||||
|
||||
const row = { id: 7, woNumber: "00000024965", severity: 3 } as WorkOrderTableRow;
|
||||
|
||||
describe("WoTableRowCells type persist", () => {
|
||||
it("forwards type save through patch onSuccess instead of onPatchRow", () => {
|
||||
const onPatch = vi.fn();
|
||||
const onPatchRow = vi.fn();
|
||||
const handlers = { onPatch } as unknown as WoTableRowHandlers;
|
||||
|
||||
renderWithProviders(
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<WoTableRowCells
|
||||
row={row}
|
||||
allRows={[]}
|
||||
users={[]}
|
||||
sites={[]}
|
||||
handlers={handlers}
|
||||
onPatchRow={onPatchRow}
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>,
|
||||
{ withAuth: false },
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "persist-type" }));
|
||||
|
||||
expect(onPatchRow).not.toHaveBeenCalled();
|
||||
expect(onPatch).toHaveBeenCalledWith(
|
||||
7,
|
||||
{ type: "Emergency", severity: 1 },
|
||||
expect.objectContaining({ onSuccess: expect.any(Function) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -133,4 +133,56 @@ describe("WoTableRow completed selective lock", () => {
|
|||
expect(screen.queryByRole("button", { name: /in progress/i })).not.toBeInTheDocument();
|
||||
expect(handlers.onPatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not expose the type picker on a Completed row", () => {
|
||||
const row = makeRow();
|
||||
renderWithProviders(
|
||||
<table>
|
||||
<tbody>
|
||||
<WoTableRow
|
||||
row={row}
|
||||
allRows={[row]}
|
||||
users={[]}
|
||||
sites={[]}
|
||||
vendors={[]}
|
||||
pmTypes={["HVAC"]}
|
||||
isSelected={false}
|
||||
isFlashing={false}
|
||||
dragViz={{ dragId: null, overId: null }}
|
||||
handlers={handlers}
|
||||
/>
|
||||
</tbody>
|
||||
</table>,
|
||||
{ withAuth: false },
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Type of WO, PM" })).not.toBeInTheDocument();
|
||||
expect(screen.getByText("PM")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not expose the type picker on a Canceled row", () => {
|
||||
const row = makeRow({ status: "Canceled" });
|
||||
renderWithProviders(
|
||||
<table>
|
||||
<tbody>
|
||||
<WoTableRow
|
||||
row={row}
|
||||
allRows={[row]}
|
||||
users={[]}
|
||||
sites={[]}
|
||||
vendors={[]}
|
||||
pmTypes={["HVAC"]}
|
||||
isSelected={false}
|
||||
isFlashing={false}
|
||||
dragViz={{ dragId: null, overId: null }}
|
||||
handlers={handlers}
|
||||
/>
|
||||
</tbody>
|
||||
</table>,
|
||||
{ withAuth: false },
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Type of WO, PM" })).not.toBeInTheDocument();
|
||||
expect(screen.getByText("PM")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue