fix(work-orders): clear stale technician drafts on selection changes

This commit is contained in:
Codex Review Integration 2026-09-16 16:50:26 -03:00
parent ee95847354
commit 712d8e0dc6
9 changed files with 137 additions and 11 deletions

View file

@ -1,6 +1,6 @@
import { expect, test, type Page, type Request, type Route } from "@playwright/test";
// SH-364 / SH-365 / SH-366: technicians registered inline from work-order entry points
// Technicians registered inline from work-order entry points
// become real vendor records only on save, through the additive vendor-company roster
// PATCH (never a whole-vendor POST that restates or blanks company contact fields).

View file

@ -38,9 +38,9 @@ export function buildCreatedTechniciansPatch(
}
/**
* SH-364: persists technicians registered inline on the Vendor & time step under the
* Persists technicians registered inline on the Vendor & time step under the
* selected company, then creates the work order. Nothing is written before Create, so
* cancelling the wizard leaves no stray vendor records (SH-366).
* cancelling the wizard leaves no stray vendor records.
*/
export function useWizardCreateWithTechnicians({
vendors,

View file

@ -29,7 +29,9 @@ export function WizardStepLocationServiceSelect({
onPatch,
}: WizardStepLocationServiceSelectProps) {
const clearVendorIfPrimaryChanged = (nextPm: string): Partial<WorkOrderWizardDraft> =>
nextPm !== draft.pm ? { vendorId: "", vendorName: "", techPhone: "" } : {};
nextPm !== draft.pm
? { vendorId: "", vendorName: "", techPhone: "", vendorCompany: "", newTechnicians: [] }
: {};
const selectSvc = (p: string) => {
onPatch({

View file

@ -34,7 +34,7 @@ function TechnicianAvatar({ vendor }: { vendor: VendorDropdownItem }) {
);
}
/** Company-first vendor selection with inline technician registration (SH-364 prototype parity). */
/** Company-first vendor selection with inline technician registration (prototype parity). */
export function WizardVendorCompanyFields({
draft,
vendors,
@ -79,8 +79,8 @@ export function WizardVendorCompanyFields({
onPick={(nextCompany) =>
onPatch({
vendorCompany: nextCompany,
...(selectedVendor != null && selectedVendor.companyName !== nextCompany
? { vendorId: "", vendorName: "", techPhone: "" }
...(nextCompany !== company
? { vendorId: "", vendorName: "", techPhone: "", newTechnicians: [] }
: {}),
})
}

View file

@ -12,7 +12,7 @@ type WizardVendorNewTechniciansProps = {
};
/**
* SH-364: inline technician registration on the wizard's Vendor & time step.
* Inline technician registration on the wizard's Vendor & time step.
* Rows stay local to the draft; they are persisted under the selected company only
* when the work order is created, so cancelling the wizard never creates a vendor.
*/

View file

@ -31,7 +31,7 @@ function takeAddedTechnician(
/**
* Adds technicians to an existing vendor company through the additive roster PATCH.
*
* SH-365: work-order entry points used to POST a whole new vendor with blank company
* Work-order entry points used to POST a whole new vendor with blank company
* fields, which the create validator rejects (company phone/email) and which would
* overwrite company data. The additive path only appends technicians, keeps the
* company record untouched, and returns the persisted technician ids.

View file

@ -50,9 +50,9 @@ export interface WorkOrderWizardDraft {
avetaRequired: boolean;
vendorId: string;
vendorName: string;
/** Company picked on the Vendor & time step; scopes the technician list (SH-364). */
/** Company picked on the Vendor & time step; scopes the technician list. */
vendorCompany?: string;
/** Technicians registered inline; persisted under `vendorCompany` only on create (SH-364). */
/** Technicians registered inline; persisted under `vendorCompany` only on create. */
newTechnicians?: WizardNewTechnician[];
techPhone: string;
vendorNotes: string;

View file

@ -43,6 +43,8 @@ describe("WizardStepLocationServiceSelect — vendor clear on service change", (
vendorId: "",
vendorName: "",
techPhone: "",
vendorCompany: "",
newTechnicians: [],
}),
);
});
@ -83,6 +85,75 @@ describe("WizardStepLocationServiceSelect — vendor clear on service change", (
vendorId: "",
vendorName: "",
techPhone: "",
vendorCompany: "",
newTechnicians: [],
});
});
it("clears the vendor company and staged inline technicians when the service changes", () => {
const onPatch = vi.fn();
const draft: WorkOrderWizardDraft = {
...EMPTY_WIZARD_DRAFT,
pm: "HVAC",
vendorCompany: "Vinewood LLC",
newTechnicians: [{ name: "Jordan Lee", phone: "(314) 555-0199" }],
};
render(
<WizardStepLocationServiceSelect
draft={draft}
services={[
{ id: "1", value: "HVAC" },
{ id: "2", value: "Plumbing" },
]}
svcOpen={false}
onSvcOpenChange={vi.fn()}
svcSearch=""
onSvcSearchChange={vi.fn()}
onUpdate={vi.fn()}
onPatch={onPatch}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "HVAC" }));
fireEvent.click(screen.getByRole("button", { name: "Plumbing" }));
expect(onPatch).toHaveBeenCalledWith({
pm: "Plumbing",
extraServices: [],
vendorId: "",
vendorName: "",
techPhone: "",
vendorCompany: "",
newTechnicians: [],
});
});
it("keeps vendor company and staged technicians when the service is unchanged", () => {
const onPatch = vi.fn();
const draft: WorkOrderWizardDraft = {
...EMPTY_WIZARD_DRAFT,
pm: "HVAC",
vendorCompany: "Vinewood LLC",
newTechnicians: [{ name: "Jordan Lee", phone: "(314) 555-0199" }],
};
render(
<WizardStepLocationServiceSelect
draft={draft}
services={[{ id: "1", value: "HVAC" }]}
svcOpen={false}
onSvcOpenChange={vi.fn()}
svcSearch=""
onSvcSearchChange={vi.fn()}
onUpdate={vi.fn()}
onPatch={onPatch}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "HVAC" }));
fireEvent.click(screen.getByRole("button", { name: "HVAC" }));
expect(onPatch).toHaveBeenCalledWith({ pm: "HVAC", extraServices: [] });
});
});

View file

@ -87,6 +87,59 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => {
expect(screen.getByRole("button", { name: "Adam Whyte" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Casey Pipe" })).not.toBeInTheDocument();
});
it("clears the picked vendor and staged technicians when the company changes", () => {
const onPatch = vi.fn();
render(
<WizardStepVendorTime
draft={draftWith({
vendorCompany: "Vinewood LLC",
vendorId: "41",
vendorName: "Vinewood LLC",
techPhone: "(314) 555-0100",
newTechnicians: [{ name: "Jordan Lee", phone: "(314) 555-0199" }],
})}
vendors={vendors}
onUpdate={vi.fn()}
onPatch={onPatch}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Vinewood LLC" }));
fireEvent.click(screen.getByRole("button", { name: "Gateway Plumbing" }));
expect(onPatch).toHaveBeenCalledWith({
vendorCompany: "Gateway Plumbing",
vendorId: "",
vendorName: "",
techPhone: "",
newTechnicians: [],
});
});
it("preserves the picked vendor and staged technicians when the same company is re-picked", () => {
const onPatch = vi.fn();
render(
<WizardStepVendorTime
draft={draftWith({
vendorCompany: "Vinewood LLC",
vendorId: "41",
vendorName: "Vinewood LLC",
techPhone: "(314) 555-0100",
newTechnicians: [{ name: "Jordan Lee", phone: "(314) 555-0199" }],
})}
vendors={vendors}
onUpdate={vi.fn()}
onPatch={onPatch}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Vinewood LLC" }));
const companyOptions = screen.getAllByRole("button", { name: "Vinewood LLC" });
fireEvent.click(companyOptions[companyOptions.length - 1]);
expect(onPatch).toHaveBeenCalledWith({ vendorCompany: "Vinewood LLC" });
});
});
describe("useWizardCreateWithTechnicians (SH-364)", () => {