mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 18:12:05 +00:00
fix(work-orders): use authoritative type/doc/media for detail closability
Stop treating integration source as WorkOrderType. Enrich detail from board detail for SH-114/SH-183 gates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8cd6d92ba7
commit
5e3b7b8057
11 changed files with 321 additions and 17 deletions
|
|
@ -1,17 +1,22 @@
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useWorkOrderDetail } from "@/domain/work-orders/use-cases/use-work-order-detail";
|
import { useVendorEditor } from "@/app/(protected)/workorders/_hooks/use-vendor-editor";
|
||||||
|
import {
|
||||||
|
useWorkOrderBoardDetail,
|
||||||
|
useWorkOrderDetail,
|
||||||
|
} from "@/domain/work-orders/use-cases/use-work-order-detail";
|
||||||
import {
|
import {
|
||||||
useAddWorkOrderComment,
|
useAddWorkOrderComment,
|
||||||
useChangeWorkOrderAssignment,
|
useChangeWorkOrderAssignment,
|
||||||
useChangeWorkOrderStatus,
|
useChangeWorkOrderStatus,
|
||||||
} from "@/domain/work-orders/use-cases/use-work-order-mutations";
|
} from "@/domain/work-orders/use-cases/use-work-order-mutations";
|
||||||
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list";
|
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list";
|
||||||
import { useVendorEditor } from "@/app/(protected)/workorders/_hooks/use-vendor-editor";
|
import { enrichDetailClosabilityFromBoard } from "@/domain/work-orders/utils/enrich-detail-closability-from-board";
|
||||||
|
|
||||||
export type WorkOrderActivityTab = "customer" | "internal" | "audit";
|
export type WorkOrderActivityTab = "customer" | "internal" | "audit";
|
||||||
|
|
||||||
export function useWorkOrderViewController(workOrderId: string) {
|
export function useWorkOrderViewController(workOrderId: string) {
|
||||||
const { data: wo, isLoading, error, refetch } = useWorkOrderDetail(workOrderId);
|
const { data: detail, isLoading, error, refetch } = useWorkOrderDetail(workOrderId);
|
||||||
|
const { data: boardDetail } = useWorkOrderBoardDetail(workOrderId);
|
||||||
const { data: users = [] } = useWorkOrderUsers();
|
const { data: users = [] } = useWorkOrderUsers();
|
||||||
const statusMutation = useChangeWorkOrderStatus(workOrderId);
|
const statusMutation = useChangeWorkOrderStatus(workOrderId);
|
||||||
const assignmentMutation = useChangeWorkOrderAssignment(workOrderId);
|
const assignmentMutation = useChangeWorkOrderAssignment(workOrderId);
|
||||||
|
|
@ -25,6 +30,11 @@ export function useWorkOrderViewController(workOrderId: string) {
|
||||||
const [statusAnchor, setStatusAnchor] = useState<HTMLElement | null>(null);
|
const [statusAnchor, setStatusAnchor] = useState<HTMLElement | null>(null);
|
||||||
const [assignAnchor, setAssignAnchor] = useState<HTMLElement | null>(null);
|
const [assignAnchor, setAssignAnchor] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const wo = useMemo(
|
||||||
|
() => (detail ? enrichDetailClosabilityFromBoard(detail, boardDetail) : undefined),
|
||||||
|
[detail, boardDetail],
|
||||||
|
);
|
||||||
|
|
||||||
const customerComments = useMemo(
|
const customerComments = useMemo(
|
||||||
() => (wo?.comments ?? []).filter((comment) => comment.commentType === "customer"),
|
() => (wo?.comments ?? []).filter((comment) => comment.commentType === "customer"),
|
||||||
[wo?.comments],
|
[wo?.comments],
|
||||||
|
|
|
||||||
101
src/domain/work-orders/mappers/read-detail-closability-fields.ts
Normal file
101
src/domain/work-orders/mappers/read-detail-closability-fields.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import {
|
||||||
|
DOC_STATUS_API,
|
||||||
|
WORK_ORDER_TYPE_API_TO_FE,
|
||||||
|
} from "@/domain/work-orders/types/work-order-board";
|
||||||
|
import type { CompletionDocStatus } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
|
|
||||||
|
const DETAIL_WO_TYPES = new Set(["PM", "Reactive", "Emergency", "Add-On", "Overdue"]);
|
||||||
|
|
||||||
|
function asRecord(raw: unknown): Record<string, unknown> {
|
||||||
|
return typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readString(record: Record<string, unknown>, ...keys: string[]): string {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNumber(record: Record<string, unknown>, ...keys: string[]): number | null {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === "string" && value !== "") {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isNaN(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Authoritative WO type only — never falls back to integration `source`. */
|
||||||
|
export function readDetailWoType(record: Record<string, unknown>): WOType | "" {
|
||||||
|
const numeric =
|
||||||
|
readNumber(record, "workOrderType", "WorkOrderType") ?? readNumber(record, "type", "Type");
|
||||||
|
if (numeric != null && WORK_ORDER_TYPE_API_TO_FE[numeric]) {
|
||||||
|
return WORK_ORDER_TYPE_API_TO_FE[numeric] as WOType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = readString(
|
||||||
|
record,
|
||||||
|
"workOrderType",
|
||||||
|
"WorkOrderType",
|
||||||
|
"type",
|
||||||
|
"Type",
|
||||||
|
"woType",
|
||||||
|
"WoType",
|
||||||
|
);
|
||||||
|
if (DETAIL_WO_TYPES.has(raw)) {
|
||||||
|
return raw as WOType;
|
||||||
|
}
|
||||||
|
if (raw === "AddOn") {
|
||||||
|
return "Add-On";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readDetailDocStatus(record: Record<string, unknown>): CompletionDocStatus {
|
||||||
|
const numeric = readNumber(record, "docStatus", "DocStatus");
|
||||||
|
if (numeric === DOC_STATUS_API.Yes) return "Yes";
|
||||||
|
if (numeric === DOC_STATUS_API.No) return "No";
|
||||||
|
if (numeric === DOC_STATUS_API.NotNeeded) return "NN";
|
||||||
|
|
||||||
|
const nestedCompletion = asRecord(record.completion ?? record.Completion);
|
||||||
|
const nestedNumeric = readNumber(nestedCompletion, "docStatus", "DocStatus");
|
||||||
|
if (nestedNumeric === DOC_STATUS_API.Yes) return "Yes";
|
||||||
|
if (nestedNumeric === DOC_STATUS_API.No) return "No";
|
||||||
|
if (nestedNumeric === DOC_STATUS_API.NotNeeded) return "NN";
|
||||||
|
|
||||||
|
const raw = readString(record, "docStatus", "DocStatus", "completionDoc", "CompletionDoc");
|
||||||
|
if (raw === "Yes" || raw === "No" || raw === "NN") {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
if (raw === "NotNeeded") {
|
||||||
|
return "NN";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readDetailMediaCount(record: Record<string, unknown>): number {
|
||||||
|
const explicit = readNumber(record, "mediaCount", "MediaCount");
|
||||||
|
if (explicit != null) {
|
||||||
|
return explicit;
|
||||||
|
}
|
||||||
|
const media = record.media ?? record.Media;
|
||||||
|
if (Array.isArray(media)) {
|
||||||
|
return media.length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
import { parseSiteCodeFromLocationName } from "@/domain/work-orders/mappers/work-order-board-mapper";
|
import { parseSiteCodeFromLocationName } from "@/domain/work-orders/mappers/work-order-board-mapper";
|
||||||
|
import {
|
||||||
|
readDetailDocStatus,
|
||||||
|
readDetailMediaCount,
|
||||||
|
readDetailWoType,
|
||||||
|
} from "@/domain/work-orders/mappers/read-detail-closability-fields";
|
||||||
import type {
|
import type {
|
||||||
EmployeeOption,
|
EmployeeOption,
|
||||||
LocationOption,
|
LocationOption,
|
||||||
|
|
@ -304,6 +309,9 @@ export function mapWorkOrderDetail(raw: unknown): WorkOrderDetail {
|
||||||
scheduledDate: toDateInputValue(readString(wo, "scheduledDate", "ScheduledDate")),
|
scheduledDate: toDateInputValue(readString(wo, "scheduledDate", "ScheduledDate")),
|
||||||
completedDate: toDateInputValue(readString(wo, "completedDate", "CompletedDate")),
|
completedDate: toDateInputValue(readString(wo, "completedDate", "CompletedDate")),
|
||||||
source: readString(wo, "source", "Source"),
|
source: readString(wo, "source", "Source"),
|
||||||
|
type: readDetailWoType(wo),
|
||||||
|
docStatus: readDetailDocStatus(wo),
|
||||||
|
mediaCount: readDetailMediaCount(wo),
|
||||||
...(contactIds !== undefined ? { contactIds } : {}),
|
...(contactIds !== undefined ? { contactIds } : {}),
|
||||||
...(categoryIds !== undefined ? { categoryIds } : {}),
|
...(categoryIds !== undefined ? { categoryIds } : {}),
|
||||||
comments: Array.isArray(wo.comments)
|
comments: Array.isArray(wo.comments)
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,9 @@ export function buildMockWorkOrderDetail(values: WorkOrderFormValues): WorkOrder
|
||||||
scheduledDate: values.scheduledDate,
|
scheduledDate: values.scheduledDate,
|
||||||
completedDate: values.completedDate,
|
completedDate: values.completedDate,
|
||||||
source: values.source,
|
source: values.source,
|
||||||
|
type: "",
|
||||||
|
docStatus: "",
|
||||||
|
mediaCount: 0,
|
||||||
comments: [],
|
comments: [],
|
||||||
auditLog: [],
|
auditLog: [],
|
||||||
dispatches: [],
|
dispatches: [],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import type { WorkOrderCommentType, WorkOrderPriority, WorkOrderStatus } from "./work-order-common";
|
import type { WorkOrderCommentType, WorkOrderPriority, WorkOrderStatus } from "./work-order-common";
|
||||||
import type { WorkOrderDispatchSummary } from "./dispatch";
|
import type { WorkOrderDispatchSummary } from "./dispatch";
|
||||||
|
import type { CompletionDocStatus } from "./work-order-table-row";
|
||||||
|
import type { WOType } from "./work-order-wizard";
|
||||||
|
|
||||||
export interface WorkOrderComment {
|
export interface WorkOrderComment {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
|
|
@ -50,6 +52,12 @@ export interface WorkOrderDetail {
|
||||||
scheduledDate: string;
|
scheduledDate: string;
|
||||||
completedDate: string;
|
completedDate: string;
|
||||||
source: string;
|
source: string;
|
||||||
|
/** Authoritative WO type (not integration `source`). Empty when unknown. */
|
||||||
|
type: WOType | "";
|
||||||
|
/** Completion document status for PM/Overdue closability gates. */
|
||||||
|
docStatus: CompletionDocStatus;
|
||||||
|
/** Photo/media count for Emergency/Reactive closability gates. */
|
||||||
|
mediaCount: number;
|
||||||
/** Present when GetById returns contacts; used to re-send on edit so backend does not wipe. */
|
/** Present when GetById returns contacts; used to re-send on edit so backend does not wipe. */
|
||||||
contactIds?: number[];
|
contactIds?: number[];
|
||||||
/** Present when GetById returns categories; used to re-send on edit so backend does not wipe. */
|
/** Present when GetById returns categories; used to re-send on edit so backend does not wipe. */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import type { WorkOrderBoardDetail } from "@/domain/work-orders/types/work-order-board-detail";
|
||||||
|
import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-detail";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overlay authoritative closability fields from board detail onto a legacy GetById detail.
|
||||||
|
* Board `info.type`, `completion.docStatus`, and `media.length` are the SH-114/SH-183 sources.
|
||||||
|
*/
|
||||||
|
export function enrichDetailClosabilityFromBoard(
|
||||||
|
detail: WorkOrderDetail,
|
||||||
|
board: WorkOrderBoardDetail | undefined,
|
||||||
|
): WorkOrderDetail {
|
||||||
|
if (!board) {
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...detail,
|
||||||
|
type: board.info.type,
|
||||||
|
docStatus: board.completion.docStatus,
|
||||||
|
mediaCount: board.media.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-detail";
|
import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-detail";
|
||||||
import type { CompletionDocStatus } from "@/domain/work-orders/types/work-order-table-row";
|
import type { CompletionDocStatus } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import {
|
import {
|
||||||
WO_TYPES,
|
|
||||||
type WOType,
|
type WOType,
|
||||||
type WorkOrderWizardDraft,
|
type WorkOrderWizardDraft,
|
||||||
} from "@/domain/work-orders/types/work-order-wizard";
|
} from "@/domain/work-orders/types/work-order-wizard";
|
||||||
|
|
@ -110,15 +109,10 @@ export function wizardDraftToClosabilityInput(draft: WorkOrderWizardDraft): WoCl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function detailSourceToWoType(source: string): WOType | "" {
|
|
||||||
return (WO_TYPES as readonly string[]).includes(source) ? (source as WOType) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map standalone detail DTO onto closability. `problem` is the same service
|
* Map standalone detail DTO onto closability. Uses authoritative `type` / `docStatus` /
|
||||||
* field the board maps into `pm`; `source` carries wizard WO type when set.
|
* `mediaCount` (enriched from board detail on the view route). Never treats `source`
|
||||||
* docStatus/media stay empty (detail route does not expose them) so those
|
* as WorkOrderType. `problem` is the same service field the board maps into `pm`.
|
||||||
* type-specific gates still fail closed.
|
|
||||||
*/
|
*/
|
||||||
export function detailToClosabilityInput(wo: WorkOrderDetail): WoClosabilityInput {
|
export function detailToClosabilityInput(wo: WorkOrderDetail): WoClosabilityInput {
|
||||||
const vendorName = wo.dispatches.find((dispatch) =>
|
const vendorName = wo.dispatches.find((dispatch) =>
|
||||||
|
|
@ -132,8 +126,8 @@ export function detailToClosabilityInput(wo: WorkOrderDetail): WoClosabilityInpu
|
||||||
completedDate: wo.completedDate,
|
completedDate: wo.completedDate,
|
||||||
woNumber: wo.workOrderNumber || wo.internalWONumber,
|
woNumber: wo.workOrderNumber || wo.internalWONumber,
|
||||||
pm: wo.problem,
|
pm: wo.problem,
|
||||||
docStatus: "",
|
docStatus: wo.docStatus,
|
||||||
mediaCount: 0,
|
mediaCount: wo.mediaCount,
|
||||||
type: detailSourceToWoType(wo.source),
|
type: wo.type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ const incompleteDetail = {
|
||||||
scheduledDate: "",
|
scheduledDate: "",
|
||||||
completedDate: "",
|
completedDate: "",
|
||||||
source: "",
|
source: "",
|
||||||
|
type: "",
|
||||||
|
docStatus: "",
|
||||||
|
mediaCount: 0,
|
||||||
comments: [],
|
comments: [],
|
||||||
auditLog: [],
|
auditLog: [],
|
||||||
dispatches: [],
|
dispatches: [],
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ describe("expandTablePatchToBoardFieldOps", () => {
|
||||||
{ field: "scheduleWeekOnly", value: "true" },
|
{ field: "scheduleWeekOnly", value: "true" },
|
||||||
{ field: "targetWeek", value: "2026-07-13" },
|
{ field: "targetWeek", value: "2026-07-13" },
|
||||||
{ field: "scheduledDate", value: "" },
|
{ field: "scheduledDate", value: "" },
|
||||||
|
{ field: "completedDate", value: "" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -102,6 +103,7 @@ describe("expandTablePatchToBoardFieldOps", () => {
|
||||||
{ field: "scheduleWeekOnly", value: "false" },
|
{ field: "scheduleWeekOnly", value: "false" },
|
||||||
{ field: "scheduledDate", value: "2026-07-15" },
|
{ field: "scheduledDate", value: "2026-07-15" },
|
||||||
{ field: "targetWeek", value: "" },
|
{ field: "targetWeek", value: "" },
|
||||||
|
{ field: "completedDate", value: "" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { WorkOrderBoardDetail } from "@/domain/work-orders/types/work-order-board-detail";
|
||||||
|
import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-detail";
|
||||||
|
import { enrichDetailClosabilityFromBoard } from "@/domain/work-orders/utils/enrich-detail-closability-from-board";
|
||||||
|
|
||||||
|
const baseDetail = {
|
||||||
|
id: 1,
|
||||||
|
internalWONumber: "SH-1",
|
||||||
|
workOrderNumber: "20260623001",
|
||||||
|
title: "Test",
|
||||||
|
location: "",
|
||||||
|
locationId: "",
|
||||||
|
locationZip: "",
|
||||||
|
assignedTo: "Alice",
|
||||||
|
dueDate: "",
|
||||||
|
createdDate: "",
|
||||||
|
createdDateFull: "",
|
||||||
|
description: "",
|
||||||
|
priority: "",
|
||||||
|
status: "Open",
|
||||||
|
po: "",
|
||||||
|
tt: "",
|
||||||
|
problem: "HVAC",
|
||||||
|
trade: "",
|
||||||
|
subTrade: "",
|
||||||
|
vendorNTE: "",
|
||||||
|
scheduledDate: "",
|
||||||
|
completedDate: "2026-07-15",
|
||||||
|
source: "PM",
|
||||||
|
type: "" as const,
|
||||||
|
docStatus: "" as const,
|
||||||
|
mediaCount: 0,
|
||||||
|
comments: [],
|
||||||
|
auditLog: [],
|
||||||
|
dispatches: [],
|
||||||
|
} satisfies WorkOrderDetail;
|
||||||
|
|
||||||
|
describe("enrichDetailClosabilityFromBoard", () => {
|
||||||
|
it("returns detail unchanged when board is undefined", () => {
|
||||||
|
expect(enrichDetailClosabilityFromBoard(baseDetail, undefined)).toBe(baseDetail);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overlays authoritative type, docStatus, and mediaCount from board", () => {
|
||||||
|
const board = {
|
||||||
|
info: { type: "PM" },
|
||||||
|
completion: { docStatus: "Yes" },
|
||||||
|
media: [{ id: "1" }, { id: "2" }],
|
||||||
|
} as WorkOrderBoardDetail;
|
||||||
|
|
||||||
|
const enriched = enrichDetailClosabilityFromBoard(baseDetail, board);
|
||||||
|
expect(enriched.type).toBe("PM");
|
||||||
|
expect(enriched.docStatus).toBe("Yes");
|
||||||
|
expect(enriched.mediaCount).toBe(2);
|
||||||
|
expect(enriched.source).toBe("PM");
|
||||||
|
expect(enriched.problem).toBe("HVAC");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -161,14 +161,17 @@ describe("wizardDraftToClosabilityInput", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("detailToClosabilityInput", () => {
|
describe("detailToClosabilityInput", () => {
|
||||||
it("maps problem/source and vendorName so Done is not permanently blocked", () => {
|
it("maps authoritative type/doc/media and problem so Done is not permanently blocked", () => {
|
||||||
const detail = {
|
const detail = {
|
||||||
assignedTo: "Alice",
|
assignedTo: "Alice",
|
||||||
completedDate: "2026-07-15",
|
completedDate: "2026-07-15",
|
||||||
workOrderNumber: "20260623001",
|
workOrderNumber: "20260623001",
|
||||||
internalWONumber: "SH-1",
|
internalWONumber: "SH-1",
|
||||||
problem: "HVAC PM",
|
problem: "HVAC PM",
|
||||||
source: "Add-On",
|
source: "procurement-integration",
|
||||||
|
type: "Add-On",
|
||||||
|
docStatus: "",
|
||||||
|
mediaCount: 0,
|
||||||
dispatches: [{ vendorName: "ACME Tech" }],
|
dispatches: [{ vendorName: "ACME Tech" }],
|
||||||
} as WorkOrderDetail;
|
} as WorkOrderDetail;
|
||||||
|
|
||||||
|
|
@ -180,12 +183,34 @@ describe("detailToClosabilityInput", () => {
|
||||||
expect(input.woNumber).toBe("20260623001");
|
expect(input.woNumber).toBe("20260623001");
|
||||||
expect(input.pm).toBe("HVAC PM");
|
expect(input.pm).toBe("HVAC PM");
|
||||||
expect(input.type).toBe("Add-On");
|
expect(input.type).toBe("Add-On");
|
||||||
|
expect(input.docStatus).toBe("");
|
||||||
|
expect(input.mediaCount).toBe(0);
|
||||||
expect(getClosabilityGaps(input)).not.toContain("Service");
|
expect(getClosabilityGaps(input)).not.toContain("Service");
|
||||||
expect(getClosabilityGaps(input)).not.toContain("Company");
|
expect(getClosabilityGaps(input)).not.toContain("Company");
|
||||||
expect(getClosabilityGaps(input)).not.toContain("Technician");
|
expect(getClosabilityGaps(input)).not.toContain("Technician");
|
||||||
expect(getClosabilityGaps(input)).toEqual([]);
|
expect(getClosabilityGaps(input)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not treat integration source as WorkOrderType", () => {
|
||||||
|
const detail = {
|
||||||
|
assignedTo: "Alice",
|
||||||
|
completedDate: "2026-07-15",
|
||||||
|
workOrderNumber: "20260623001",
|
||||||
|
internalWONumber: "SH-1",
|
||||||
|
problem: "HVAC",
|
||||||
|
source: "PM",
|
||||||
|
type: "",
|
||||||
|
docStatus: "No",
|
||||||
|
mediaCount: 0,
|
||||||
|
dispatches: [{ vendorName: "ACME Tech" }],
|
||||||
|
} as WorkOrderDetail;
|
||||||
|
|
||||||
|
const input = detailToClosabilityInput(detail);
|
||||||
|
expect(input.type).toBe("");
|
||||||
|
expect(getClosabilityGaps(input)).toEqual([]);
|
||||||
|
expect(getClosabilityGaps(input)).not.toContain("Completion document");
|
||||||
|
});
|
||||||
|
|
||||||
it("fails closed on missing service when problem is empty", () => {
|
it("fails closed on missing service when problem is empty", () => {
|
||||||
const detail = {
|
const detail = {
|
||||||
assignedTo: "Alice",
|
assignedTo: "Alice",
|
||||||
|
|
@ -194,6 +219,9 @@ describe("detailToClosabilityInput", () => {
|
||||||
internalWONumber: "SH-1",
|
internalWONumber: "SH-1",
|
||||||
problem: "",
|
problem: "",
|
||||||
source: "Reactive",
|
source: "Reactive",
|
||||||
|
type: "Reactive",
|
||||||
|
docStatus: "",
|
||||||
|
mediaCount: 1,
|
||||||
dispatches: [{ vendorName: "ACME Tech" }],
|
dispatches: [{ vendorName: "ACME Tech" }],
|
||||||
} as WorkOrderDetail;
|
} as WorkOrderDetail;
|
||||||
|
|
||||||
|
|
@ -202,4 +230,72 @@ describe("detailToClosabilityInput", () => {
|
||||||
expect(input.type).toBe("Reactive");
|
expect(input.type).toBe("Reactive");
|
||||||
expect(getClosabilityGaps(input)).toContain("Service");
|
expect(getClosabilityGaps(input)).toContain("Service");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows PM detail transition when completion document is present", () => {
|
||||||
|
const detail = {
|
||||||
|
assignedTo: "Alice",
|
||||||
|
completedDate: "2026-07-15",
|
||||||
|
workOrderNumber: "20260623001",
|
||||||
|
internalWONumber: "SH-1",
|
||||||
|
problem: "HVAC PM",
|
||||||
|
source: "procurement",
|
||||||
|
type: "PM",
|
||||||
|
docStatus: "Yes",
|
||||||
|
mediaCount: 0,
|
||||||
|
dispatches: [{ vendorName: "ACME Tech" }],
|
||||||
|
} as WorkOrderDetail;
|
||||||
|
|
||||||
|
expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks PM detail transition without completion document", () => {
|
||||||
|
const detail = {
|
||||||
|
assignedTo: "Alice",
|
||||||
|
completedDate: "2026-07-15",
|
||||||
|
workOrderNumber: "20260623001",
|
||||||
|
internalWONumber: "SH-1",
|
||||||
|
problem: "HVAC PM",
|
||||||
|
source: "procurement",
|
||||||
|
type: "PM",
|
||||||
|
docStatus: "No",
|
||||||
|
mediaCount: 0,
|
||||||
|
dispatches: [{ vendorName: "ACME Tech" }],
|
||||||
|
} as WorkOrderDetail;
|
||||||
|
|
||||||
|
expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual(["Completion document"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows Emergency detail transition when at least one photo exists", () => {
|
||||||
|
const detail = {
|
||||||
|
assignedTo: "Alice",
|
||||||
|
completedDate: "2026-07-15",
|
||||||
|
workOrderNumber: "20260623001",
|
||||||
|
internalWONumber: "SH-1",
|
||||||
|
problem: "Leak",
|
||||||
|
source: "portal",
|
||||||
|
type: "Emergency",
|
||||||
|
docStatus: "No",
|
||||||
|
mediaCount: 1,
|
||||||
|
dispatches: [{ vendorName: "ACME Tech" }],
|
||||||
|
} as WorkOrderDetail;
|
||||||
|
|
||||||
|
expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks Emergency detail transition without photos", () => {
|
||||||
|
const detail = {
|
||||||
|
assignedTo: "Alice",
|
||||||
|
completedDate: "2026-07-15",
|
||||||
|
workOrderNumber: "20260623001",
|
||||||
|
internalWONumber: "SH-1",
|
||||||
|
problem: "Leak",
|
||||||
|
source: "portal",
|
||||||
|
type: "Emergency",
|
||||||
|
docStatus: "No",
|
||||||
|
mediaCount: 0,
|
||||||
|
dispatches: [{ vendorName: "ACME Tech" }],
|
||||||
|
} as WorkOrderDetail;
|
||||||
|
|
||||||
|
expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual(["At least 1 photo"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue