fix(work-orders): preserve wizard POC edits, clear stale vendor, persist range start

This commit is contained in:
arthur.bassi 2026-08-05 10:39:28 -03:00
parent 162d47fe1a
commit b3d8be571e
7 changed files with 134 additions and 8 deletions

View file

@ -4,7 +4,11 @@ import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order
type SetDraft = (updater: (current: WorkOrderWizardDraft) => WorkOrderWizardDraft) => void;
/** Auto-fills POC name/phone from the fetched location, once per selected location. */
/**
* Auto-fills POC name/phone from the fetched location, once per selected location.
* Never clobbers fields the dispatcher already typed while detail was loading
* (prototype: autofill from site records, then edit if outdated).
*/
export function useWizardPocAutofill(
open: boolean,
locationId: string,
@ -25,8 +29,12 @@ export function useWizardPocAutofill(
}
setDraft((current) => ({
...current,
pocName: locationDetail.contact ?? current.pocName,
pocPhone: locationDetail.phone ?? current.pocPhone,
pocName: current.pocName.trim()
? current.pocName
: (locationDetail.contact ?? current.pocName),
pocPhone: current.pocPhone.trim()
? current.pocPhone
: (locationDetail.phone ?? current.pocPhone),
}));
setPocAutoFilledFor(locationId);
}, [locationId, locationDetail, pocAutoFilledFor, setDraft]);

View file

@ -30,11 +30,16 @@ export function WizardStepLocationServiceSelect({
}: WizardStepLocationServiceSelectProps) {
const svcSel = [draft.pm, ...draft.extraServices].filter((p, i, a) => !!p && a.indexOf(p) === i);
const clearVendorIfPrimaryChanged = (nextPm: string): Partial<WorkOrderWizardDraft> =>
nextPm !== draft.pm ? { vendorId: "", vendorName: "", techPhone: "" } : {};
const toggleSvc = (p: string) => {
const next = svcSel.includes(p) ? svcSel.filter((x) => x !== p) : [...svcSel, p];
const nextPm = next[0] ?? "";
onPatch({
pm: next[0] ?? "",
pm: nextPm,
extraServices: next.slice(1),
...clearVendorIfPrimaryChanged(nextPm),
});
};
@ -62,6 +67,7 @@ export function WizardStepLocationServiceSelect({
onPatch({
pm: p,
extraServices: draft.extraServices.filter((x) => x !== p),
...clearVendorIfPrimaryChanged(p),
})
}
/>

View file

@ -33,10 +33,14 @@ export function WizardStepLocationSiteSelect({
}))}
onPick={(id) => {
const loc = locations.find((l) => String(l.id) === id);
// Switching site invalidates the previous POC — clear so autofill can
// seed from the new location without clobbering mid-load edits later.
onPatch({
locationId: id,
locationName: loc?.name ?? "",
siteCode: loc ? resolveSiteCode(loc) : "",
pocName: "",
pocPhone: "",
});
}}
/>

View file

@ -2,7 +2,11 @@ import type { WorkOrderBoardCreatePayload } from "@/domain/work-orders/types/wor
import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
import { mondayOf } from "@/domain/work-orders/utils/wizard-date-utils";
/** Mutates `payload` with `dueDate` and day/week schedule fields (range mode has no schedule fields). */
/**
* Mutates `payload` with `dueDate` and schedule fields.
* Range mode lands on the board using the range start as `scheduledDate`
* (prototype `hasScheduleDate`: a range start qualifies as scheduled).
*/
export function assignWizardScheduleFields(
payload: WorkOrderBoardCreatePayload,
draft: WorkOrderWizardDraft,
@ -21,7 +25,7 @@ export function assignWizardScheduleFields(
if (week) {
payload.targetWeek = week;
}
} else if (!isRange && draft.scheduledOn) {
} else if (draft.scheduledOn && (isRange || !draft.scheduleWeekOnly)) {
payload.scheduledDate = draft.scheduledOn;
}
}

View file

@ -0,0 +1,56 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { useWizardPocAutofill } from "@/app/(protected)/workorders/_components/wizard/use-wizard-poc-autofill";
import type { Location } from "@/domain/locations/types/location";
import {
EMPTY_WIZARD_DRAFT,
type WorkOrderWizardDraft,
} from "@/domain/work-orders/types/work-order-wizard";
import { useState } from "react";
function baseLocation(overrides: Partial<Location> = {}): Location {
return {
id: "loc-1",
name: "BK5",
contact: "Janelle Pickett",
phone: "214-555-0111",
...overrides,
} as Location;
}
describe("useWizardPocAutofill", () => {
it("fills empty POC fields from location detail once", () => {
const { result } = renderHook(() => {
const [draft, setDraft] = useState<WorkOrderWizardDraft>({ ...EMPTY_WIZARD_DRAFT });
useWizardPocAutofill(true, "loc-1", baseLocation(), setDraft);
return draft;
});
expect(result.current.pocName).toBe("Janelle Pickett");
expect(result.current.pocPhone).toBe("214-555-0111");
});
it("does not overwrite POC edits made before location detail arrives", () => {
const { result, rerender } = renderHook(
({ detail }: { detail: Location | undefined }) => {
const [draft, setDraft] = useState<WorkOrderWizardDraft>({
...EMPTY_WIZARD_DRAFT,
pocName: "Dispatcher Typed",
pocPhone: "999-000-1111",
});
useWizardPocAutofill(true, "loc-1", detail, setDraft);
return draft;
},
{ initialProps: { detail: undefined as Location | undefined } },
);
expect(result.current.pocName).toBe("Dispatcher Typed");
act(() => {
rerender({ detail: baseLocation() });
});
expect(result.current.pocName).toBe("Dispatcher Typed");
expect(result.current.pocPhone).toBe("999-000-1111");
});
});

View file

@ -0,0 +1,48 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { WizardStepLocationServiceSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select";
import {
EMPTY_WIZARD_DRAFT,
type WorkOrderWizardDraft,
} from "@/domain/work-orders/types/work-order-wizard";
describe("WizardStepLocationServiceSelect — vendor clear on service change", () => {
it("clears vendor fields when the primary service changes", () => {
const onPatch = vi.fn();
const draft: WorkOrderWizardDraft = {
...EMPTY_WIZARD_DRAFT,
pm: "HVAC",
vendorId: "9",
vendorName: "Old Vendor",
techPhone: "555-0100",
};
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(
expect.objectContaining({
pm: "Plumbing",
vendorId: "",
vendorName: "",
techPhone: "",
}),
);
});
});

View file

@ -254,7 +254,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
).toThrow("Site code is required");
});
it("omits scheduledDate for range schedule and puts range in description only", () => {
it("sets scheduledDate from range start and keeps range text in description", () => {
const payload = mapWizardDraftToBoardCreatePayload({
...EMPTY_WIZARD_DRAFT,
@ -273,7 +273,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
vendorNotes: "Also not in description",
});
expect(payload.scheduledDate).toBeUndefined();
expect(payload.scheduledDate).toBe("2026-06-08");
expect(payload.scheduleWeekOnly).toBeUndefined();