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"; 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 // 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). // 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 * 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({ export function useWizardCreateWithTechnicians({
vendors, vendors,

View file

@ -29,7 +29,9 @@ export function WizardStepLocationServiceSelect({
onPatch, onPatch,
}: WizardStepLocationServiceSelectProps) { }: WizardStepLocationServiceSelectProps) {
const clearVendorIfPrimaryChanged = (nextPm: string): Partial<WorkOrderWizardDraft> => const clearVendorIfPrimaryChanged = (nextPm: string): Partial<WorkOrderWizardDraft> =>
nextPm !== draft.pm ? { vendorId: "", vendorName: "", techPhone: "" } : {}; nextPm !== draft.pm
? { vendorId: "", vendorName: "", techPhone: "", vendorCompany: "", newTechnicians: [] }
: {};
const selectSvc = (p: string) => { const selectSvc = (p: string) => {
onPatch({ 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({ export function WizardVendorCompanyFields({
draft, draft,
vendors, vendors,
@ -79,8 +79,8 @@ export function WizardVendorCompanyFields({
onPick={(nextCompany) => onPick={(nextCompany) =>
onPatch({ onPatch({
vendorCompany: nextCompany, vendorCompany: nextCompany,
...(selectedVendor != null && selectedVendor.companyName !== nextCompany ...(nextCompany !== company
? { vendorId: "", vendorName: "", techPhone: "" } ? { 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 * 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. * 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. * 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 * fields, which the create validator rejects (company phone/email) and which would
* overwrite company data. The additive path only appends technicians, keeps the * overwrite company data. The additive path only appends technicians, keeps the
* company record untouched, and returns the persisted technician ids. * company record untouched, and returns the persisted technician ids.

View file

@ -50,9 +50,9 @@ export interface WorkOrderWizardDraft {
avetaRequired: boolean; avetaRequired: boolean;
vendorId: string; vendorId: string;
vendorName: 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; vendorCompany?: string;
/** Technicians registered inline; persisted under `vendorCompany` only on create (SH-364). */ /** Technicians registered inline; persisted under `vendorCompany` only on create. */
newTechnicians?: WizardNewTechnician[]; newTechnicians?: WizardNewTechnician[];
techPhone: string; techPhone: string;
vendorNotes: string; vendorNotes: string;

View file

@ -43,6 +43,8 @@ describe("WizardStepLocationServiceSelect — vendor clear on service change", (
vendorId: "", vendorId: "",
vendorName: "", vendorName: "",
techPhone: "", techPhone: "",
vendorCompany: "",
newTechnicians: [],
}), }),
); );
}); });
@ -83,6 +85,75 @@ describe("WizardStepLocationServiceSelect — vendor clear on service change", (
vendorId: "", vendorId: "",
vendorName: "", vendorName: "",
techPhone: "", 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.getByRole("button", { name: "Adam Whyte" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Casey Pipe" })).not.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)", () => { describe("useWizardCreateWithTechnicians (SH-364)", () => {