fix(work-orders): require vendor company not technician to complete (SH-183)

This commit is contained in:
Arthur Bassi 2026-08-19 11:49:19 -03:00
parent 772d7f94df
commit 1554cf30d0
6 changed files with 67 additions and 8 deletions

View file

@ -26,6 +26,7 @@ function rowClosabilityGaps(wo: WorkOrderTableRow) {
dispatcherId: wo.dispatcherId,
dispatcherName: wo.dispatcherName,
company: wo.company,
vendorId: wo.vendorId ?? "",
tech: wo.tech,
completedDate: wo.completedDate ?? "",
woNumber: wo.woNumber,

View file

@ -11,6 +11,8 @@ export type WoClosabilityInput = {
dispatcherName: string;
/** Company / vendor assignment — required for Completed (SH-183). */
company: string;
/** Board vendor id; assignment is valid with vendorId even when company label is empty. */
vendorId: string;
/** Technician is optional for closability; modeled separately from company. */
tech: string;
/** Work completed date (API CompletedDate). Empty until set. */
@ -57,8 +59,8 @@ function hasDispatcher(row: Pick<WoClosabilityInput, "dispatcherId" | "dispatche
return Boolean(row.dispatcherId.trim() || row.dispatcherName.trim());
}
function hasCompany(company: string): boolean {
return Boolean(company.trim());
function hasAssignedCompany(row: Pick<WoClosabilityInput, "company" | "vendorId">): boolean {
return Boolean((row.company ?? "").trim() || (row.vendorId ?? "").trim());
}
function hasService(pm: string): boolean {
@ -80,7 +82,7 @@ export function getClosabilityGaps(row: WoClosabilityInput): string[] {
if (!hasDispatcher(row)) {
gaps.push("Dispatcher");
}
if (!hasCompany(row.company)) {
if (!hasAssignedCompany(row)) {
gaps.push("Company");
}
if (!row.completedDate.trim()) {
@ -143,6 +145,7 @@ export function wizardDraftToClosabilityInput(draft: WorkOrderWizardDraft): WoCl
dispatcherId: draft.assigneeId,
dispatcherName: draft.assigneeName,
company: draft.vendorName,
vendorId: draft.vendorId ?? "",
tech: "",
completedDate: "",
woNumber: draft.woNumber,
@ -159,13 +162,15 @@ export function wizardDraftToClosabilityInput(draft: WorkOrderWizardDraft): WoCl
* as WorkOrderType. `problem` is the same service field the board maps into `pm`.
*/
export function detailToClosabilityInput(wo: WorkOrderDetail): WoClosabilityInput {
const vendorName = wo.dispatches.find((dispatch) =>
Boolean(dispatch.vendorName.trim()),
)?.vendorName;
const assigned = wo.dispatches.find((dispatch) => {
const vendorId = String(dispatch.vendorId ?? "").trim();
return Boolean(dispatch.vendorName.trim() || vendorId);
});
return {
dispatcherId: "",
dispatcherName: wo.assignedTo,
company: vendorName ?? "",
company: assigned?.vendorName ?? "",
vendorId: assigned ? String(assigned.vendorId ?? "").trim() : "",
tech: "",
completedDate: wo.completedDate,
woNumber: wo.workOrderNumber || wo.internalWONumber,

View file

@ -9,6 +9,7 @@ const incompleteRow = {
dispatcherId: "",
dispatcherName: "",
company: "",
vendorId: "",
tech: "",
completedDate: "",
woNumber: "",

View file

@ -9,6 +9,7 @@ const pastDueRow = {
dispatcherId: "",
dispatcherName: "",
company: "",
vendorId: "",
tech: "",
completedDate: "",
woNumber: "",

View file

@ -125,7 +125,7 @@ describe("StatusCell pending uplift closability", () => {
renderWithProviders(
<StatusCell
wo={makeRow({ company: "" })}
wo={makeRow({ company: "", vendorId: "" })}
onChangeStatus={onChangeStatus}
onOpenSlideOver={vi.fn()}
/>,
@ -141,6 +141,24 @@ describe("StatusCell pending uplift closability", () => {
expect(onChangeStatus).not.toHaveBeenCalled();
});
it("allows Completed when vendorId is set even if company label is empty (SH-183)", () => {
const onChangeStatus = vi.fn();
renderWithProviders(
<StatusCell
wo={makeRow({ company: "", vendorId: "7", tech: "" })}
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 (SH-183)", () => {
const onChangeStatus = vi.fn();

View file

@ -21,6 +21,7 @@ function makeInput(overrides: Partial<WoClosabilityInput> = {}): WoClosabilityIn
dispatcherId: "u1",
dispatcherName: "Alex",
company: "Acme HVAC",
vendorId: "",
tech: "Sam",
completedDate: "2026-07-01",
woNumber: "1001",
@ -85,6 +86,16 @@ describe("getClosabilityGaps", () => {
expect(getClosabilityGaps(makeInput({ company: " ", tech: "Sam" }))).toEqual(["Company"]);
});
it("allows Completed when vendorId is set and company/technician are empty (SH-183)", () => {
expect(getClosabilityGaps(makeInput({ company: "", vendorId: "7", tech: "" }))).toEqual([]);
});
it("blocks Completed when both company and vendorId are empty even with a technician", () => {
expect(getClosabilityGaps(makeInput({ company: "", vendorId: " ", tech: "Sam" }))).toEqual([
"Company",
]);
});
it("lists field gaps before uplift and document gaps", () => {
expect(
getClosabilityGaps(
@ -188,6 +199,7 @@ describe("wizardDraftToClosabilityInput", () => {
dispatcherId: "u1",
dispatcherName: "Alice",
company: "ACME",
vendorId: "",
tech: "",
completedDate: "",
woNumber: "20260623001",
@ -221,6 +233,7 @@ describe("detailToClosabilityInput", () => {
const input = detailToClosabilityInput(detail);
expect(input.dispatcherName).toBe("Alice");
expect(input.company).toBe("ACME Tech");
expect(input.vendorId).toBe("");
expect(input.woNumber).toBe("20260623001");
expect(input.pm).toBe("HVAC PM");
expect(getClosabilityGaps(input)).toEqual([]);
@ -277,6 +290,26 @@ describe("detailToClosabilityInput", () => {
expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual(["At least 1 photo"]);
});
it("treats vendorId without vendorName as assigned company (SH-183)", () => {
const detail = {
assignedTo: "Alice",
completedDate: "2026-07-15",
workOrderNumber: "20260623001",
internalWONumber: "SH-1",
problem: "HVAC PM",
source: "procurement-integration",
type: "",
docStatus: "",
mediaCount: 0,
dispatches: [{ vendorName: "", vendorId: 45 }],
} as WorkOrderDetail;
const input = detailToClosabilityInput(detail);
expect(input.company).toBe("");
expect(input.vendorId).toBe("45");
expect(getClosabilityGaps(input)).toEqual([]);
});
});
describe("readDetailWoType", () => {