fix(workorders): preserve explicit no-technician choice

This commit is contained in:
Alexandre Brandizzi 2026-09-29 20:06:05 -03:00
parent 316cafccaa
commit 64c53e9da0
7 changed files with 126 additions and 3 deletions

View file

@ -134,6 +134,19 @@ async function mockApi(
});
}
if (pathname === "/api/workorders/board" && method === "POST") {
const body = bodyOf(request) as { vendorId?: number; technicianAssigned?: boolean };
const vendorId = body.vendorId ?? 0;
currentBoardRows.push({
...BOARD_ROW,
id: 900,
woNumber: "SH-900",
lifecycleStatusLabel: "Incomplete",
vendorId: vendorId || null,
vendorName: vendorId > 0 ? "Vinewood LLC" : null,
technicianAssigned: body.technicianAssigned,
techName: body.technicianAssigned === true ? "Jordan Lee" : null,
techPhone: body.technicianAssigned === true ? "(314) 555-0199" : null,
});
return fulfillJson(route, { data: { id: 900, woNumber: "SH-900" } });
}
if (/^\/api\/workorders\/\d+\/board$/u.test(pathname) && method === "PATCH") {
@ -280,6 +293,44 @@ test.describe("Inline technician registration", () => {
expect(create?.body).toEqual(expect.objectContaining({ vendorId: 42 }));
});
test("wizard No technician selection overrides a staged technician through create and reopen", async ({
page,
}) => {
const writes: Captured[] = [];
await mockApi(page, writes);
await openWizardVendorStep(page);
await page.getByRole("button", { name: "No technician" }).first().click();
await page.getByRole("button", { name: "No technician" }).last().click();
await page.getByRole("button", { name: "Create work order" }).click();
await expect
.poll(() =>
writes.find(
(write) => write.pathname === "/api/workorders/board" && write.method === "POST",
),
)
.toBeTruthy();
const create = writes.find(
(write) => write.pathname === "/api/workorders/board" && write.method === "POST",
);
expect(create?.body).toEqual(
expect.objectContaining({ vendorId: 41, technicianAssigned: false }),
);
expect(vendorWrites(writes)).toEqual([
expect.objectContaining({ pathname: "/api/vendor-company-roster/7" }),
]);
await page.reload();
const createdRow = page.locator("#wo-row-900");
await expect(createdRow.getByRole("button", { name: /Vinewood LLC/ })).toBeVisible();
await createdRow.getByRole("button", { name: /Vinewood LLC/ }).click();
const vendorDialog = page.getByRole("dialog", { name: "Vendor" });
await expect(vendorDialog.getByRole("button", { name: "No technician" })).toBeVisible();
await expect(vendorDialog.locator('input[type="tel"]')).toHaveCount(0);
await expect(vendorDialog.getByRole("button", { name: "Jordan Lee" })).toHaveCount(0);
});
test("company-only save returns no technician and stays empty after reopen and reload", async ({
page,
}) => {

View file

@ -172,9 +172,7 @@ export function useVendorDialogForm({
notes,
pendingCreateCount: Object.keys(pendingCreates).length,
},
baseline: {
...buildSubmitBaseline({ ...technicianKeyArgs, techPhone }),
},
baseline: buildSubmitBaseline({ ...technicianKeyArgs, techPhone }),
});
const { handleSave } = useVendorDialogSave({

View file

@ -33,6 +33,7 @@ export function buildCreatedTechniciansPatch(
if (
created.length === 1 &&
only != null &&
draft.technicianSelectionCleared !== true &&
(draft.technicianAssigned === false || draft.vendorId === "")
) {
patch.vendorId = String(only.id);

View file

@ -76,6 +76,7 @@ export function WizardVendorCompanyFields({
techPhone: "",
vendorNotes: "",
newTechnicians: [],
technicianSelectionCleared: false,
})
}
>
@ -96,6 +97,7 @@ export function WizardVendorCompanyFields({
vendorId: nextAnchor ? String(nextAnchor.id) : "",
vendorName: nextAnchor ? nextCompany : "",
technicianAssigned: false,
technicianSelectionCleared: false,
techPhone: "",
vendorNotes: "",
newTechnicians: [],
@ -116,6 +118,7 @@ export function WizardVendorCompanyFields({
vendorId: noTechnicianVendorId,
vendorName: companyAnchor ? company : "",
technicianAssigned: false,
technicianSelectionCleared: true,
techPhone: "",
vendorNotes: "",
})
@ -147,6 +150,7 @@ export function WizardVendorCompanyFields({
vendorId: noTechnicianVendorId,
vendorName: companyAnchor ? company : "",
technicianAssigned: false,
technicianSelectionCleared: true,
techPhone: "",
});
return;
@ -157,6 +161,7 @@ export function WizardVendorCompanyFields({
vendorId: id,
vendorName: vendor?.companyName ?? "",
technicianAssigned: true,
technicianSelectionCleared: false,
techPhone: "",
});
}}

View file

@ -56,6 +56,8 @@ export interface WorkOrderWizardDraft {
vendorCompany?: string;
/** Explicit technician choice; null/omitted retains legacy inference. */
technicianAssigned?: boolean | null;
/** UI intent: an explicit clear overrides staged single-technician autoassignment. */
technicianSelectionCleared?: boolean;
/** Technicians registered inline; persisted under `vendorCompany` only on create. */
newTechnicians?: WizardNewTechnician[];
techPhone: string;

View file

@ -116,6 +116,7 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => {
vendorId: "52",
vendorName: "Gateway Plumbing",
technicianAssigned: false,
technicianSelectionCleared: false,
techPhone: "",
vendorNotes: "",
newTechnicians: [],
@ -141,6 +142,7 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => {
vendorId: "41",
vendorName: "Vinewood LLC",
technicianAssigned: false,
technicianSelectionCleared: false,
techPhone: "",
vendorNotes: "",
newTechnicians: [],
@ -170,10 +172,41 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => {
vendorId: "41",
vendorName: "Vinewood LLC",
technicianAssigned: true,
technicianSelectionCleared: false,
techPhone: "",
});
});
it("records explicit no-technician intent while leaving a staged technician untouched", async () => {
const onPatch = vi.fn();
render(
<WizardStepVendorTime
draft={draftWith({
vendorCompany: "Vinewood LLC",
vendorId: "41",
technicianAssigned: false,
newTechnicians: [{ name: "Jordan Lee", phone: "" }],
})}
vendors={vendors}
onUpdate={vi.fn()}
onPatch={onPatch}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "No technician" }));
const noTechnicianOptions = screen.getAllByRole("button", { name: "No technician" });
fireEvent.click(noTechnicianOptions[noTechnicianOptions.length - 1]!);
expect(onPatch).toHaveBeenCalledWith({
vendorId: "41",
vendorName: "Vinewood LLC",
technicianAssigned: false,
technicianSelectionCleared: true,
techPhone: "",
});
expect(screen.getByRole("group", { name: "New Technician 1" })).toBeInTheDocument();
});
it("retains the same company vendor id when clearing a second technician", async () => {
const onPatch = vi.fn();
const secondVinewoodTech = {
@ -201,6 +234,7 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => {
vendorId: "42",
vendorName: "Vinewood LLC",
technicianAssigned: false,
technicianSelectionCleared: true,
techPhone: "",
});
});
@ -327,6 +361,24 @@ describe("useWizardCreateWithTechnicians (SH-364)", () => {
});
});
it("does not autoassign a staged technician after No technician was explicitly chosen", () => {
const created = { ...vendors[0], id: 99, contactName: "Jordan Lee" };
expect(
buildCreatedTechniciansPatch(
draftWith({
vendorCompany: "Vinewood LLC",
vendorId: "41",
technicianAssigned: false,
technicianSelectionCleared: true,
newTechnicians: [{ name: "Jordan Lee", phone: "" }],
}),
"Vinewood LLC",
[{ name: "Jordan Lee", phone: "" }],
[created],
),
).toEqual({ newTechnicians: [] });
});
it("blocks Create and skips the roster save when a staged phone is not a valid number", async () => {
const createMutation = { mutate: vi.fn(), isPending: false };
const { result } = renderHook(() =>

View file

@ -77,6 +77,20 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
expect(payload).not.toHaveProperty("customer");
});
it("omits wizard-only technician selection intent from the create payload", () => {
const payload = mapWizardDraftToBoardCreatePayload({
...EMPTY_WIZARD_DRAFT,
type: "PM",
siteCode: "BK5",
vendorId: "41",
technicianAssigned: false,
technicianSelectionCleared: true,
});
expect(payload).toEqual(expect.objectContaining({ vendorId: 41, technicianAssigned: false }));
expect(payload).not.toHaveProperty("technicianSelectionCleared");
});
it("allows create mapping without a service", () => {
const payload = mapWizardDraftToBoardCreatePayload({
...EMPTY_WIZARD_DRAFT,