mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-03 10:23:21 +00:00
fix(work-orders): ignore inactive primary dispatch on board closability (SH-183)
Board Completed must not treat Cancelled/Canceled/Refused leftover vendorId as a live company assignment.
This commit is contained in:
parent
854267e213
commit
e9b00bab57
9 changed files with 140 additions and 16 deletions
|
|
@ -10,6 +10,7 @@ import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-ta
|
|||
import {
|
||||
formatClosabilityMissingTitle,
|
||||
getClosabilityGaps,
|
||||
tableRowToClosabilityInput,
|
||||
} from "@/domain/work-orders/utils/wo-closability";
|
||||
import { HoverChevron } from "./hover-chevron";
|
||||
import { WoTableStatusLabel } from "./wo-table-status-label";
|
||||
|
|
@ -22,21 +23,7 @@ type StatusCellProps = {
|
|||
};
|
||||
|
||||
function rowClosabilityGaps(wo: WorkOrderTableRow) {
|
||||
return getClosabilityGaps({
|
||||
dispatcherId: wo.dispatcherId,
|
||||
dispatcherName: wo.dispatcherName,
|
||||
company: wo.company,
|
||||
vendorId: wo.vendorId ?? "",
|
||||
tech: wo.tech,
|
||||
completedDate: wo.completedDate ?? "",
|
||||
woNumber: wo.woNumber,
|
||||
pm: wo.pm,
|
||||
docStatus: wo.docStatus,
|
||||
mediaCount: wo.mediaCount,
|
||||
type: wo.type,
|
||||
upliftSummary: wo.upliftSummary,
|
||||
hasPendingUplift: wo.hasPendingUplift,
|
||||
});
|
||||
return getClosabilityGaps(tableRowToClosabilityInput(wo));
|
||||
}
|
||||
|
||||
function PastDueStatusCell({
|
||||
|
|
|
|||
|
|
@ -267,6 +267,18 @@ function readHasPendingUplift(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function readPrimaryDispatchStatus(item: Record<string, unknown>): string {
|
||||
return readString(
|
||||
item,
|
||||
"primaryDispatchStatus",
|
||||
"PrimaryDispatchStatus",
|
||||
"dispatchStatus",
|
||||
"DispatchStatus",
|
||||
"vendorDispatchStatus",
|
||||
"VendorDispatchStatus",
|
||||
);
|
||||
}
|
||||
|
||||
export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
|
||||
const item = asRecord(raw);
|
||||
const scheduledOn = toDateInputValue(
|
||||
|
|
@ -348,6 +360,7 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
|
|||
extraServices: readStringArray(item, "extraServices", "ExtraServices"),
|
||||
vendorId: vendorIdNum != null ? String(vendorIdNum) : "",
|
||||
company: readString(item, "company", "Company", "vendorName", "VendorName"),
|
||||
primaryDispatchStatus: readPrimaryDispatchStatus(item),
|
||||
tech: readString(item, "techName", "TechName", "tech", "Tech", "technician", "Technician"),
|
||||
techPhone: readString(item, "techPhone", "TechPhone"),
|
||||
apptStart,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ export interface WorkOrderBoardDetailInfoApi {
|
|||
dayGroup?: string | null;
|
||||
vendorId?: number | null;
|
||||
vendorName?: string | null;
|
||||
/** Primary dispatch status when the board contract supplies it (SH-183). */
|
||||
primaryDispatchStatus?: string | null;
|
||||
dispatchStatus?: string | null;
|
||||
vendorDispatchStatus?: string | null;
|
||||
techName?: string | null;
|
||||
techPhone?: string | null;
|
||||
apptDate?: string | null;
|
||||
|
|
|
|||
|
|
@ -184,6 +184,10 @@ export interface WorkOrderBoardRowApi {
|
|||
dayGroup?: string | null;
|
||||
vendorId?: number | null;
|
||||
vendorName?: string | null;
|
||||
/** Primary dispatch status when the board contract supplies it (SH-183). */
|
||||
primaryDispatchStatus?: string | null;
|
||||
dispatchStatus?: string | null;
|
||||
vendorDispatchStatus?: string | null;
|
||||
techName?: string | null;
|
||||
techPhone?: string | null;
|
||||
apptDate?: string | null;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ export interface WorkOrderTableRow {
|
|||
extraServices: string[];
|
||||
vendorId: string;
|
||||
company: string;
|
||||
/** Primary dispatch status when the board contract supplies it (SH-183). */
|
||||
primaryDispatchStatus?: string;
|
||||
tech: string;
|
||||
techPhone: string;
|
||||
apptStart: string;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { WorkOrderDispatchSummary } from "@/domain/work-orders/types/dispat
|
|||
import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-detail";
|
||||
import type {
|
||||
CompletionDocStatus,
|
||||
WorkOrderTableRow,
|
||||
WorkOrderUpliftSummary,
|
||||
} from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { WOType, WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
|
||||
|
|
@ -160,10 +161,30 @@ export function wizardDraftToClosabilityInput(draft: WorkOrderWizardDraft): WoCl
|
|||
/** Backend terminal-cancel spellings; dispatch status is a raw API string. */
|
||||
const INACTIVE_DISPATCH_STATUSES = new Set(["cancelled", "canceled", "refused"]);
|
||||
|
||||
function isInactiveDispatchStatus(status: string | undefined): boolean {
|
||||
export function isInactiveDispatchStatus(status: string | undefined): boolean {
|
||||
return INACTIVE_DISPATCH_STATUSES.has((status ?? "").trim().toLowerCase());
|
||||
}
|
||||
|
||||
/** Map board row onto closability; inactive primary dispatch is not a live company (SH-183). */
|
||||
export function tableRowToClosabilityInput(wo: WorkOrderTableRow): WoClosabilityInput {
|
||||
const inactive = isInactiveDispatchStatus(wo.primaryDispatchStatus);
|
||||
return {
|
||||
dispatcherId: wo.dispatcherId,
|
||||
dispatcherName: wo.dispatcherName,
|
||||
company: inactive ? "" : wo.company,
|
||||
vendorId: inactive ? "" : (wo.vendorId ?? ""),
|
||||
tech: wo.tech,
|
||||
completedDate: wo.completedDate ?? "",
|
||||
woNumber: wo.woNumber,
|
||||
pm: wo.pm,
|
||||
docStatus: wo.docStatus,
|
||||
mediaCount: wo.mediaCount,
|
||||
type: wo.type,
|
||||
upliftSummary: wo.upliftSummary,
|
||||
hasPendingUplift: wo.hasPendingUplift,
|
||||
};
|
||||
}
|
||||
|
||||
/** Current vendor assignment — cancelled/refused dispatches do not count as company (SH-183). */
|
||||
function currentAssignedDispatch(
|
||||
dispatches: WorkOrderDispatchSummary[],
|
||||
|
|
|
|||
|
|
@ -159,6 +159,53 @@ describe("StatusCell pending uplift closability", () => {
|
|||
expect(onChangeStatus).toHaveBeenCalledWith("Completed");
|
||||
});
|
||||
|
||||
it("allows Completed when vendorId has an empty company label and a live dispatch status (SH-183)", () => {
|
||||
const onChangeStatus = vi.fn();
|
||||
|
||||
renderWithProviders(
|
||||
<StatusCell
|
||||
wo={makeRow({ company: "", vendorId: "7", tech: "", primaryDispatchStatus: "Sent" })}
|
||||
onChangeStatus={onChangeStatus}
|
||||
onOpenSlideOver={vi.fn()}
|
||||
/>,
|
||||
{ withAuth: false },
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /in progress/i }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /completed/i }));
|
||||
|
||||
expect(onChangeStatus).toHaveBeenCalledWith("Completed");
|
||||
});
|
||||
|
||||
it.each(["Cancelled", "Canceled", "Refused"] as const)(
|
||||
"disables Completed when vendorId is leftover from a %s dispatch (SH-183)",
|
||||
(primaryDispatchStatus) => {
|
||||
const onChangeStatus = vi.fn();
|
||||
|
||||
renderWithProviders(
|
||||
<StatusCell
|
||||
wo={makeRow({
|
||||
company: "",
|
||||
vendorId: "45",
|
||||
tech: "",
|
||||
primaryDispatchStatus,
|
||||
})}
|
||||
onChangeStatus={onChangeStatus}
|
||||
onOpenSlideOver={vi.fn()}
|
||||
/>,
|
||||
{ withAuth: false },
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /in progress/i }));
|
||||
|
||||
const completed = screen.getByRole("button", { name: /completed/i });
|
||||
expect(completed).toBeDisabled();
|
||||
expect(completed).toHaveAttribute("title", "Missing: Company");
|
||||
fireEvent.click(completed);
|
||||
expect(onChangeStatus).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("allows Completed when technician is empty if company is set (SH-183)", () => {
|
||||
const onChangeStatus = vi.fn();
|
||||
|
||||
|
|
|
|||
|
|
@ -50,3 +50,15 @@ describe("mapWorkOrderTableRow isAddOn", () => {
|
|||
expect(row.isAddOn).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapWorkOrderTableRow primaryDispatchStatus", () => {
|
||||
it.each([
|
||||
["primaryDispatchStatus", "Refused"],
|
||||
["dispatchStatus", "Canceled"],
|
||||
["vendorDispatchStatus", "Cancelled"],
|
||||
] as const)("maps %s onto primaryDispatchStatus", (key, value) => {
|
||||
const row = mapWorkOrderTableRow({ id: 9, vendorId: 45, [key]: value });
|
||||
expect(row.vendorId).toBe("45");
|
||||
expect(row.primaryDispatchStatus).toBe(value);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { readDetailWoType } from "@/domain/work-orders/utils/read-detail-closability-fields";
|
||||
import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-detail";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { EMPTY_WIZARD_DRAFT } from "@/domain/work-orders/types/work-order-wizard";
|
||||
import type { WoClosabilityInput } from "@/domain/work-orders/utils/wo-closability";
|
||||
import {
|
||||
|
|
@ -13,6 +14,7 @@ import {
|
|||
hasPendingUplift,
|
||||
isCompletionStatusLabel,
|
||||
isRealWoNumber,
|
||||
tableRowToClosabilityInput,
|
||||
wizardDraftToClosabilityInput,
|
||||
} from "@/domain/work-orders/utils/wo-closability";
|
||||
|
||||
|
|
@ -396,6 +398,38 @@ describe("detailToClosabilityInput", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("tableRowToClosabilityInput", () => {
|
||||
const closableRow = {
|
||||
dispatcherId: "u1",
|
||||
dispatcherName: "Alex",
|
||||
company: "",
|
||||
vendorId: "45",
|
||||
tech: "",
|
||||
completedDate: "2026-07-15",
|
||||
woNumber: "20260623001",
|
||||
pm: "HVAC PM",
|
||||
docStatus: "",
|
||||
mediaCount: 0,
|
||||
type: "",
|
||||
} as WorkOrderTableRow;
|
||||
|
||||
it("keeps vendorId as a live assignment when primary dispatch status is absent (SH-183)", () => {
|
||||
const input = tableRowToClosabilityInput(closableRow);
|
||||
expect(input.vendorId).toBe("45");
|
||||
expect(getClosabilityGaps(input)).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(["Cancelled", "Canceled", "Refused"] as const)(
|
||||
"clears company assignment when primary dispatch status is %s (SH-183)",
|
||||
(primaryDispatchStatus) => {
|
||||
const input = tableRowToClosabilityInput({ ...closableRow, primaryDispatchStatus });
|
||||
expect(input.company).toBe("");
|
||||
expect(input.vendorId).toBe("");
|
||||
expect(getClosabilityGaps(input)).toContain("Company");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("readDetailWoType", () => {
|
||||
it.each(["PM", "Reactive", "Emergency", "Overdue"])(
|
||||
"passes through the business type %s",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue