mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
fix(work-orders): treat vendor save as a live company assignment
Clear leftover primary dispatch status on vendor patch so Completed is not blocked after a new company is chosen. Keep Jira keys out of source comments.
This commit is contained in:
parent
72de2ea78d
commit
4c9f8b7d81
14 changed files with 164 additions and 53 deletions
|
|
@ -63,6 +63,10 @@ a status or mark unverified work Done.
|
||||||
- **MUST NOT** leave comments that only restate what Prettier or ESLint already
|
- **MUST NOT** leave comments that only restate what Prettier or ESLint already
|
||||||
enforces (formatting, naming nits the linter catches). Style is settled by the
|
enforces (formatting, naming nits the linter catches). Style is settled by the
|
||||||
gates; review is for behavior, correctness, security, and architecture.
|
gates; review is for behavior, correctness, security, and architecture.
|
||||||
|
- **MUST NOT** put Jira issue keys or ticket titles in source comments, JSDoc,
|
||||||
|
or test names (for example `(SH-183)`). Ticket identity belongs in the PR,
|
||||||
|
commit message, and branch — not in the code. Flag and request removal if a
|
||||||
|
diff adds them.
|
||||||
- **MUST** make every comment actionable: tie it to a behavior, a risk, or an
|
- **MUST** make every comment actionable: tie it to a behavior, a risk, or an
|
||||||
evidence-based convention in these docs, and offer a concrete fix or a
|
evidence-based convention in these docs, and offer a concrete fix or a
|
||||||
targeted question. Use GitHub suggestion blocks when safe.
|
targeted question. Use GitHub suggestion blocks when safe.
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import type {
|
||||||
} from "@/domain/work-orders/types/work-order";
|
} from "@/domain/work-orders/types/work-order";
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import type { WizardWOStatus } from "@/domain/work-orders/types/work-order-wizard";
|
import type { WizardWOStatus } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
|
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
||||||
import { DocBadge } from "./slide-over-doc-badge";
|
import { DocBadge } from "./slide-over-doc-badge";
|
||||||
|
|
||||||
type SlideOverInfoTabEditViewProps = {
|
type SlideOverInfoTabEditViewProps = {
|
||||||
|
|
@ -140,14 +141,7 @@ export function SlideOverInfoTabEditView({
|
||||||
tech={draft.tech}
|
tech={draft.tech}
|
||||||
techPhone={draft.techPhone}
|
techPhone={draft.techPhone}
|
||||||
vendors={vendors}
|
vendors={vendors}
|
||||||
onSave={(patch) =>
|
onSave={(patch) => onDraftChange(toVendorTablePatch(patch))}
|
||||||
onDraftChange({
|
|
||||||
vendorId: patch.vendorId,
|
|
||||||
company: patch.company,
|
|
||||||
tech: patch.tech,
|
|
||||||
techPhone: patch.techPhone,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Status">
|
<SlideOverInfoRow label="Status">
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { PMTypeCell } from "./cells/pm-type-cell";
|
||||||
import { StatusCell } from "./cells/status-cell";
|
import { StatusCell } from "./cells/status-cell";
|
||||||
import { TypeCell } from "./cells/type-cell";
|
import { TypeCell } from "./cells/type-cell";
|
||||||
import { VendorCell } from "./cells/vendor-cell";
|
import { VendorCell } from "./cells/vendor-cell";
|
||||||
|
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
||||||
import { toInlineDraftRow } from "./to-inline-draft-row";
|
import { toInlineDraftRow } from "./to-inline-draft-row";
|
||||||
import { WoTableInlineIdentityCells } from "./wo-table-inline-identity-cells";
|
import { WoTableInlineIdentityCells } from "./wo-table-inline-identity-cells";
|
||||||
|
|
||||||
|
|
@ -193,14 +194,7 @@ export function WoTableInlineRowCells({
|
||||||
tech={draft.tech}
|
tech={draft.tech}
|
||||||
techPhone={draft.techPhone}
|
techPhone={draft.techPhone}
|
||||||
vendors={vendors}
|
vendors={vendors}
|
||||||
onSave={(p) =>
|
onSave={(p) => onPatch(toVendorTablePatch(p))}
|
||||||
onPatch({
|
|
||||||
vendorId: p.vendorId,
|
|
||||||
company: p.company,
|
|
||||||
tech: p.tech,
|
|
||||||
techPhone: p.techPhone,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="group/cell" style={{ padding: "0 14px" }}>
|
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { UpliftCell } from "./cells/uplift-cell";
|
||||||
import { VendorCell } from "./cells/vendor-cell";
|
import { VendorCell } from "./cells/vendor-cell";
|
||||||
import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift";
|
import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift";
|
||||||
import { canOpenUpliftsDialog } from "@/domain/work-orders/utils/uplift-display-utils";
|
import { canOpenUpliftsDialog } from "@/domain/work-orders/utils/uplift-display-utils";
|
||||||
|
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
||||||
import type { WoTableRowHandlers } from "./wo-table-row";
|
import type { WoTableRowHandlers } from "./wo-table-row";
|
||||||
|
|
||||||
type WoTableRowServiceCellsProps = {
|
type WoTableRowServiceCellsProps = {
|
||||||
|
|
@ -67,14 +68,7 @@ export function WoTableRowServiceCells({
|
||||||
techPhone={row.techPhone}
|
techPhone={row.techPhone}
|
||||||
vendors={vendors}
|
vendors={vendors}
|
||||||
q={search}
|
q={search}
|
||||||
onSave={(p) =>
|
onSave={(p) => onPatchRow(toVendorTablePatch(p))}
|
||||||
onPatchRow({
|
|
||||||
vendorId: p.vendorId,
|
|
||||||
company: p.company,
|
|
||||||
tech: p.tech,
|
|
||||||
techPhone: p.techPhone,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ export interface WorkOrderBoardDetailInfoApi {
|
||||||
dayGroup?: string | null;
|
dayGroup?: string | null;
|
||||||
vendorId?: number | null;
|
vendorId?: number | null;
|
||||||
vendorName?: string | null;
|
vendorName?: string | null;
|
||||||
/** Exact board DTO field; inactive primary dispatch is not a live company (SH-183). */
|
|
||||||
primaryDispatchStatus?: string | null;
|
primaryDispatchStatus?: string | null;
|
||||||
techName?: string | null;
|
techName?: string | null;
|
||||||
techPhone?: string | null;
|
techPhone?: string | null;
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,6 @@ export interface WorkOrderBoardRowApi {
|
||||||
dayGroup?: string | null;
|
dayGroup?: string | null;
|
||||||
vendorId?: number | null;
|
vendorId?: number | null;
|
||||||
vendorName?: string | null;
|
vendorName?: string | null;
|
||||||
/** Exact board DTO field; inactive primary dispatch is not a live company (SH-183). */
|
|
||||||
primaryDispatchStatus?: string | null;
|
primaryDispatchStatus?: string | null;
|
||||||
techName?: string | null;
|
techName?: string | null;
|
||||||
techPhone?: string | null;
|
techPhone?: string | null;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ export interface WorkOrderTableRow {
|
||||||
extraServices: string[];
|
extraServices: string[];
|
||||||
vendorId: string;
|
vendorId: string;
|
||||||
company: string;
|
company: string;
|
||||||
/** Primary dispatch status when the board contract supplies it (SH-183). */
|
|
||||||
primaryDispatchStatus?: string;
|
primaryDispatchStatus?: string;
|
||||||
tech: string;
|
tech: string;
|
||||||
techPhone: string;
|
techPhone: string;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-deta
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overlay authoritative closability fields from board detail onto a legacy GetById 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.
|
* Board `info.type`, `completion.docStatus`, and `media.length` are the SH-114 sources.
|
||||||
*/
|
*/
|
||||||
export function enrichDetailClosabilityFromBoard(
|
export function enrichDetailClosabilityFromBoard(
|
||||||
detail: WorkOrderDetail,
|
detail: WorkOrderDetail,
|
||||||
|
|
|
||||||
18
src/domain/work-orders/utils/vendor-assignment-patch.ts
Normal file
18
src/domain/work-orders/utils/vendor-assignment-patch.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { WorkOrderTablePatch } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
|
export type VendorAssignmentFields = {
|
||||||
|
vendorId: string;
|
||||||
|
company: string;
|
||||||
|
tech: string;
|
||||||
|
techPhone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toVendorTablePatch(fields: VendorAssignmentFields): WorkOrderTablePatch {
|
||||||
|
return {
|
||||||
|
vendorId: fields.vendorId,
|
||||||
|
company: fields.company,
|
||||||
|
tech: fields.tech,
|
||||||
|
techPhone: fields.techPhone,
|
||||||
|
primaryDispatchStatus: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -7,11 +7,11 @@ import type {
|
||||||
} from "@/domain/work-orders/types/work-order-table-row";
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import type { WOType, WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
|
import type { WOType, WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
|
|
||||||
/** Fields required to evaluate whether a WO may be marked Completed (SH-183 ∪ pending uplift). */
|
/** Fields required to evaluate whether a WO may be marked Completed. */
|
||||||
export type WoClosabilityInput = {
|
export type WoClosabilityInput = {
|
||||||
dispatcherId: string;
|
dispatcherId: string;
|
||||||
dispatcherName: string;
|
dispatcherName: string;
|
||||||
/** Company / vendor assignment — required for Completed (SH-183). */
|
/** Company / vendor assignment — required for Completed. */
|
||||||
company: string;
|
company: string;
|
||||||
/** Board vendor id; assignment is valid with vendorId even when company label is empty. */
|
/** Board vendor id; assignment is valid with vendorId even when company label is empty. */
|
||||||
vendorId: string;
|
vendorId: string;
|
||||||
|
|
@ -158,14 +158,12 @@ 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"]);
|
const INACTIVE_DISPATCH_STATUSES = new Set(["cancelled", "canceled", "refused"]);
|
||||||
|
|
||||||
export function isInactiveDispatchStatus(status: string | undefined): boolean {
|
export function isInactiveDispatchStatus(status: string | undefined): boolean {
|
||||||
return INACTIVE_DISPATCH_STATUSES.has((status ?? "").trim().toLowerCase());
|
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 {
|
export function tableRowToClosabilityInput(wo: WorkOrderTableRow): WoClosabilityInput {
|
||||||
const inactive = isInactiveDispatchStatus(wo.primaryDispatchStatus);
|
const inactive = isInactiveDispatchStatus(wo.primaryDispatchStatus);
|
||||||
return {
|
return {
|
||||||
|
|
@ -185,7 +183,6 @@ export function tableRowToClosabilityInput(wo: WorkOrderTableRow): WoClosability
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Current vendor assignment — cancelled/refused dispatches do not count as company (SH-183). */
|
|
||||||
function currentAssignedDispatch(
|
function currentAssignedDispatch(
|
||||||
dispatches: WorkOrderDispatchSummary[],
|
dispatches: WorkOrderDispatchSummary[],
|
||||||
): WorkOrderDispatchSummary | undefined {
|
): WorkOrderDispatchSummary | undefined {
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ describe("StatusCell pending uplift closability", () => {
|
||||||
expect(onChangeStatus).toHaveBeenCalledWith("Completed");
|
expect(onChangeStatus).toHaveBeenCalledWith("Completed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables Completed when company is missing (SH-183)", () => {
|
it("disables Completed when company is missing", () => {
|
||||||
const onChangeStatus = vi.fn();
|
const onChangeStatus = vi.fn();
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
|
|
@ -141,7 +141,7 @@ describe("StatusCell pending uplift closability", () => {
|
||||||
expect(onChangeStatus).not.toHaveBeenCalled();
|
expect(onChangeStatus).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows Completed when vendorId is set even if company label is empty (SH-183)", () => {
|
it("allows Completed when vendorId is set even if company label is empty", () => {
|
||||||
const onChangeStatus = vi.fn();
|
const onChangeStatus = vi.fn();
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
|
|
@ -159,7 +159,7 @@ describe("StatusCell pending uplift closability", () => {
|
||||||
expect(onChangeStatus).toHaveBeenCalledWith("Completed");
|
expect(onChangeStatus).toHaveBeenCalledWith("Completed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows Completed when vendorId has an empty company label and a live dispatch status (SH-183)", () => {
|
it("allows Completed when vendorId has an empty company label and a live dispatch status", () => {
|
||||||
const onChangeStatus = vi.fn();
|
const onChangeStatus = vi.fn();
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
|
|
@ -178,7 +178,7 @@ describe("StatusCell pending uplift closability", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(["Cancelled", "Canceled", "Refused"] as const)(
|
it.each(["Cancelled", "Canceled", "Refused"] as const)(
|
||||||
"disables Completed when vendorId is leftover from a %s dispatch (SH-183)",
|
"disables Completed when vendorId is leftover from a %s dispatch",
|
||||||
(primaryDispatchStatus) => {
|
(primaryDispatchStatus) => {
|
||||||
const onChangeStatus = vi.fn();
|
const onChangeStatus = vi.fn();
|
||||||
|
|
||||||
|
|
@ -206,7 +206,54 @@ describe("StatusCell pending uplift closability", () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("allows Completed when technician is empty if company is set (SH-183)", () => {
|
it("disables Completed when the board omits vendorId after a Refused primary", () => {
|
||||||
|
const onChangeStatus = vi.fn();
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<StatusCell
|
||||||
|
wo={makeRow({
|
||||||
|
company: "",
|
||||||
|
vendorId: "",
|
||||||
|
tech: "",
|
||||||
|
primaryDispatchStatus: "Refused",
|
||||||
|
})}
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows Completed after a vendor patch clears leftover refused status", () => {
|
||||||
|
const onChangeStatus = vi.fn();
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<StatusCell
|
||||||
|
wo={makeRow({
|
||||||
|
company: "New Co",
|
||||||
|
vendorId: "9",
|
||||||
|
tech: "Pat",
|
||||||
|
primaryDispatchStatus: "",
|
||||||
|
})}
|
||||||
|
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("allows Completed when technician is empty if company is set", () => {
|
||||||
const onChangeStatus = vi.fn();
|
const onChangeStatus = vi.fn();
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
|
|
|
||||||
|
|
@ -61,13 +61,27 @@ describe("mapWorkOrderTableRow primaryDispatchStatus", () => {
|
||||||
expect(row.primaryDispatchStatus).toBe(value);
|
expect(row.primaryDispatchStatus).toBe(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores undocumented dispatchStatus aliases", () => {
|
it("maps backend #74 omitted vendor with Refused primaryDispatchStatus", () => {
|
||||||
|
const row = mapWorkOrderTableRow({
|
||||||
|
id: 9,
|
||||||
|
vendorId: null,
|
||||||
|
vendorName: null,
|
||||||
|
primaryDispatchStatus: "Refused",
|
||||||
|
});
|
||||||
|
expect(row.vendorId).toBe("");
|
||||||
|
expect(row.company).toBe("");
|
||||||
|
expect(row.primaryDispatchStatus).toBe("Refused");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps live vendorId with empty company label and Sent status", () => {
|
||||||
const row = mapWorkOrderTableRow({
|
const row = mapWorkOrderTableRow({
|
||||||
id: 9,
|
id: 9,
|
||||||
vendorId: 45,
|
vendorId: 45,
|
||||||
dispatchStatus: "Refused",
|
vendorName: "",
|
||||||
vendorDispatchStatus: "Cancelled",
|
primaryDispatchStatus: "Sent",
|
||||||
});
|
});
|
||||||
expect(row.primaryDispatchStatus).toBe("");
|
expect(row.vendorId).toBe("45");
|
||||||
|
expect(row.company).toBe("");
|
||||||
|
expect(row.primaryDispatchStatus).toBe("Sent");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
||||||
|
import {
|
||||||
|
getClosabilityGaps,
|
||||||
|
tableRowToClosabilityInput,
|
||||||
|
} from "@/domain/work-orders/utils/wo-closability";
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
|
describe("toVendorTablePatch", () => {
|
||||||
|
it("clears leftover primaryDispatchStatus so a new vendor is a live assignment", () => {
|
||||||
|
const patch = toVendorTablePatch({
|
||||||
|
vendorId: "9",
|
||||||
|
company: "New Co",
|
||||||
|
tech: "Pat",
|
||||||
|
techPhone: "555",
|
||||||
|
});
|
||||||
|
expect(patch.primaryDispatchStatus).toBe("");
|
||||||
|
expect(patch.vendorId).toBe("9");
|
||||||
|
|
||||||
|
const input = tableRowToClosabilityInput({
|
||||||
|
dispatcherId: "u1",
|
||||||
|
dispatcherName: "Alex",
|
||||||
|
company: patch.company ?? "",
|
||||||
|
vendorId: patch.vendorId ?? "",
|
||||||
|
tech: patch.tech ?? "",
|
||||||
|
completedDate: "2026-07-15",
|
||||||
|
woNumber: "20260623001",
|
||||||
|
pm: "HVAC PM",
|
||||||
|
docStatus: "Yes",
|
||||||
|
mediaCount: 1,
|
||||||
|
type: "PM",
|
||||||
|
primaryDispatchStatus: patch.primaryDispatchStatus,
|
||||||
|
} as WorkOrderTableRow);
|
||||||
|
expect(getClosabilityGaps(input)).not.toContain("Company");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -77,7 +77,7 @@ describe("getClosabilityGaps", () => {
|
||||||
expect(getClosabilityGaps(makeInput())).toEqual([]);
|
expect(getClosabilityGaps(makeInput())).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows company without technician (SH-183)", () => {
|
it("allows company without technician", () => {
|
||||||
const gaps = getClosabilityGaps(makeInput({ tech: "" }));
|
const gaps = getClosabilityGaps(makeInput({ tech: "" }));
|
||||||
expect(gaps).toEqual([]);
|
expect(gaps).toEqual([]);
|
||||||
expect(gaps).not.toContain("Technician");
|
expect(gaps).not.toContain("Technician");
|
||||||
|
|
@ -88,7 +88,7 @@ describe("getClosabilityGaps", () => {
|
||||||
expect(getClosabilityGaps(makeInput({ company: " ", tech: "Sam" }))).toEqual(["Company"]);
|
expect(getClosabilityGaps(makeInput({ company: " ", tech: "Sam" }))).toEqual(["Company"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows Completed when vendorId is set and company/technician are empty (SH-183)", () => {
|
it("allows Completed when vendorId is set and company/technician are empty", () => {
|
||||||
expect(getClosabilityGaps(makeInput({ company: "", vendorId: "7", tech: "" }))).toEqual([]);
|
expect(getClosabilityGaps(makeInput({ company: "", vendorId: "7", tech: "" }))).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ describe("getClosabilityGaps", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(["PM", "Reactive", "Emergency", "Overdue", ""] as const)(
|
it.each(["PM", "Reactive", "Emergency", "Overdue", ""] as const)(
|
||||||
"blocks Completed without a company for type %s (SH-183)",
|
"blocks Completed without a company for type %s",
|
||||||
(type) => {
|
(type) => {
|
||||||
const gaps = getClosabilityGaps(
|
const gaps = getClosabilityGaps(
|
||||||
makeInput({
|
makeInput({
|
||||||
|
|
@ -309,7 +309,7 @@ describe("detailToClosabilityInput", () => {
|
||||||
expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual(["At least 1 photo"]);
|
expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual(["At least 1 photo"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("treats vendorId without vendorName as assigned company (SH-183)", () => {
|
it("treats vendorId without vendorName as assigned company", () => {
|
||||||
const detail = {
|
const detail = {
|
||||||
assignedTo: "Alice",
|
assignedTo: "Alice",
|
||||||
completedDate: "2026-07-15",
|
completedDate: "2026-07-15",
|
||||||
|
|
@ -330,7 +330,7 @@ describe("detailToClosabilityInput", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(["Cancelled", "Canceled", "Refused", "CANCELLED", " canceled "])(
|
it.each(["Cancelled", "Canceled", "Refused", "CANCELLED", " canceled "])(
|
||||||
"ignores inactive dispatch status %s when resolving assigned company (SH-183)",
|
"ignores inactive dispatch status %s when resolving assigned company",
|
||||||
(status) => {
|
(status) => {
|
||||||
const inactiveOnly = {
|
const inactiveOnly = {
|
||||||
assignedTo: "Alice",
|
assignedTo: "Alice",
|
||||||
|
|
@ -352,7 +352,7 @@ describe("detailToClosabilityInput", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each(["Verified", "Completed", "Sent"])(
|
it.each(["Verified", "Completed", "Sent"])(
|
||||||
"treats dispatch status %s as a live company assignment (SH-183)",
|
"treats dispatch status %s as a live company assignment",
|
||||||
(status) => {
|
(status) => {
|
||||||
const liveAssignment = {
|
const liveAssignment = {
|
||||||
assignedTo: "Alice",
|
assignedTo: "Alice",
|
||||||
|
|
@ -374,7 +374,7 @@ describe("detailToClosabilityInput", () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("uses the active dispatch after an inactive cancelled spelling (SH-183)", () => {
|
it("uses the active dispatch after an inactive cancelled spelling", () => {
|
||||||
const supersededThenCurrent = {
|
const supersededThenCurrent = {
|
||||||
assignedTo: "Alice",
|
assignedTo: "Alice",
|
||||||
completedDate: "2026-07-15",
|
completedDate: "2026-07-15",
|
||||||
|
|
@ -413,14 +413,14 @@ describe("tableRowToClosabilityInput", () => {
|
||||||
type: "",
|
type: "",
|
||||||
} as WorkOrderTableRow;
|
} as WorkOrderTableRow;
|
||||||
|
|
||||||
it("keeps vendorId as a live assignment when primary dispatch status is absent (SH-183)", () => {
|
it("keeps vendorId as a live assignment when primary dispatch status is absent", () => {
|
||||||
const input = tableRowToClosabilityInput(closableRow);
|
const input = tableRowToClosabilityInput(closableRow);
|
||||||
expect(input.vendorId).toBe("45");
|
expect(input.vendorId).toBe("45");
|
||||||
expect(getClosabilityGaps(input)).toEqual([]);
|
expect(getClosabilityGaps(input)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(["Cancelled", "Canceled", "Refused"] as const)(
|
it.each(["Cancelled", "Canceled", "Refused"] as const)(
|
||||||
"clears company assignment when primary dispatch status is %s (SH-183)",
|
"clears company assignment when primary dispatch status is %s",
|
||||||
(primaryDispatchStatus) => {
|
(primaryDispatchStatus) => {
|
||||||
const input = tableRowToClosabilityInput({ ...closableRow, primaryDispatchStatus });
|
const input = tableRowToClosabilityInput({ ...closableRow, primaryDispatchStatus });
|
||||||
expect(input.company).toBe("");
|
expect(input.company).toBe("");
|
||||||
|
|
@ -429,11 +429,26 @@ describe("tableRowToClosabilityInput", () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("raises Company when the board omits vendor assignment (inactive primary, SH-183)", () => {
|
it("raises Company when the board omits vendor assignment (inactive primary)", () => {
|
||||||
const input = tableRowToClosabilityInput({ ...closableRow, vendorId: "", company: "" });
|
const input = tableRowToClosabilityInput({
|
||||||
|
...closableRow,
|
||||||
|
vendorId: "",
|
||||||
|
company: "",
|
||||||
|
primaryDispatchStatus: "Refused",
|
||||||
|
});
|
||||||
expect(input.vendorId).toBe("");
|
expect(input.vendorId).toBe("");
|
||||||
expect(getClosabilityGaps(input)).toContain("Company");
|
expect(getClosabilityGaps(input)).toContain("Company");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps a live vendorId after a vendor patch clears leftover refused status", () => {
|
||||||
|
const input = tableRowToClosabilityInput({
|
||||||
|
...closableRow,
|
||||||
|
company: "New Co",
|
||||||
|
vendorId: "9",
|
||||||
|
primaryDispatchStatus: "",
|
||||||
|
});
|
||||||
|
expect(getClosabilityGaps(input)).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("readDetailWoType", () => {
|
describe("readDetailWoType", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue