mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 04:42:03 +00:00
Merge pull request #228 from Sea-Haven-Industries/fix/ab/sh-379-poc-persist
Persist manual POC edits via dedicated POC endpoint (SH-379)
This commit is contained in:
commit
f25a3dd628
12 changed files with 329 additions and 67 deletions
|
|
@ -30,6 +30,7 @@ export const API_PATHS = {
|
||||||
boardCreate: "workorders/board",
|
boardCreate: "workorders/board",
|
||||||
boardSearch: "workorders/board/search",
|
boardSearch: "workorders/board/search",
|
||||||
boardPatch: (id: string | number) => `workorders/${id}/board`,
|
boardPatch: (id: string | number) => `workorders/${id}/board`,
|
||||||
|
pocPatch: (id: string | number) => `workorders/${id}/poc`,
|
||||||
boardDetail: (id: string | number) => `workorders/${id}/detail`,
|
boardDetail: (id: string | number) => `workorders/${id}/detail`,
|
||||||
completionTemplates: "workorders/completion-templates",
|
completionTemplates: "workorders/completion-templates",
|
||||||
comments: (id: string | number) => `workorders/${id}/comments`,
|
comments: (id: string | number) => `workorders/${id}/comments`,
|
||||||
|
|
|
||||||
81
src/domain/work-orders/api/work-order-board-patch-api.ts
Normal file
81
src/domain/work-orders/api/work-order-board-patch-api.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { API_PATHS } from "@/api/api-paths";
|
||||||
|
import { ApiError, mapHttpStatusToMessage } from "@/api/api-error";
|
||||||
|
import { apiRequestRaw } from "@/api/api";
|
||||||
|
import {
|
||||||
|
BoardPatchConflictError,
|
||||||
|
BoardPatchValidationError,
|
||||||
|
} from "@/domain/work-orders/errors/board-patch-errors";
|
||||||
|
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-mapper";
|
||||||
|
import type {
|
||||||
|
WorkOrderBoardPatchPayload,
|
||||||
|
WorkOrderPocPatchPayload,
|
||||||
|
} from "@/domain/work-orders/types/work-order-board";
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
|
/** Shared request/error handling for board row PATCH endpoints (board + poc). */
|
||||||
|
async function patchBoardRow(
|
||||||
|
operationName: string,
|
||||||
|
method: "patch",
|
||||||
|
url: string,
|
||||||
|
payload: WorkOrderBoardPatchPayload | WorkOrderPocPatchPayload,
|
||||||
|
): Promise<WorkOrderTableRow> {
|
||||||
|
const response = await apiRequestRaw(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
{ json: payload, throwHttpErrors: false },
|
||||||
|
operationName,
|
||||||
|
);
|
||||||
|
|
||||||
|
let data: unknown;
|
||||||
|
try {
|
||||||
|
data = await response.json();
|
||||||
|
} catch {
|
||||||
|
data = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return mapWorkOrderTableRow(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
|
||||||
|
|
||||||
|
if (response.status === 409) {
|
||||||
|
const currentStateRaw = record.currentState ?? record.CurrentState;
|
||||||
|
const currentState = currentStateRaw != null ? mapWorkOrderTableRow(currentStateRaw) : null;
|
||||||
|
throw new BoardPatchConflictError(
|
||||||
|
typeof record.message === "string"
|
||||||
|
? record.message
|
||||||
|
: "The record was modified by another user. Refresh and retry.",
|
||||||
|
currentState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 422) {
|
||||||
|
throw new BoardPatchValidationError(
|
||||||
|
typeof record.code === "string" ? record.code : "ValidationError",
|
||||||
|
typeof record.message === "string" ? record.message : mapHttpStatusToMessage(422, data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiError(mapHttpStatusToMessage(response.status, data), response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const workOrderBoardPatchApi = {
|
||||||
|
patchBoardField: async (
|
||||||
|
id: string | number,
|
||||||
|
payload: WorkOrderBoardPatchPayload,
|
||||||
|
): Promise<WorkOrderTableRow> =>
|
||||||
|
patchBoardRow(
|
||||||
|
"workOrdersApi.patchBoardField",
|
||||||
|
"patch",
|
||||||
|
API_PATHS.workOrder.boardPatch(id),
|
||||||
|
payload,
|
||||||
|
),
|
||||||
|
|
||||||
|
/** Manual POC override — same row/error contract as the board patch. */
|
||||||
|
updatePoc: async (
|
||||||
|
id: string | number,
|
||||||
|
payload: WorkOrderPocPatchPayload,
|
||||||
|
): Promise<WorkOrderTableRow> =>
|
||||||
|
patchBoardRow("workOrdersApi.updatePoc", "patch", API_PATHS.workOrder.pocPatch(id), payload),
|
||||||
|
};
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import { API_PATHS } from "@/api/api-paths";
|
import { API_PATHS } from "@/api/api-paths";
|
||||||
import { ApiError, mapHttpStatusToMessage } from "@/api/api-error";
|
import { apiDelete, apiGet, apiPost, apiPostForm, apiPut } from "@/api/api";
|
||||||
import { apiDelete, apiGet, apiPost, apiPostForm, apiPut, apiRequestRaw } from "@/api/api";
|
|
||||||
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
||||||
import {
|
|
||||||
BoardPatchConflictError,
|
|
||||||
BoardPatchValidationError,
|
|
||||||
} from "@/domain/work-orders/errors/board-patch-errors";
|
|
||||||
import { workOrderBoardDocumentsApi } from "@/domain/work-orders/api/work-order-board-documents-api";
|
import { workOrderBoardDocumentsApi } from "@/domain/work-orders/api/work-order-board-documents-api";
|
||||||
|
import { workOrderBoardPatchApi } from "@/domain/work-orders/api/work-order-board-patch-api";
|
||||||
import {
|
import {
|
||||||
mapDispatchComment,
|
mapDispatchComment,
|
||||||
mapDispatchDetail,
|
mapDispatchDetail,
|
||||||
|
|
@ -20,7 +16,6 @@ import {
|
||||||
mapWorkOrderDetail,
|
mapWorkOrderDetail,
|
||||||
mapWorkOrderFormToCreatePayload,
|
mapWorkOrderFormToCreatePayload,
|
||||||
mapWorkOrderFormToUpdateFormData,
|
mapWorkOrderFormToUpdateFormData,
|
||||||
mapWorkOrderTableRow,
|
|
||||||
mapWorkOrderTableRows,
|
mapWorkOrderTableRows,
|
||||||
} from "@/domain/work-orders/mappers/work-order-mapper";
|
} from "@/domain/work-orders/mappers/work-order-mapper";
|
||||||
import { mapWizardDraftToBoardCreatePayload } from "@/domain/work-orders/mappers/work-order-board-mapper";
|
import { mapWizardDraftToBoardCreatePayload } from "@/domain/work-orders/mappers/work-order-board-mapper";
|
||||||
|
|
@ -42,7 +37,6 @@ import type {
|
||||||
import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
|
import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
import type {
|
import type {
|
||||||
WorkOrderBoardListResult,
|
WorkOrderBoardListResult,
|
||||||
WorkOrderBoardPatchPayload,
|
|
||||||
WorkOrderBoardQueryParams,
|
WorkOrderBoardQueryParams,
|
||||||
WorkOrderBoardResponseApi,
|
WorkOrderBoardResponseApi,
|
||||||
WorkOrderBoardSearchQueryParams,
|
WorkOrderBoardSearchQueryParams,
|
||||||
|
|
@ -310,51 +304,10 @@ export const workOrdersApi = {
|
||||||
return mapWorkOrderDetail(handleApiResponse(data));
|
return mapWorkOrderDetail(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
patchBoardField: async (
|
patchBoardField: workOrderBoardPatchApi.patchBoardField,
|
||||||
id: string | number,
|
|
||||||
payload: WorkOrderBoardPatchPayload,
|
|
||||||
): Promise<WorkOrderTableRow> => {
|
|
||||||
const response = await apiRequestRaw(
|
|
||||||
"patch",
|
|
||||||
API_PATHS.workOrder.boardPatch(id),
|
|
||||||
{ json: payload, throwHttpErrors: false },
|
|
||||||
"workOrdersApi.patchBoardField",
|
|
||||||
);
|
|
||||||
|
|
||||||
let data: unknown;
|
/** Manual POC override — same row/error contract as the board patch. */
|
||||||
try {
|
updatePoc: workOrderBoardPatchApi.updatePoc,
|
||||||
data = await response.json();
|
|
||||||
} catch {
|
|
||||||
data = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return mapWorkOrderTableRow(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const record =
|
|
||||||
typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
|
|
||||||
|
|
||||||
if (response.status === 409) {
|
|
||||||
const currentStateRaw = record.currentState ?? record.CurrentState;
|
|
||||||
const currentState = currentStateRaw != null ? mapWorkOrderTableRow(currentStateRaw) : null;
|
|
||||||
throw new BoardPatchConflictError(
|
|
||||||
typeof record.message === "string"
|
|
||||||
? record.message
|
|
||||||
: "The record was modified by another user. Refresh and retry.",
|
|
||||||
currentState,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 422) {
|
|
||||||
throw new BoardPatchValidationError(
|
|
||||||
typeof record.code === "string" ? record.code : "ValidationError",
|
|
||||||
typeof record.message === "string" ? record.message : mapHttpStatusToMessage(422, data),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ApiError(mapHttpStatusToMessage(response.status, data), response.status);
|
|
||||||
},
|
|
||||||
|
|
||||||
update: async (
|
update: async (
|
||||||
id: string | number,
|
id: string | number,
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,6 @@ const LOCAL_ONLY_KEYS = new Set<keyof WorkOrderTablePatch>([
|
||||||
"techPhone",
|
"techPhone",
|
||||||
"location",
|
"location",
|
||||||
"locationId",
|
"locationId",
|
||||||
"pocName",
|
|
||||||
"pocPhone",
|
|
||||||
"pocNotes",
|
|
||||||
"title",
|
"title",
|
||||||
"priority",
|
"priority",
|
||||||
"apptStart",
|
"apptStart",
|
||||||
|
|
@ -53,16 +50,44 @@ export function isLocalOnlyTablePatch(patch: WorkOrderTablePatch): boolean {
|
||||||
return keys.length > 0 && keys.every((key) => LOCAL_ONLY_KEYS.has(key));
|
return keys.length > 0 && keys.every((key) => LOCAL_ONLY_KEYS.has(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Composite manual POC override op routed to PATCH …/poc. */
|
||||||
|
export type BoardPocOp = {
|
||||||
|
kind: "poc";
|
||||||
|
pocName?: string;
|
||||||
|
pocPhone?: string;
|
||||||
|
pocNotes?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BoardOp = BoardFieldOp | BoardPocOp;
|
||||||
|
|
||||||
|
function expandPocOp(patch: WorkOrderTablePatch): BoardPocOp[] {
|
||||||
|
const hasPocFields =
|
||||||
|
patch.pocName !== undefined || patch.pocPhone !== undefined || patch.pocNotes !== undefined;
|
||||||
|
if (!hasPocFields) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
kind: "poc",
|
||||||
|
pocName: patch.pocName,
|
||||||
|
pocPhone: patch.pocPhone,
|
||||||
|
pocNotes: patch.pocNotes,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expands a UI table patch into ordered board field ops (one PATCH each).
|
* Expands a UI table patch into ordered board ops (one request each).
|
||||||
* Schedule/week-only fields are emitted first in a stable order.
|
* Schedule/week-only fields are emitted first in a stable order; the composite
|
||||||
|
* POC op follows, then scalar fields.
|
||||||
*/
|
*/
|
||||||
export function expandTablePatchToBoardFieldOps(patch: WorkOrderTablePatch): BoardFieldOp[] {
|
export function expandTablePatchToBoardFieldOps(patch: WorkOrderTablePatch): BoardOp[] {
|
||||||
if (isLocalOnlyTablePatch(patch)) {
|
if (isLocalOnlyTablePatch(patch)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...expandScheduleBoardOps(patch), ...expandScalarBoardOps(patch)];
|
return [...expandScheduleBoardOps(patch), ...expandPocOp(patch), ...expandScalarBoardOps(patch)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDispatchBoardField(field: WorkOrderBoardEditableField): boolean {
|
export function isDispatchBoardField(field: WorkOrderBoardEditableField): boolean {
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,14 @@ export interface WorkOrderBoardPatchPayload {
|
||||||
primaryDispatchId?: number | null;
|
primaryDispatchId?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Payload for PATCH /workorders/{id}/poc (manual POC override). */
|
||||||
|
export interface WorkOrderPocPatchPayload {
|
||||||
|
pocName: string;
|
||||||
|
pocPhone: string;
|
||||||
|
pocNotes: string;
|
||||||
|
workOrderVersion: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkOrderBoardEditableField =
|
export type WorkOrderBoardEditableField =
|
||||||
| "woNumber"
|
| "woNumber"
|
||||||
| "workOrderType"
|
| "workOrderType"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
buildBoardPatchPayload,
|
buildBoardPatchPayload,
|
||||||
expandTablePatchToBoardFieldOps,
|
expandTablePatchToBoardFieldOps,
|
||||||
isLocalOnlyTablePatch,
|
isLocalOnlyTablePatch,
|
||||||
|
type BoardOp,
|
||||||
} from "@/domain/work-orders/mappers/work-order-board-patch-mapper";
|
} from "@/domain/work-orders/mappers/work-order-board-patch-mapper";
|
||||||
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-mapper";
|
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-mapper";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -53,6 +54,30 @@ export type WorkOrderTablePatchHandlers = {
|
||||||
type PatchResult =
|
type PatchResult =
|
||||||
{ kind: "local"; patch: WorkOrderTablePatch } | { kind: "board"; row: WorkOrderTableRow };
|
{ kind: "local"; patch: WorkOrderTablePatch } | { kind: "board"; row: WorkOrderTableRow };
|
||||||
|
|
||||||
|
function isPocOp(op: BoardOp): op is Extract<BoardOp, { kind: "poc" }> {
|
||||||
|
return "kind" in op && op.kind === "poc";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyBoardOp(
|
||||||
|
id: string | number,
|
||||||
|
op: BoardOp,
|
||||||
|
row: WorkOrderTableRow,
|
||||||
|
): Promise<WorkOrderTableRow> {
|
||||||
|
if (isPocOp(op)) {
|
||||||
|
if (!row.rowVersion) {
|
||||||
|
throw new Error("workOrderVersion is required for board patch");
|
||||||
|
}
|
||||||
|
return workOrdersApi.updatePoc(id, {
|
||||||
|
pocName: op.pocName ?? row.pocName,
|
||||||
|
pocPhone: op.pocPhone ?? row.pocPhone,
|
||||||
|
pocNotes: op.pocNotes ?? row.pocNotes,
|
||||||
|
workOrderVersion: row.rowVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return workOrdersApi.patchBoardField(id, buildBoardPatchPayload(op, row));
|
||||||
|
}
|
||||||
|
|
||||||
async function applyBoardFieldOps(
|
async function applyBoardFieldOps(
|
||||||
id: string | number,
|
id: string | number,
|
||||||
patch: WorkOrderTablePatch,
|
patch: WorkOrderTablePatch,
|
||||||
|
|
@ -72,7 +97,7 @@ async function applyBoardFieldOps(
|
||||||
let completedOps = 0;
|
let completedOps = 0;
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
try {
|
try {
|
||||||
latest = await workOrdersApi.patchBoardField(id, buildBoardPatchPayload(op, latest));
|
latest = await applyBoardOp(id, op, latest);
|
||||||
completedOps += 1;
|
completedOps += 1;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (completedOps > 0) {
|
if (completedOps > 0) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import type {
|
||||||
/**
|
/**
|
||||||
* Fields compared for dirty state and included in Save patch.
|
* Fields compared for dirty state and included in Save patch.
|
||||||
* Only keys that map to board PATCH (or required companions like dispatcherName).
|
* Only keys that map to board PATCH (or required companions like dispatcherName).
|
||||||
* Primary POC scalars and tech phone are not board-editable — exclude so Save cannot no-op.
|
* Primary POC scalars persist via the dedicated POC endpoint;
|
||||||
|
* tech phone is not board-editable — exclude so Save cannot no-op.
|
||||||
* additionalContacts is board-editable (SH-218 PATCH field).
|
* additionalContacts is board-editable (SH-218 PATCH field).
|
||||||
*/
|
*/
|
||||||
export const SLIDE_OVER_EDIT_KEYS = [
|
export const SLIDE_OVER_EDIT_KEYS = [
|
||||||
|
|
@ -32,6 +33,9 @@ export const SLIDE_OVER_EDIT_KEYS = [
|
||||||
"vendorId",
|
"vendorId",
|
||||||
// company/tech are display companions; board persists vendor via vendorId only
|
// company/tech are display companions; board persists vendor via vendorId only
|
||||||
"status",
|
"status",
|
||||||
|
"pocName",
|
||||||
|
"pocPhone",
|
||||||
|
"pocNotes",
|
||||||
] as const satisfies ReadonlyArray<keyof WorkOrderTableRow>;
|
] as const satisfies ReadonlyArray<keyof WorkOrderTableRow>;
|
||||||
|
|
||||||
type EditKey = (typeof SLIDE_OVER_EDIT_KEYS)[number];
|
type EditKey = (typeof SLIDE_OVER_EDIT_KEYS)[number];
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ function DayAssignHarness({
|
||||||
dispatcherId: id,
|
dispatcherId: id,
|
||||||
dispatcherName: name,
|
dispatcherName: name,
|
||||||
});
|
});
|
||||||
const patched = await patchBoardField(item.id, ops[0]!);
|
const patched = await patchBoardField(item.id, ops[0] as { field: string });
|
||||||
setRow({ ...patched, dispatcherId: id, dispatcherName: name });
|
setRow({ ...patched, dispatcherId: id, dispatcherName: name });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -658,6 +658,100 @@ describe("workOrdersApi.patchBoardField", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("workOrdersApi.updatePoc", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
apiPatchFn.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PATCHes poc payload to workorders/{id}/poc and maps row", async () => {
|
||||||
|
apiPatchFn.mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
id: 10,
|
||||||
|
siteCode: "BK5",
|
||||||
|
pocName: "Manual POC",
|
||||||
|
pocPhone: "312-555-0001",
|
||||||
|
pocNotes: "Call first",
|
||||||
|
lifecycleStatusLabel: "Scheduled",
|
||||||
|
rowVersion: "AAAAAAAB",
|
||||||
|
dispatchRowVersion: null,
|
||||||
|
primaryDispatchId: null,
|
||||||
|
vendorId: null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const row = await workOrdersApi.updatePoc(10, {
|
||||||
|
pocName: "Manual POC",
|
||||||
|
pocPhone: "312-555-0001",
|
||||||
|
pocNotes: "Call first",
|
||||||
|
workOrderVersion: "AAAAAAA=",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(apiPatchFn).toHaveBeenCalledWith(API_PATHS.workOrder.pocPatch(10), {
|
||||||
|
json: {
|
||||||
|
pocName: "Manual POC",
|
||||||
|
pocPhone: "312-555-0001",
|
||||||
|
pocNotes: "Call first",
|
||||||
|
workOrderVersion: "AAAAAAA=",
|
||||||
|
},
|
||||||
|
throwHttpErrors: false,
|
||||||
|
});
|
||||||
|
expect(row.pocName).toBe("Manual POC");
|
||||||
|
expect(row.rowVersion).toBe("AAAAAAAB");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws BoardPatchConflictError with currentState on 409", async () => {
|
||||||
|
apiPatchFn.mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 409,
|
||||||
|
json: async () => ({
|
||||||
|
message: "The record was modified by another user. Refresh and retry.",
|
||||||
|
currentState: {
|
||||||
|
id: 10,
|
||||||
|
siteCode: "BK5",
|
||||||
|
rowVersion: "NEWVER==",
|
||||||
|
lifecycleStatusLabel: "Scheduled",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { BoardPatchConflictError } =
|
||||||
|
await import("@/domain/work-orders/errors/board-patch-errors");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
workOrdersApi.updatePoc(10, {
|
||||||
|
pocName: "Manual POC",
|
||||||
|
pocPhone: "312-555-0001",
|
||||||
|
pocNotes: "",
|
||||||
|
workOrderVersion: "STALE==",
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(BoardPatchConflictError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws BoardPatchValidationError on 422 ReadOnly", async () => {
|
||||||
|
apiPatchFn.mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 422,
|
||||||
|
json: async () => ({
|
||||||
|
code: "ReadOnly",
|
||||||
|
message: "Work order is read-only in its current status.",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
workOrdersApi.updatePoc(10, {
|
||||||
|
pocName: "Manual POC",
|
||||||
|
pocPhone: "312-555-0001",
|
||||||
|
pocNotes: "",
|
||||||
|
workOrderVersion: "AAAAAAA=",
|
||||||
|
}),
|
||||||
|
).rejects.toMatchObject({
|
||||||
|
name: "BoardPatchValidationError",
|
||||||
|
code: "ReadOnly",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("workOrdersApi.createComment", () => {
|
describe("workOrdersApi.createComment", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
apiPost.mockReset();
|
apiPost.mockReset();
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,12 @@ describe("isLocalOnlyTablePatch", () => {
|
||||||
it("treats scheduleEnd and schedMode as local-only overlays", () => {
|
it("treats scheduleEnd and schedMode as local-only overlays", () => {
|
||||||
expect(isLocalOnlyTablePatch({ scheduleEnd: "2026-06-12", schedMode: "range" })).toBe(true);
|
expect(isLocalOnlyTablePatch({ scheduleEnd: "2026-06-12", schedMode: "range" })).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not treat POC edits as local-only", () => {
|
||||||
|
expect(isLocalOnlyTablePatch({ pocName: "Pat", pocPhone: "111", pocNotes: "call" })).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("expandTablePatchToBoardFieldOps", () => {
|
describe("expandTablePatchToBoardFieldOps", () => {
|
||||||
|
|
@ -185,7 +191,33 @@ describe("expandTablePatchToBoardFieldOps", () => {
|
||||||
pocPhone: "111",
|
pocPhone: "111",
|
||||||
pocNotes: "call",
|
pocNotes: "call",
|
||||||
}),
|
}),
|
||||||
).toEqual([{ field: "siteCode", value: "BK5" }]);
|
).toEqual([
|
||||||
|
{
|
||||||
|
kind: "poc",
|
||||||
|
pocName: "Pat",
|
||||||
|
pocPhone: "111",
|
||||||
|
pocNotes: "call",
|
||||||
|
},
|
||||||
|
{ field: "siteCode", value: "BK5" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits one composite POC op before scalar ops", () => {
|
||||||
|
expect(
|
||||||
|
expandTablePatchToBoardFieldOps({
|
||||||
|
pocName: "Pat",
|
||||||
|
pocNotes: "call first",
|
||||||
|
docStatus: "Yes",
|
||||||
|
}),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
kind: "poc",
|
||||||
|
pocName: "Pat",
|
||||||
|
pocPhone: undefined,
|
||||||
|
pocNotes: "call first",
|
||||||
|
},
|
||||||
|
{ field: "docStatus", value: "Yes" },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps dueDate and scheduledDate patches independent", () => {
|
it("keeps dueDate and scheduledDate patches independent", () => {
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ import type {
|
||||||
|
|
||||||
const getById = vi.fn();
|
const getById = vi.fn();
|
||||||
const update = vi.fn();
|
const update = vi.fn();
|
||||||
|
const updatePoc = vi.fn();
|
||||||
const patchBoardField = vi.fn();
|
const patchBoardField = vi.fn();
|
||||||
|
|
||||||
vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
|
vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
|
||||||
workOrdersApi: {
|
workOrdersApi: {
|
||||||
getById: (...args: unknown[]) => getById(...args),
|
getById: (...args: unknown[]) => getById(...args),
|
||||||
update: (...args: unknown[]) => update(...args),
|
update: (...args: unknown[]) => update(...args),
|
||||||
|
updatePoc: (...args: unknown[]) => updatePoc(...args),
|
||||||
patchBoardField: (...args: unknown[]) => patchBoardField(...args),
|
patchBoardField: (...args: unknown[]) => patchBoardField(...args),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
@ -121,6 +123,7 @@ describe("useWorkOrderTableMutations completion-doc persistence", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getById.mockReset();
|
getById.mockReset();
|
||||||
update.mockReset();
|
update.mockReset();
|
||||||
|
updatePoc.mockReset();
|
||||||
patchBoardField.mockReset();
|
patchBoardField.mockReset();
|
||||||
serverState = { ...BASE_SERVER };
|
serverState = { ...BASE_SERVER };
|
||||||
rowState = { ...BASE_ROW };
|
rowState = { ...BASE_ROW };
|
||||||
|
|
@ -829,6 +832,40 @@ describe("useWorkOrderTableMutations type and severity", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("useWorkOrderTableMutations POC persistence", () => {
|
||||||
|
it("fills omitted POC fields from the current row before the full replacement", async () => {
|
||||||
|
const currentRow = {
|
||||||
|
...BASE_ROW,
|
||||||
|
pocName: "Alex",
|
||||||
|
pocPhone: "555-0000",
|
||||||
|
pocNotes: "Gate",
|
||||||
|
};
|
||||||
|
updatePoc.mockResolvedValue({ ...currentRow, pocName: "Jordan", rowVersion: "wo-v2" });
|
||||||
|
|
||||||
|
const { result } = renderHook(
|
||||||
|
() =>
|
||||||
|
useWorkOrderTableMutations({
|
||||||
|
onPatch: vi.fn(),
|
||||||
|
clearPatch: () => undefined,
|
||||||
|
getRow: () => currentRow,
|
||||||
|
}),
|
||||||
|
{ wrapper: makeWrapper() },
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.patchField(42, { pocName: "Jordan" });
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(updatePoc).toHaveBeenCalled());
|
||||||
|
expect(updatePoc).toHaveBeenCalledWith(42, {
|
||||||
|
pocName: "Jordan",
|
||||||
|
pocPhone: "555-0000",
|
||||||
|
pocNotes: "Gate",
|
||||||
|
workOrderVersion: "AAAAAAA=",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("useWorkOrderTableMutations audit cache refresh", () => {
|
describe("useWorkOrderTableMutations audit cache refresh", () => {
|
||||||
it("invalidates work-order queries after a successful status transition", async () => {
|
it("invalidates work-order queries after a successful status transition", async () => {
|
||||||
const client = new QueryClient({
|
const client = new QueryClient({
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,9 @@ describe("slide-over-draft", () => {
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("vendorId");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("vendorId");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("additionalContacts");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("additionalContacts");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("avetaRequired");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("avetaRequired");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("pocName");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocName");
|
||||||
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocPhone");
|
||||||
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocNotes");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("locationId");
|
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("locationId");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("techPhone");
|
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("techPhone");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("company");
|
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("company");
|
||||||
|
|
@ -85,11 +87,11 @@ describe("slide-over-draft", () => {
|
||||||
expect(buildSlideOverPatch(vendorDraft, row)).toEqual({ vendorId: "v2" });
|
expect(buildSlideOverPatch(vendorDraft, row)).toEqual({ vendorId: "v2" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores POC-only edits for dirty state", () => {
|
it("tracks POC edits for dirty state and patch", () => {
|
||||||
const row = baseRow();
|
const row = baseRow();
|
||||||
const draft = baseRow({ pocName: "Jordan", pocPhone: "555-0100" });
|
const draft = baseRow({ pocName: "Jordan", pocPhone: "555-0100" });
|
||||||
expect(isSlideOverDraftDirty(draft, row)).toBe(false);
|
expect(isSlideOverDraftDirty(draft, row)).toBe(true);
|
||||||
expect(buildSlideOverPatch(draft, row)).toEqual({});
|
expect(buildSlideOverPatch(draft, row)).toEqual({ pocName: "Jordan", pocPhone: "555-0100" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects dirty draft when a tracked field changes", () => {
|
it("detects dirty draft when a tracked field changes", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue