fix(work-orders): require company not technician for Completed (SH-183)

This commit is contained in:
arthur.bassi 2026-08-17 13:54:05 -03:00
parent aaba7ca2ea
commit 37a6098ecb
3 changed files with 58 additions and 11 deletions

View file

@ -4,13 +4,13 @@ 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-114 ∪ pending uplift). */ /** Fields required to evaluate whether a WO may be marked Completed (SH-183 ∪ pending uplift). */
export type WoClosabilityInput = { export type WoClosabilityInput = {
dispatcherId: string; dispatcherId: string;
dispatcherName: string; dispatcherName: string;
/** Vendor/company assignment on the board row. */ /** Company / vendor assignment — required for Completed (SH-183). */
company: string; company: string;
/** Technician assigned to the work order. */ /** Technician is optional for closability; modeled separately from company. */
tech: string; tech: string;
/** Work completed date (API CompletedDate). Empty until set. */ /** Work completed date (API CompletedDate). Empty until set. */
completedDate: string; completedDate: string;
@ -51,6 +51,10 @@ function hasDispatcher(row: Pick<WoClosabilityInput, "dispatcherId" | "dispatche
return Boolean(row.dispatcherId.trim() || row.dispatcherName.trim()); return Boolean(row.dispatcherId.trim() || row.dispatcherName.trim());
} }
function hasCompany(company: string): boolean {
return Boolean(company.trim());
}
function hasService(pm: string): boolean { function hasService(pm: string): boolean {
return Boolean(pm.trim()); return Boolean(pm.trim());
} }
@ -70,8 +74,8 @@ export function getClosabilityGaps(row: WoClosabilityInput): string[] {
if (!hasDispatcher(row)) { if (!hasDispatcher(row)) {
gaps.push("Dispatcher"); gaps.push("Dispatcher");
} }
if (!row.tech.trim()) { if (!hasCompany(row.company)) {
gaps.push("Technician"); gaps.push("Company");
} }
if (!row.completedDate.trim()) { if (!row.completedDate.trim()) {
gaps.push("Completed Date"); gaps.push("Completed Date");
@ -133,7 +137,7 @@ export function wizardDraftToClosabilityInput(draft: WorkOrderWizardDraft): WoCl
dispatcherId: draft.assigneeId, dispatcherId: draft.assigneeId,
dispatcherName: draft.assigneeName, dispatcherName: draft.assigneeName,
company: draft.vendorName, company: draft.vendorName,
tech: draft.vendorName, tech: "",
completedDate: "", completedDate: "",
woNumber: draft.woNumber, woNumber: draft.woNumber,
pm: draft.pm, pm: draft.pm,

View file

@ -10,6 +10,7 @@ function makeRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow
woNumber: "1001", woNumber: "1001",
site: "BK5", site: "BK5",
type: "PM", type: "PM",
severity: null,
dispatcherId: "u1", dispatcherId: "u1",
dispatcherName: "Alex", dispatcherName: "Alex",
scheduledOn: "2026-07-15", scheduledOn: "2026-07-15",
@ -116,4 +117,43 @@ describe("StatusCell pending uplift closability", () => {
expect(onChangeStatus).toHaveBeenCalledWith("Completed"); expect(onChangeStatus).toHaveBeenCalledWith("Completed");
}); });
it("disables Completed when company is missing (SH-183)", () => {
const onChangeStatus = vi.fn();
renderWithProviders(
<StatusCell
wo={makeRow({ company: "" })}
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();
renderWithProviders(
<StatusCell
wo={makeRow({ 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");
});
}); });

View file

@ -60,12 +60,15 @@ describe("getClosabilityGaps", () => {
expect(getClosabilityGaps(makeInput())).toEqual([]); expect(getClosabilityGaps(makeInput())).toEqual([]);
}); });
it("blocks Completed when technician is missing", () => { it("allows company without technician (SH-183)", () => {
expect(getClosabilityGaps(makeInput({ tech: "" }))).toEqual(["Technician"]); const gaps = getClosabilityGaps(makeInput({ tech: "" }));
expect(gaps).toEqual([]);
expect(gaps).not.toContain("Technician");
}); });
it("does not block Completed when vendor/company is empty if technician is assigned", () => { it("blocks Completed when company is missing even if technician is assigned", () => {
expect(getClosabilityGaps(makeInput({ company: "" }))).toEqual([]); expect(getClosabilityGaps(makeInput({ company: "", tech: "Sam" }))).toEqual(["Company"]);
expect(getClosabilityGaps(makeInput({ company: " ", tech: "Sam" }))).toEqual(["Company"]);
}); });
it("lists field gaps before uplift and document gaps", () => { it("lists field gaps before uplift and document gaps", () => {
@ -86,7 +89,7 @@ describe("getClosabilityGaps", () => {
), ),
).toEqual([ ).toEqual([
"Dispatcher", "Dispatcher",
"Technician", "Company",
"Completed Date", "Completed Date",
"WO number", "WO number",
"Service", "Service",