fix(work-orders): preserve explicit empty technician assignment

This commit is contained in:
Alexandre Brandizzi 2026-09-29 18:22:30 -03:00
parent 24cbd09a60
commit 5adde3723b
35 changed files with 722 additions and 48 deletions

View file

@ -25,6 +25,13 @@ const VENDORS = [
address: "1 Market St",
},
];
const COASTAL_VENDOR = {
id: 50,
companyName: "Coastal HVAC",
contactName: "Avery Chen",
tradeSpecialties: "HVAC",
address: "9 Shore Rd",
};
const ROSTER = {
companyId: 7,
@ -55,6 +62,10 @@ const BOARD_ROW = {
vendorId: 41,
vendorName: "Vinewood LLC",
techName: "Adam Whyte",
technicianAssigned: true,
completedDate: "2026-08-19",
docStatus: "Yes",
mediaCount: 1,
title: "Quarterly HVAC PM",
rowVersion: "rv-1",
};
@ -81,7 +92,12 @@ function rosterAfterAdd(body: { addTechnicians?: Array<Record<string, unknown>>
return { ...ROSTER, rowVersion: "AAAAAAAAB9F=", technicians: [...ROSTER.technicians, ...added] };
}
async function mockApi(page: Page, writes: Captured[]) {
async function mockApi(
page: Page,
writes: Captured[],
initialRows: Array<Record<string, unknown>> = [BOARD_ROW],
) {
const currentBoardRows = [...initialRows];
await page.clock.install({ time: new Date(FROZEN_NOW) });
await page.clock.resume();
await page.addInitScript((user) => localStorage.setItem("auth", JSON.stringify(user)), TEST_USER);
@ -100,16 +116,59 @@ async function mockApi(page: Page, writes: Captured[]) {
return fulfillJson(route, {
weekStart: "2026-08-17",
weekEnd: "2026-08-21",
counts: { returned: 1, total: 1 },
counts: { returned: currentBoardRows.length, total: currentBoardRows.length },
unscheduled: [],
scheduled: [BOARD_ROW],
scheduled: currentBoardRows,
});
}
if (pathname === "/api/workorders/board" && method === "POST") {
return fulfillJson(route, { data: { id: 900, woNumber: "SH-900" } });
}
if (pathname === "/api/workorders/1/board" && method === "PATCH") {
return fulfillJson(route, { ...BOARD_ROW, vendorId: 42, techName: "Jordan Lee" });
if (/^\/api\/workorders\/\d+\/board$/u.test(pathname) && method === "PATCH") {
const body = bodyOf(request) as {
field?: string;
value?: string;
technicianAssigned?: boolean;
};
const id = Number(pathname.split("/")[3]);
const index = currentBoardRows.findIndex((row) => row.id === id);
if (index < 0) return fulfillJson(route, { message: "Missing fixture row" }, 404);
const current = currentBoardRows[index];
if (current == null) return fulfillJson(route, { message: "Missing fixture row" }, 404);
if (body.field === "vendorId") {
const vendorId = Number(body.value);
currentBoardRows[index] = {
...current,
vendorId,
vendorName: vendorId === 50 ? "Coastal HVAC" : "Vinewood LLC",
...(body.technicianAssigned === false
? { technicianAssigned: false, techName: null, techPhone: null }
: body.technicianAssigned === true
? {
technicianAssigned: true,
techName: vendorId === 50 ? "Avery Chen" : "Adam Whyte",
}
: {}),
rowVersion: "rv-2",
};
} else if (body.field === "lifecycleStatus") {
currentBoardRows[index] = {
...current,
lifecycleStatusLabel: body.value,
...(body.value === "Completed"
? {
frozenCompany: {
name: current.vendorName,
contactName: current.vendorName === "Coastal HVAC" ? "Avery Chen" : "Adam Whyte",
phone: "555-0100",
technicianAssigned: current.technicianAssigned,
},
}
: {}),
rowVersion: "rv-3",
};
}
return fulfillJson(route, currentBoardRows[index]);
}
if (pathname === "/api/vendor-company-roster" && method === "GET") {
return fulfillJson(route, ROSTER);
@ -118,7 +177,7 @@ async function mockApi(page: Page, writes: Captured[]) {
return fulfillJson(route, rosterAfterAdd(bodyOf(request) as never));
}
if (pathname.toLowerCase().includes("/vendors/dropdown")) {
return fulfillJson(route, { data: VENDORS });
return fulfillJson(route, { data: [...VENDORS, COASTAL_VENDOR] });
}
if (pathname.includes("/locations/sites")) {
return fulfillJson(route, {
@ -208,6 +267,171 @@ test.describe("Inline technician registration", () => {
expect(create?.body).toEqual(expect.objectContaining({ vendorId: 42 }));
});
test("company-only save returns no technician and stays empty after reopen and reload", async ({
page,
}) => {
const writes: Captured[] = [];
await mockApi(page, writes);
await page.goto("/workorders");
const row = page.locator("#wo-row-1");
await row.getByRole("button", { name: /Adam Whyte/ }).click();
const dialog = page.getByRole("dialog");
await dialog
.getByRole("button", { name: /Gateway Plumbing/ })
.first()
.click();
await dialog.getByRole("button", { name: "Coastal HVAC" }).click();
await dialog.getByRole("button", { name: /^save$/i }).click();
await expect(dialog).toBeHidden();
const boardPatch = writes.find((write) => write.pathname === "/api/workorders/1/board");
expect(boardPatch?.body).toEqual(
expect.objectContaining({ field: "vendorId", value: "50", technicianAssigned: false }),
);
await expect(row.getByRole("button", { name: /Coastal HVAC/ })).toBeVisible();
await row.getByRole("button", { name: /Coastal HVAC/ }).click();
await expect(
page.getByRole("dialog").getByRole("button", { name: "No technician" }),
).toBeVisible();
await page
.getByRole("dialog")
.getByRole("button", { name: /cancel/i })
.click();
await page.reload();
const reloadedRow = page.locator("#wo-row-1");
await expect(reloadedRow.getByRole("button", { name: /Coastal HVAC/ })).toBeVisible();
await reloadedRow.getByRole("button", { name: /Coastal HVAC/ }).click();
await expect(
page.getByRole("dialog").getByRole("button", { name: "No technician" }),
).toBeVisible();
await page
.getByRole("dialog")
.getByRole("button", { name: /cancel/i })
.click();
await reloadedRow.getByRole("button", { name: /in progress/i }).click();
await page.getByRole("button", { name: "Completed" }).click();
await page
.getByRole("dialog", { name: "Mark as Complete?" })
.getByRole("button", { name: "Mark as Complete" })
.click();
await expect
.poll(() =>
writes.some(
(write) =>
write.pathname === "/api/workorders/1/board" &&
(write.body as { field?: string }).field === "lifecycleStatus",
),
)
.toBe(true);
await expect(reloadedRow.getByRole("button", { name: /completed/i })).toBeVisible();
await reloadedRow.getByRole("button", { name: /Coastal HVAC/ }).click();
const completedDialog = page.getByRole("dialog");
await expect(completedDialog.getByText("—", { exact: true })).toBeVisible();
await expect(completedDialog.getByRole("button", { name: /^save$/i })).toHaveCount(0);
expect(
writes.some(
(write) =>
write.pathname === "/api/workorders/1/board" &&
(write.body as { field?: string }).field === "lifecycleStatus",
),
).toBe(true);
expect(writes.some((write) => write.pathname.startsWith("/api/vendors"))).toBe(false);
});
test("company absence blocks completion for PM, Emergency, Reactive, and Overdue", async ({
page,
}) => {
const writes: Captured[] = [];
const withoutCompany = [
[1, 2],
[2, 3],
[3, 6],
[4, 8],
].map(([id, workOrderType]) => ({
...BOARD_ROW,
id,
woNumber: `WO-${600 + id}`,
workOrderType,
lifecycleStatusLabel: "In Progress",
vendorId: null,
vendorName: null,
techName: "Avery Chen",
technicianAssigned: true,
docStatus: "Yes",
mediaCount: 1,
}));
await mockApi(page, writes, withoutCompany);
await page.goto("/workorders");
for (const id of [1, 2, 3, 4]) {
const row = page.locator(`#wo-row-${id}`);
await row.getByRole("button", { name: /in progress/i }).click();
const completed = page.getByRole("button", { name: "Completed" });
await expect(completed).toBeDisabled();
await expect(completed).toHaveAttribute(
"title",
"Fill in Vendor before completing this work order.",
);
await page.keyboard.press("Escape");
}
expect(writes.filter((write) => write.pathname.endsWith("/board"))).toEqual([]);
});
test("explicit technician choice is retained and can complete after reload", async ({ page }) => {
const writes: Captured[] = [];
const unassigned = {
...BOARD_ROW,
technicianAssigned: false,
techName: null,
techPhone: null,
};
await mockApi(page, writes, [unassigned]);
await page.goto("/workorders");
const row = page.locator("#wo-row-1");
await row.getByRole("button", { name: /Vinewood LLC/ }).click();
const dialog = page.getByRole("dialog");
await dialog.getByRole("button", { name: "No technician" }).click();
await dialog.getByRole("button", { name: "Adam Whyte" }).click();
await dialog.getByRole("button", { name: /^save$/i }).click();
const assignmentWrite = writes.find((write) => write.pathname === "/api/workorders/1/board");
expect(assignmentWrite?.body).toEqual(
expect.objectContaining({ field: "vendorId", value: "41", technicianAssigned: true }),
);
await expect(row.getByRole("button", { name: /Adam Whyte/ })).toBeVisible();
await page.reload();
const reloadedRow = page.locator("#wo-row-1");
await expect(reloadedRow.getByRole("button", { name: /Adam Whyte/ })).toBeVisible();
await reloadedRow.getByRole("button", { name: /Adam Whyte/ }).click();
await expect(
page.getByRole("dialog").getByRole("button", { name: "Adam Whyte" }),
).toBeVisible();
await page
.getByRole("dialog")
.getByRole("button", { name: /cancel/i })
.click();
await reloadedRow.getByRole("button", { name: /in progress/i }).click();
await page.getByRole("button", { name: "Completed" }).click();
await page
.getByRole("dialog", { name: "Mark as Complete?" })
.getByRole("button", { name: "Mark as Complete" })
.click();
await expect
.poll(() => writes.filter((write) => write.pathname === "/api/workorders/1/board").length)
.toBe(2);
await expect(reloadedRow.getByRole("button", { name: /completed/i })).toBeVisible();
expect(
writes.some(
(write) =>
write.pathname === "/api/workorders/1/board" &&
(write.body as { field?: string; value?: string }).field === "lifecycleStatus",
),
).toBe(true);
});
test("cancelling the wizard after staging a technician writes no vendor (SH-366)", async ({
page,
}) => {

View file

@ -116,7 +116,10 @@ export function useCompDocDialog({
amazonRep,
seaRep,
siteCode: site.code,
tech: company.contactName || "",
tech:
info.technicianAssigned === false || company.technicianAssigned === false
? ""
: company.contactName || "",
company: company.name || "",
};

View file

@ -172,6 +172,7 @@ function SlideOverVendorField({
company={company.name ?? source.company}
tech={company.contactName ?? source.tech}
techPhone={company.phone ?? source.techPhone}
technicianAssigned={source.technicianAssigned ?? source.frozenCompany?.technicianAssigned}
vendors={vendors}
viewOnly
frozenCompany={source.frozenCompany}

View file

@ -216,6 +216,7 @@ export function SlideOverInfoTabEditView({
company={draft.company}
tech={draft.tech}
techPhone={draft.techPhone}
technicianAssigned={draft.technicianAssigned}
vendors={vendors}
onSave={(patch) => onDraftChange(toVendorTablePatch(patch))}
/>

View file

@ -33,6 +33,7 @@ export function buildInlineDraftCreateInput(
avetaRequired: Boolean(draft.avetaRequired),
pmNote: draft.serviceNotes ?? "",
vendorId: draft.vendorId || "",
technicianAssigned: draft.technicianAssigned,
vendorName: draft.company,
techPhone: draft.techPhone,
vendorNotes: "",

View file

@ -1,5 +1,7 @@
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
export const NO_TECHNICIAN_KEY = "__no_technician__";
/**
* Board rows often have tech/company but no vendorId — resolve via vendors list.
* Prefers a stable vendor id over name+company matching.

View file

@ -11,7 +11,10 @@ import {
mergeVendorDropdownItems,
resolveVendorCompanyAnchor,
} from "@/app/(protected)/workorders/_components/list/table/cells/resolve-vendor-company-anchor";
import { resolveTechnicianKey } from "@/app/(protected)/workorders/_components/list/table/cells/resolve-technician-key";
import {
NO_TECHNICIAN_KEY,
resolveTechnicianKey,
} from "@/app/(protected)/workorders/_components/list/table/cells/resolve-technician-key";
import { useVendorDialogSave } from "@/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-save";
import { computeVendorDialogCanSubmit } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-can-submit";
import type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
@ -23,6 +26,7 @@ type UseVendorDialogFormArgs = {
company: string;
tech: string;
techPhone: string;
technicianAssigned?: boolean | null;
vendors: VendorDropdownItem[];
onSave: (patch: VendorPatch) => void;
};
@ -34,13 +38,16 @@ export function useVendorDialogForm({
company,
tech,
techPhone,
technicianAssigned,
vendors,
onSave,
}: UseVendorDialogFormArgs) {
const addTechnicians = useAddTechniciansToCompany();
const [selectedCompany, setSelectedCompany] = useState(company);
const [selectedKey, setSelectedKey] = useState(() =>
resolveTechnicianKey(vendorId, tech, company, vendors),
technicianAssigned === false
? NO_TECHNICIAN_KEY
: resolveTechnicianKey(vendorId, tech, company, vendors),
);
const [t, setT] = useState(tech);
const [phone, setPhone] = useState(techPhone);
@ -66,7 +73,10 @@ export function useVendorDialogForm({
useEffect(() => {
if (open) {
const key = resolveTechnicianKey(vendorId, tech, company, vendors);
const key =
technicianAssigned === false
? NO_TECHNICIAN_KEY
: resolveTechnicianKey(vendorId, tech, company, vendors);
const initialOptions = buildTechnicianOptions(vendors, company, tech, company);
setSelectedCompany(company);
setSelectedKey(key);
@ -78,7 +88,7 @@ export function useVendorDialogForm({
setSaveError(null);
}
// invariant: rebuilding options after Add must not clear staged technician drafts.
}, [open, tech, techPhone, vendors, company, vendorId]);
}, [open, tech, techPhone, vendors, company, vendorId, technicianAssigned]);
const selected = technicianOptions.find((option) => option.key === selectedKey);
const resolvedCompany = selectedCompany;
@ -102,7 +112,10 @@ export function useVendorDialogForm({
},
baseline: {
company,
selectedKey: resolveTechnicianKey(vendorId, tech, company, vendors),
selectedKey:
technicianAssigned === false
? NO_TECHNICIAN_KEY
: resolveTechnicianKey(vendorId, tech, company, vendors),
phone:
techPhone || phoneForOption(tech, buildTechnicianOptions(vendors, company, tech, company)),
notes: "",
@ -114,6 +127,8 @@ export function useVendorDialogForm({
allVendors,
pendingCreates,
selectedKey,
vendorId,
originalCompany: company,
selectedVendor,
resolvedCompany,
anchorCompanyName,
@ -155,6 +170,12 @@ export function useVendorDialogForm({
}
},
handlePick: (key: string) => {
if (key === NO_TECHNICIAN_KEY) {
setSelectedKey(key);
setT("");
setPhone("");
return;
}
const option = technicianOptions.find((o) => o.key === key);
setSelectedKey(key);
setT(option?.name ?? key);

View file

@ -8,6 +8,8 @@ type UseVendorDialogSaveArgs = {
allVendors: VendorDropdownItem[];
pendingCreates: Record<string, VendorFormValues>;
selectedKey: string;
vendorId: string | number;
originalCompany: string;
selectedVendor: VendorDropdownItem | undefined;
resolvedCompany: string;
anchorCompanyName: string;
@ -56,6 +58,8 @@ export function useVendorDialogSave({
allVendors,
pendingCreates,
selectedKey,
vendorId,
originalCompany,
selectedVendor,
resolvedCompany,
anchorCompanyName,
@ -68,7 +72,7 @@ export function useVendorDialogSave({
canSubmit,
}: UseVendorDialogSaveArgs) {
const resolveSaveVendor = () =>
allVendors.find((vendor) => String(vendor.id) === selectedKey) ??
(selectedKey ? allVendors.find((vendor) => String(vendor.id) === selectedKey) : undefined) ??
allVendors.find(
(vendor) =>
vendor.contactName === tech &&
@ -97,11 +101,22 @@ export function useVendorDialogSave({
}
}
const noTechnician = selectedKey === "" || selectedKey === "__no_technician__";
const preservedVendorId =
noTechnician && resolvedCompany === originalCompany && vendorId !== "" && vendorId != null
? String(vendorId)
: "";
onSave({
vendorId: saveVendor ? String(saveVendor.id) : "",
vendorId:
selectedKey && !noTechnician
? saveVendor
? String(saveVendor.id)
: ""
: preservedVendorId || (noTechnician && saveVendor ? String(saveVendor.id) : ""),
company: resolvedCompany || anchorCompanyName,
tech: selectedKey ? (saveVendor?.contactName ?? tech) : "",
techPhone: selectedKey ? phone : "",
tech: selectedKey && !noTechnician ? (saveVendor?.contactName ?? tech) : "",
techPhone: selectedKey && !noTechnician ? phone : "",
technicianAssigned: Boolean(selectedKey && !noTechnician),
});
onOpenChange(false);
};

View file

@ -10,6 +10,7 @@ type VendorCellProps = {
company: string;
tech: string;
techPhone?: string;
technicianAssigned?: boolean | null;
vendors: VendorDropdownItem[];
q?: string;
viewOnly?: boolean;
@ -22,6 +23,7 @@ export function VendorCell({
company,
tech,
techPhone,
technicianAssigned,
vendors,
q,
viewOnly = false,
@ -29,7 +31,9 @@ export function VendorCell({
onSave,
}: VendorCellProps) {
const [open, setOpen] = useState(false);
const hasAny = !!(tech || company);
const effectiveTech =
technicianAssigned === false || frozenCompany?.technicianAssigned === false ? "" : tech;
const hasAny = !!(effectiveTech || company);
return (
<>
@ -44,9 +48,13 @@ export function VendorCell({
{hasAny ? (
<div className="min-w-0 flex-1 leading-tight">
<div className="truncate text-[13px] font-semibold text-[var(--foreground)]">
{tech ? <HighlightText text={tech} q={q} /> : <HighlightText text={company} q={q} />}
{effectiveTech ? (
<HighlightText text={effectiveTech} q={q} />
) : (
<HighlightText text={company} q={q} />
)}
</div>
{Boolean(tech && company) && (
{Boolean(effectiveTech && company) && (
<div className="truncate text-[11px] text-[var(--muted-foreground)]">
<HighlightText text={company} q={q} />
</div>
@ -64,6 +72,7 @@ export function VendorCell({
company={company}
tech={tech}
techPhone={techPhone}
technicianAssigned={technicianAssigned ?? frozenCompany?.technicianAssigned}
vendors={vendors}
viewOnly={viewOnly}
frozenCompany={frozenCompany}

View file

@ -15,6 +15,7 @@ import type {
TechnicianOption,
} from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-options";
import type { WorkOrderFrozenCompany } from "@/domain/work-orders/types/work-order-table-row";
import { NO_TECHNICIAN_KEY } from "@/app/(protected)/workorders/_components/list/table/cells/resolve-technician-key";
type VendorDialogBodyProps = {
selectedCompany: string;
@ -29,6 +30,7 @@ type VendorDialogBodyProps = {
resolvedCompany: string;
selectedVendor: VendorDropdownItem | undefined;
frozenCompany?: WorkOrderFrozenCompany;
technicianAssigned?: boolean | null;
saveError: string | null;
onCompanyPick: (company: string) => void;
onPick: (key: string) => void;
@ -54,6 +56,7 @@ export function VendorDialogBody({
resolvedCompany,
selectedVendor,
frozenCompany,
technicianAssigned,
saveError,
onCompanyPick,
onPick,
@ -100,7 +103,9 @@ export function VendorDialogBody({
<FieldLabel>Technician (optional)</FieldLabel>
{frozenCompany != null ? (
<div className={DLG_INPUT_CLS} style={DLG_INPUT_STYLE}>
{frozenCompany.contactName?.trim() || tech || "—"}
{technicianAssigned === false || frozenCompany.technicianAssigned === false
? "—"
: frozenCompany.contactName?.trim() || tech || "—"}
</div>
) : (
<WizardFieldSelect
@ -115,10 +120,13 @@ export function VendorDialogBody({
/>
}
searchPlaceholder="Search technician…"
options={technicianOptions.map((option) => ({
key: option.key,
label: option.name,
}))}
options={[
...technicianOptions.map((option) => ({
key: option.key,
label: option.name,
})),
{ key: NO_TECHNICIAN_KEY, label: "No technician" },
]}
onPick={onPick}
/>
)}

View file

@ -3,4 +3,5 @@ export type VendorPatch = {
company: string;
tech: string;
techPhone: string;
technicianAssigned?: boolean;
};

View file

@ -25,6 +25,7 @@ type VendorDialogProps = {
company: string;
tech: string;
techPhone?: string;
technicianAssigned?: boolean | null;
vendors: VendorDropdownItem[];
viewOnly?: boolean;
frozenCompany?: WorkOrderFrozenCompany;
@ -38,6 +39,7 @@ export function VendorDialog({
company,
tech,
techPhone = "",
technicianAssigned,
vendors,
viewOnly = false,
frozenCompany,
@ -50,6 +52,7 @@ export function VendorDialog({
company,
tech,
techPhone,
technicianAssigned,
vendors,
onSave,
});
@ -94,12 +97,21 @@ export function VendorDialog({
technicianOptions={form.technicianOptions}
anchorCompanyName={form.anchorCompanyName}
companyVendorForAdd={viewOnly ? undefined : (form.companyVendor ?? undefined)}
tech={frozenCompany?.contactName ?? form.selected?.name ?? form.t}
phone={frozenCompany?.phone ?? form.phone}
tech={
technicianAssigned === false || frozenCompany?.technicianAssigned === false
? ""
: (frozenCompany?.contactName ?? form.selected?.name ?? form.t)
}
phone={
technicianAssigned === false || frozenCompany?.technicianAssigned === false
? ""
: (frozenCompany?.phone ?? form.phone)
}
notes={form.notes}
resolvedCompany={form.resolvedCompany}
selectedVendor={form.selectedVendor}
frozenCompany={frozenCompany}
technicianAssigned={technicianAssigned}
saveError={form.saveError}
onCompanyPick={form.handleCompanyPick}
onPick={form.handlePick}

View file

@ -21,6 +21,7 @@ export function toInlineDraftRow(draft: WorkOrderTableDraft) {
scheduleEnd: draft.scheduleEnd,
schedMode: draft.schedMode,
vendorId: draft.vendorId,
technicianAssigned: draft.technicianAssigned,
completedDate: "",
dueDate: draft.dueDate,
pm: draft.pm,

View file

@ -77,6 +77,7 @@ export function WoTableRowServiceCells({
company={company.name ?? row.company}
tech={company.contactName ?? row.tech}
techPhone={company.phone ?? row.techPhone}
technicianAssigned={row.technicianAssigned ?? row.frozenCompany?.technicianAssigned}
vendors={vendors}
q={search}
viewOnly={viewOnly}

View file

@ -30,9 +30,14 @@ export function buildCreatedTechniciansPatch(
const patch: Partial<WorkOrderWizardDraft> = { newTechnicians: [] };
const [only] = created;
// Prototype parity: a single new technician is assigned when none was picked.
if (created.length === 1 && only != null && draft.vendorId === "") {
if (
created.length === 1 &&
only != null &&
(draft.technicianAssigned === false || draft.vendorId === "")
) {
patch.vendorId = String(only.id);
patch.vendorName = company;
patch.technicianAssigned = true;
patch.techPhone = pending[0]?.phone.trim() || draft.techPhone;
}
return patch;

View file

@ -190,7 +190,10 @@ export function WizardStepVendorTime({
onUpdate,
onPatch,
}: WizardStepVendorTimeProps) {
const selectedVendor = vendors.find((v) => String(v.id) === draft.vendorId);
const selectedVendor =
draft.technicianAssigned === false
? undefined
: vendors.find((v) => String(v.id) === draft.vendorId);
const statusValue = (draft.status || "Incomplete") as WizardWOStatus;
const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Incomplete;
const selectedDistance = formatVendorDistance(selectedVendor?.distanceMiles);

View file

@ -5,6 +5,7 @@ import { buildCompanyOptions } from "@/app/(protected)/workorders/_components/li
import { WizardFieldSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-field-select";
import { WizLabelRow } from "@/app/(protected)/workorders/_components/wizard/wizard-labels";
import { WizardVendorNewTechnicians } from "@/app/(protected)/workorders/_components/wizard/wizard-vendor-new-technicians";
import { NO_TECHNICIAN_KEY } from "@/app/(protected)/workorders/_components/list/table/cells/resolve-technician-key";
type WizardVendorCompanyFieldsProps = {
draft: WorkOrderWizardDraft;
@ -49,6 +50,15 @@ export function WizardVendorCompanyFields({
key: String(vendor.id),
label: vendor.contactName.trim() || vendor.companyName,
}));
const companyAnchor = vendors.find((vendor) => vendor.companyName === company);
const currentVendorInCompany = vendors.some(
(vendor) => String(vendor.id) === draft.vendorId && vendor.companyName === company,
);
const noTechnicianVendorId = currentVendorInCompany
? draft.vendorId
: companyAnchor
? String(companyAnchor.id)
: "";
return (
<>
@ -77,20 +87,22 @@ export function WizardVendorCompanyFields({
icon={<Building2 size={16} className="shrink-0" style={ICON_STYLE} />}
searchPlaceholder="Search company…"
options={companyOptions.map((option) => ({ key: option.key, label: option.label }))}
onPick={(nextCompany) =>
onPick={(nextCompany) => {
const nextAnchor = vendors.find((vendor) => vendor.companyName === nextCompany);
onPatch({
vendorCompany: nextCompany,
...(nextCompany !== company
? {
vendorId: "",
vendorName: "",
vendorId: nextAnchor ? String(nextAnchor.id) : "",
vendorName: nextAnchor ? nextCompany : "",
technicianAssigned: false,
techPhone: "",
vendorNotes: "",
newTechnicians: [],
}
: {}),
})
}
});
}}
/>
</div>
@ -98,15 +110,26 @@ export function WizardVendorCompanyFields({
<WizLabelRow
optional
onClear={
draft.vendorId !== ""
? () => onPatch({ vendorId: "", vendorName: "", techPhone: "", vendorNotes: "" })
draft.vendorId !== "" || draft.technicianAssigned === true
? () =>
onPatch({
vendorId: noTechnicianVendorId,
vendorName: companyAnchor ? company : "",
technicianAssigned: false,
techPhone: "",
vendorNotes: "",
})
: undefined
}
>
Technician
</WizLabelRow>
<WizardFieldSelect
value={draft.vendorId}
value={
draft.technicianAssigned === false && draft.vendorId
? NO_TECHNICIAN_KEY
: draft.vendorId
}
placeholder={company === "" ? "Pick a company first" : "Select technician…"}
disabled={company === ""}
icon={
@ -117,13 +140,23 @@ export function WizardVendorCompanyFields({
)
}
searchPlaceholder="Search technician…"
options={technicianOptions}
options={[...technicianOptions, { key: NO_TECHNICIAN_KEY, label: "No technician" }]}
onPick={(id) => {
if (id === NO_TECHNICIAN_KEY) {
onPatch({
vendorId: noTechnicianVendorId,
vendorName: companyAnchor ? company : "",
technicianAssigned: false,
techPhone: "",
});
return;
}
const vendor = vendors.find((v) => String(v.id) === id);
onPatch({
vendorCompany: vendor?.companyName ?? company,
vendorId: id,
vendorName: vendor?.companyName ?? "",
technicianAssigned: true,
techPhone: "",
});
}}

View file

@ -11,10 +11,13 @@ export function assignWizardVendorApptFields(
draft: WorkOrderWizardDraft,
): void {
const vendorId = draft.vendorId.trim() ? Number(draft.vendorId) : NaN;
const hasVendor = Number.isFinite(vendorId);
const hasVendor = Number.isFinite(vendorId) && vendorId > 0;
if (hasVendor) {
payload.vendorId = vendorId;
if (draft.technicianAssigned != null) {
payload.technicianAssigned = draft.technicianAssigned;
}
}
const apptTime = formatApptTimeValue(draft.apptStart, draft.apptEnd);

View file

@ -7,6 +7,7 @@ import { mapDocStatusToApi } from "@/domain/work-orders/mappers/doc-status-mappe
export type BoardFieldOp = {
field: WorkOrderBoardEditableField;
value: string;
technicianAssigned?: boolean;
};
export { mapDocStatusToApi };
@ -57,7 +58,13 @@ export function expandScalarBoardOps(patch: WorkOrderTablePatch): BoardFieldOp[]
for (const rule of SCALAR_BOARD_OP_RULES) {
const value = patch[rule.key];
if (value === undefined) continue;
ops.push({ field: rule.field, value: rule.toValue(value) });
ops.push({
field: rule.field,
value: rule.toValue(value),
...(rule.key === "vendorId" && patch.technicianAssigned != null
? { technicianAssigned: patch.technicianAssigned }
: {}),
});
}
return ops;

View file

@ -108,6 +108,10 @@ export function buildBoardPatchPayload(
workOrderVersion: row.rowVersion,
};
if (op.field === "vendorId" && op.technicianAssigned != null) {
payload.technicianAssigned = op.technicianAssigned;
}
if (isDispatchBoardField(op.field)) {
if (row.dispatchRowVersion) {
payload.dispatchVersion = row.dispatchRowVersion;

View file

@ -285,6 +285,8 @@ export function mapWorkOrderDispatchSummary(raw: unknown): WorkOrderDispatchSumm
export function mapWorkOrderDetail(raw: unknown): WorkOrderDetail {
const wo = asRecord(raw);
const technicianAssigned =
"technicianAssigned" in wo ? wo.technicianAssigned : wo.TechnicianAssigned;
const locationId = wo.locationId ?? wo.LocationId;
const contactIds = readOptionalIdList(wo, "contactIds", "ContactIds", "contacts", "Contacts");
const categoryIds = readOptionalIdList(
@ -296,6 +298,9 @@ export function mapWorkOrderDetail(raw: unknown): WorkOrderDetail {
);
return {
...(typeof technicianAssigned === "boolean" || technicianAssigned === null
? { technicianAssigned: technicianAssigned as boolean | null }
: {}),
id: readId(wo),
internalWONumber: readString(wo, "internalWONumber", "InternalWONumber"),
workOrderNumber: readString(wo, "workerOrderNumber", "workOrderNumber", "WorkerOrderNumber"),

View file

@ -72,6 +72,18 @@ function readBool(record: Record<string, unknown>, ...keys: string[]): boolean {
return false;
}
function readNullableBool(
record: Record<string, unknown>,
...keys: string[]
): boolean | null | undefined {
for (const key of keys) {
if (!(key in record)) continue;
const value = record[key];
return typeof value === "boolean" ? value : null;
}
return undefined;
}
function readStringArray(record: Record<string, unknown>, ...keys: string[]): string[] {
for (const key of keys) {
const value = record[key];
@ -293,9 +305,11 @@ function readFrozenSite(item: Record<string, unknown>): WorkOrderFrozenSite | un
function readFrozenCompany(item: Record<string, unknown>): WorkOrderFrozenCompany | undefined {
const source = asRecord(item.frozenCompany ?? item.FrozenCompany);
if (Object.keys(source).length === 0) return undefined;
const technicianAssigned = readNullableBool(source, "technicianAssigned", "TechnicianAssigned");
return {
name: readString(source, "name", "Name"),
contactName: readString(source, "contactName", "ContactName"),
...(technicianAssigned !== undefined ? { technicianAssigned } : {}),
address: readString(source, "address", "Address"),
city: readString(source, "city", "City"),
state: readString(source, "state", "State"),
@ -380,6 +394,14 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
const boardUpliftSummary = readUpliftSummary(item);
const hasPendingUplift = readHasPendingUplift(item, boardUpliftSummary);
const upliftSummary = boardUpliftSummary == null ? undefined : mapUpliftSummary(item);
const frozenCompany = readFrozenCompany(item);
const rowTechnicianAssigned = readNullableBool(item, "technicianAssigned", "TechnicianAssigned");
const technicianAssigned =
rowTechnicianAssigned === false || frozenCompany?.technicianAssigned === false
? false
: rowTechnicianAssigned !== undefined
? rowTechnicianAssigned
: frozenCompany?.technicianAssigned;
const row: WorkOrderTableRow = {
id: readId(item),
@ -406,8 +428,12 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
vendorId: vendorIdNum != null ? String(vendorIdNum) : "",
company: readString(item, "company", "Company", "vendorName", "VendorName"),
primaryDispatchStatus: readPrimaryDispatchStatus(item),
tech: readString(item, "techName", "TechName", "tech", "Tech", "technician", "Technician"),
techPhone: readString(item, "techPhone", "TechPhone"),
tech:
technicianAssigned === false
? ""
: readString(item, "techName", "TechName", "tech", "Tech", "technician", "Technician"),
techPhone: technicianAssigned === false ? "" : readString(item, "techPhone", "TechPhone"),
...(technicianAssigned !== undefined ? { technicianAssigned } : {}),
apptStart,
apptEnd,
apptTime,
@ -418,7 +444,7 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
pocNotes: readString(item, "pocNotes", "PocNotes", "POCNotes"),
additionalContacts: readAdditionalContacts(item),
frozenSite: readFrozenSite(item),
frozenCompany: readFrozenCompany(item),
frozenCompany,
frozenPoc: readFrozenPoc(item),
dueDate: toDateInputValue(readString(item, "dueDate", "DueDate")),
rescheduleCount: readNumber(item, "rescheduleCount", "RescheduleCount") ?? 0,

View file

@ -42,6 +42,7 @@ export interface WorkOrderBoardDetailInfoApi {
scheduleWeekOnly?: boolean | null;
dayGroup?: string | null;
vendorId?: number | null;
technicianAssigned?: boolean | null;
vendorName?: string | null;
primaryDispatchStatus?: string | null;
techName?: string | null;

View file

@ -98,6 +98,8 @@ export interface WorkOrderBoardCreatePayload {
*/
isAddOn?: boolean;
vendorId?: number;
/** Explicit technician choice; null/omitted retains legacy vendor inference. */
technicianAssigned?: boolean | null;
apptDate?: string;
apptTime?: string;
docStatus?: DocStatusApi;
@ -131,6 +133,8 @@ export interface WorkOrderBoardCreatePayload {
export interface WorkOrderBoardPatchPayload {
field: string;
value?: string | null;
/** Atomic with vendorId PATCH; never sent as an independent field operation. */
technicianAssigned?: boolean;
workOrderVersion: string;
dispatchVersion?: string | null;
primaryDispatchId?: number | null;
@ -193,6 +197,8 @@ export interface WorkOrderBoardRowApi {
additionalContacts?: WorkOrderAdditionalContact[] | null;
frozenSite?: WorkOrderFrozenSite | null;
frozenCompany?: WorkOrderFrozenCompany | null;
/** Work-order-owned assignment state; null/omitted means legacy inference. */
technicianAssigned?: boolean | null;
frozenPoc?: WorkOrderFrozenPoc | null;
lifecycleStatus?: number | null;
lifecycleStatusLabel?: string | null;

View file

@ -31,6 +31,7 @@ export interface WorkOrderAuditEntry {
}
export interface WorkOrderDetail {
technicianAssigned?: boolean | null;
id: string | number;
internalWONumber: string;
workOrderNumber: string;

View file

@ -28,6 +28,7 @@ export type WorkOrderFrozenSite = {
export type WorkOrderFrozenCompany = {
name?: string;
contactName?: string;
technicianAssigned?: boolean | null;
address?: string;
city?: string;
state?: string;
@ -70,6 +71,8 @@ export interface WorkOrderTableRow {
primaryDispatchStatus?: string;
tech: string;
techPhone: string;
/** Work-order-owned assignment state; null/omitted means legacy inference. */
technicianAssigned?: boolean | null;
apptStart: string;
apptEnd: string;
apptTime: string;
@ -133,6 +136,7 @@ export interface WorkOrderTableDraft {
company: string;
tech: string;
techPhone: string;
technicianAssigned?: boolean | null;
apptStart: string;
apptEnd: string;
status: WizardWOStatus | "";

View file

@ -54,6 +54,8 @@ export interface WorkOrderWizardDraft {
vendorName: string;
/** Company picked on the Vendor & time step; scopes the technician list. */
vendorCompany?: string;
/** Explicit technician choice; null/omitted retains legacy inference. */
technicianAssigned?: boolean | null;
/** Technicians registered inline; persisted under `vendorCompany` only on create. */
newTechnicians?: WizardNewTechnician[];
techPhone: string;

View file

@ -41,7 +41,7 @@ function withCompDocIdentity(address: CompDocAddress, row: CompDocIdentityRow):
return {
...address,
code: frozen?.code ?? row.site,
tech: company.contactName ?? "",
tech: company.technicianAssigned === false ? "" : (company.contactName ?? ""),
company: company.name ?? "",
};
}

View file

@ -5,11 +5,12 @@ export type VendorAssignmentFields = {
company: string;
tech: string;
techPhone: string;
technicianAssigned?: boolean;
};
export type VendorTablePatch = Pick<
WorkOrderTableRow,
"vendorId" | "company" | "tech" | "techPhone" | "primaryDispatchStatus"
"vendorId" | "company" | "tech" | "techPhone" | "technicianAssigned" | "primaryDispatchStatus"
>;
export function toVendorTablePatch(fields: VendorAssignmentFields): VendorTablePatch {
@ -18,6 +19,9 @@ export function toVendorTablePatch(fields: VendorAssignmentFields): VendorTableP
company: fields.company,
tech: fields.tech,
techPhone: fields.techPhone,
...(fields.technicianAssigned !== undefined
? { technicianAssigned: fields.technicianAssigned }
: {}),
primaryDispatchStatus: "",
};
}

View file

@ -93,6 +93,7 @@ describe("VendorDialog save gate", () => {
vendorId: "11",
tech: "Casey New",
company: "Gateway Plumbing",
technicianAssigned: true,
}),
);
});
@ -122,10 +123,40 @@ describe("VendorDialog save gate", () => {
company: "Gateway Plumbing",
tech: "",
techPhone: "",
technicianAssigned: false,
}),
);
});
it("clears a selected technician while retaining the same company vendor anchor", async () => {
const onSave = vi.fn();
renderWithProviders(
<VendorDialog
open
onOpenChange={vi.fn()}
vendorId={11}
company="Gateway Plumbing"
tech="Casey New"
techPhone="555-0111"
technicianAssigned
vendors={[jordan, casey]}
onSave={onSave}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /casey new/i }));
fireEvent.click(await screen.findByRole("button", { name: "No technician" }));
fireEvent.click(screen.getByRole("button", { name: /^save$/i }));
expect(onSave).toHaveBeenCalledWith({
vendorId: "11",
company: "Gateway Plumbing",
tech: "",
techPhone: "",
technicianAssigned: false,
});
});
it("only offers technicians from the selected company", async () => {
renderWithProviders(
<VendorDialog

View file

@ -1,7 +1,10 @@
import { act, fireEvent, render, renderHook, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { WizardStepVendorTime } from "@/app/(protected)/workorders/_components/wizard/wizard-step-vendor-time";
import { useWizardCreateWithTechnicians } from "@/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians";
import {
buildCreatedTechniciansPatch,
useWizardCreateWithTechnicians,
} from "@/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians";
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
import {
EMPTY_WIZARD_DRAFT,
@ -110,14 +113,98 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => {
expect(onPatch).toHaveBeenCalledWith({
vendorCompany: "Gateway Plumbing",
vendorId: "",
vendorName: "",
vendorId: "52",
vendorName: "Gateway Plumbing",
technicianAssigned: false,
techPhone: "",
vendorNotes: "",
newTechnicians: [],
});
});
it("keeps the existing company anchor when company is chosen without a technician", async () => {
const onPatch = vi.fn();
render(
<WizardStepVendorTime
draft={draftWith({})}
vendors={vendors}
onUpdate={vi.fn()}
onPatch={onPatch}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /select company/i }));
fireEvent.click(await screen.findByRole("button", { name: "Vinewood LLC" }));
expect(onPatch).toHaveBeenCalledWith({
vendorCompany: "Vinewood LLC",
vendorId: "41",
vendorName: "Vinewood LLC",
technicianAssigned: false,
techPhone: "",
vendorNotes: "",
newTechnicians: [],
});
});
it("marks the exact selected technician as assigned", async () => {
const onPatch = vi.fn();
render(
<WizardStepVendorTime
draft={draftWith({
vendorCompany: "Vinewood LLC",
vendorId: "41",
technicianAssigned: false,
})}
vendors={vendors}
onUpdate={vi.fn()}
onPatch={onPatch}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "No technician" }));
fireEvent.click(await screen.findByRole("button", { name: "Adam Whyte" }));
expect(onPatch).toHaveBeenLastCalledWith({
vendorCompany: "Vinewood LLC",
vendorId: "41",
vendorName: "Vinewood LLC",
technicianAssigned: true,
techPhone: "",
});
});
it("retains the same company vendor id when clearing a second technician", async () => {
const onPatch = vi.fn();
const secondVinewoodTech = {
...vendors[0],
id: 42,
contactName: "Second Tech",
};
render(
<WizardStepVendorTime
draft={draftWith({
vendorCompany: "Vinewood LLC",
vendorId: "42",
technicianAssigned: true,
})}
vendors={[vendors[0], secondVinewoodTech, vendors[1]]}
onUpdate={vi.fn()}
onPatch={onPatch}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /Second Tech/u }));
fireEvent.click(await screen.findByRole("button", { name: "No technician" }));
expect(onPatch).toHaveBeenCalledWith({
vendorId: "42",
vendorName: "Vinewood LLC",
technicianAssigned: false,
techPhone: "",
});
});
it("preserves the picked vendor and staged technicians when the same company is re-picked", () => {
const onPatch = vi.fn();
render(
@ -192,11 +279,54 @@ describe("useWizardCreateWithTechnicians (SH-364)", () => {
vendorId: "99",
vendorName: "Vinewood LLC",
techPhone: "(314) 555-0199",
technicianAssigned: true,
};
expect(patchDraft).toHaveBeenCalledWith(expectedPatch);
expect(createMutation.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedPatch));
});
it.each([
["legacy selection", { vendorId: "41" }, { newTechnicians: [] }],
["explicit selection", { vendorId: "41", technicianAssigned: true }, { newTechnicians: [] }],
])("preserves the existing %s when one technician is staged", (_caseName, existing, expected) => {
const created = { ...vendors[0], id: 99, contactName: "Jordan Lee" };
const patch = buildCreatedTechniciansPatch(
draftWith({
vendorCompany: "Vinewood LLC",
...existing,
newTechnicians: [{ name: "Jordan Lee", phone: "" }],
}),
"Vinewood LLC",
[{ name: "Jordan Lee", phone: "" }],
[created],
);
expect(patch).toEqual(expected);
});
it("assigns the only new technician when the existing company is explicitly company-only", () => {
const created = { ...vendors[0], id: 99, contactName: "Jordan Lee" };
expect(
buildCreatedTechniciansPatch(
draftWith({
vendorCompany: "Vinewood LLC",
vendorId: "41",
technicianAssigned: false,
newTechnicians: [{ name: "Jordan Lee", phone: "" }],
}),
"Vinewood LLC",
[{ name: "Jordan Lee", phone: "" }],
[created],
),
).toEqual({
newTechnicians: [],
vendorId: "99",
vendorName: "Vinewood LLC",
technicianAssigned: true,
techPhone: "",
});
});
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

@ -172,6 +172,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
scheduleWeekOnly: false,
vendorId: "5",
technicianAssigned: false,
apptStart: "09:00",
@ -214,6 +215,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
isAddOn: true,
vendorId: 5,
technicianAssigned: false,
apptDate: "2026-06-25",
@ -336,6 +338,17 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
expect(payload.apptTime).toBeUndefined();
});
it("creates with the selected technician id and explicit assignment state", () => {
const payload = mapWizardDraftToBoardCreatePayload({
...EMPTY_WIZARD_DRAFT,
type: "PM",
siteCode: "BK5",
vendorId: "11",
technicianAssigned: true,
});
expect(payload).toMatchObject({ vendorId: 11, technicianAssigned: true });
});
it("omits appointment fields when vendor is missing but still sends techPhone and vendorNotes", () => {
const payload = mapWizardDraftToBoardCreatePayload({
...EMPTY_WIZARD_DRAFT,

View file

@ -178,9 +178,10 @@ describe("expandTablePatchToBoardFieldOps", () => {
company: "New Co",
tech: "Pat",
techPhone: "555",
technicianAssigned: false,
}),
),
).toEqual([{ field: "vendorId", value: "9" }]);
).toEqual([{ field: "vendorId", value: "9", technicianAssigned: false }]);
expect(
expandTablePatchToBoardFieldOps({
@ -259,6 +260,32 @@ describe("buildBoardPatchPayload", () => {
});
});
it("keeps technician choice in the same vendorId PATCH envelope", () => {
const op = expandTablePatchToBoardFieldOps({
vendorId: "11",
technicianAssigned: false,
company: "Gateway Plumbing",
tech: "",
techPhone: "",
})[0];
expect(op).toEqual({ field: "vendorId", value: "11", technicianAssigned: false });
expect(
buildBoardPatchPayload(
op as { field: "vendorId"; value: string; technicianAssigned: boolean },
{
rowVersion: "wo-v1",
dispatchRowVersion: "disp-v1",
primaryDispatchId: 9,
},
),
).toMatchObject({
field: "vendorId",
value: "11",
technicianAssigned: false,
primaryDispatchId: 9,
});
});
it("omits non-positive primaryDispatchId on dispatch fields", () => {
const payload = buildBoardPatchPayload(
{ field: "vendorId", value: "7" },

View file

@ -212,6 +212,57 @@ describe("mapWorkOrderTableRow completion snapshot", () => {
});
});
it("honors explicit no-technician on the returned row and frozen company snapshot", () => {
const current = mapWorkOrderTableRow({
id: 201,
vendorId: 10,
vendorName: "Gateway Plumbing",
technicianAssigned: false,
techName: null,
techPhone: null,
});
expect(current).toMatchObject({
vendorId: "10",
company: "Gateway Plumbing",
technicianAssigned: false,
tech: "",
techPhone: "",
});
const frozen = mapWorkOrderTableRow({
id: 202,
vendorId: 10,
vendorName: "Gateway Plumbing",
techName: "Jordan Lee",
techPhone: "555-0100",
frozenCompany: {
name: "Gateway Plumbing",
contactName: "Jordan Lee",
phone: "555-0100",
technicianAssigned: false,
},
});
expect(frozen.tech).toBe("");
expect(frozen.techPhone).toBe("");
expect(frozen.frozenCompany).toMatchObject({
contactName: "Jordan Lee",
technicianAssigned: false,
});
});
it("preserves legacy technician inference when the flag is missing or null", () => {
for (const technicianAssigned of [undefined, null]) {
const row = mapWorkOrderTableRow({
id: 203,
vendorId: 10,
techName: "Jordan Lee",
...(technicianAssigned !== undefined ? { technicianAssigned } : {}),
});
expect(row.tech).toBe("Jordan Lee");
expect(row.technicianAssigned).toBe(technicianAssigned);
}
});
it("maps frozen POC contact notes when the payload supplies them", () => {
const row = mapWorkOrderTableRow({
id: 192,

View file

@ -59,4 +59,22 @@ describe("resolveCompDocSite", () => {
expect(result.street).toBe("900 Live St");
expect(result.label).toBe("Milwaukee, WI");
});
it("does not reuse the frozen company contact as a technician when assignment is false", () => {
const result = resolveCompDocSite({
site: "BK5",
location: "Chicago, IL",
status: "Completed",
company: "Live Co",
tech: "Live Tech",
frozenCompany: {
name: "Frozen Co",
contactName: "Company Contact",
technicianAssigned: false,
},
});
expect(result.company).toBe("Frozen Co");
expect(result.tech).toBe("");
});
});