mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 03:23:12 +00:00
Compare commits
11 commits
24cbd09a60
...
29d235e133
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29d235e133 | ||
|
|
1746ded434 | ||
|
|
64c53e9da0 | ||
|
|
316cafccaa | ||
|
|
55b5441d71 | ||
|
|
262c93b41c | ||
|
|
fdbb602e0c | ||
|
|
5b4da11631 | ||
|
|
1c9b909861 | ||
|
|
bfdc2dbdec | ||
|
|
5adde3723b |
40 changed files with 1301 additions and 103 deletions
|
|
@ -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,85 @@ 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 (/^\/api\/workorders\/\d+\/detail$/u.test(pathname) && method === "GET") {
|
||||
const id = Number(pathname.split("/")[3]);
|
||||
const detailRow = currentBoardRows.find((row) => row.id === id);
|
||||
if (detailRow == null) return fulfillJson(route, { message: "Missing fixture row" }, 404);
|
||||
return fulfillJson(route, {
|
||||
info: detailRow,
|
||||
completion: { docStatus: 1 },
|
||||
comments: [],
|
||||
audit: [],
|
||||
media: [],
|
||||
});
|
||||
}
|
||||
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 (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",
|
||||
techPhone: "(314) 555-0141",
|
||||
}
|
||||
: {}),
|
||||
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 +203,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 +293,262 @@ 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,
|
||||
}) => {
|
||||
const writes: Captured[] = [];
|
||||
await mockApi(page, writes, [{ ...BOARD_ROW, lifecycleStatusLabel: "In Progress" }]);
|
||||
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: "Vinewood LLC" }).click();
|
||||
await page.getByRole("button", { name: "Coastal HVAC" }).last().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 expect(page.locator('input[type="tel"]')).toHaveCount(0);
|
||||
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.getByText("Completed", { exact: true })).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,
|
||||
lifecycleStatusLabel: "In Progress",
|
||||
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 page.getByRole("button", { name: "Adam Whyte" }).last().click();
|
||||
await expect(page.locator('input[type="tel"]')).toBeVisible();
|
||||
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();
|
||||
const reopenedDialog = page.getByRole("dialog");
|
||||
await expect(reopenedDialog.getByRole("button", { name: "Adam Whyte" })).toBeVisible();
|
||||
await expect(reopenedDialog.locator('input[type="tel"]')).toHaveValue("(314) 555-0141");
|
||||
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.getByText("Completed", { exact: true })).toBeVisible();
|
||||
expect(
|
||||
writes.some(
|
||||
(write) =>
|
||||
write.pathname === "/api/workorders/1/board" &&
|
||||
(write.body as { field?: string; value?: string }).field === "lifecycleStatus",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("slide-over saves a same-company no-technician selection with its vendor anchor", async ({
|
||||
page,
|
||||
}) => {
|
||||
const writes: Captured[] = [];
|
||||
const companyOnly = {
|
||||
...BOARD_ROW,
|
||||
vendorId: 50,
|
||||
vendorName: "Coastal HVAC",
|
||||
technicianAssigned: false,
|
||||
techName: null,
|
||||
techPhone: null,
|
||||
};
|
||||
await mockApi(page, writes, [companyOnly]);
|
||||
await page.goto("/workorders");
|
||||
|
||||
const row = page.locator("#wo-row-1");
|
||||
await row.hover();
|
||||
await row.getByRole("button", { name: "View details" }).click();
|
||||
const details = page.getByRole("dialog", { name: "Work order details" });
|
||||
await expect(details).toBeVisible();
|
||||
await details.getByRole("button", { name: "Edit WO" }).click();
|
||||
|
||||
await details.getByRole("button", { name: /Coastal HVAC/ }).click();
|
||||
const assignment = page.getByRole("dialog").last();
|
||||
await assignment.getByRole("button", { name: "Coastal HVAC" }).click();
|
||||
await page.getByRole("button", { name: "Vinewood LLC" }).last().click();
|
||||
await assignment.getByRole("button", { name: /select technician/i }).click();
|
||||
await page.getByRole("button", { name: "No technician" }).last().click();
|
||||
await assignment.getByRole("button", { name: /^save$/i }).click();
|
||||
await details.getByRole("button", { name: "Save", exact: true }).click();
|
||||
|
||||
const slideOverPatch = writes.find(
|
||||
(write) =>
|
||||
write.pathname === "/api/workorders/1/board" &&
|
||||
(write.body as { field?: string }).field === "vendorId",
|
||||
);
|
||||
expect(slideOverPatch?.body).toEqual(
|
||||
expect.objectContaining({ field: "vendorId", value: "41", technicianAssigned: false }),
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
const reloadedRow = page.locator("#wo-row-1");
|
||||
await reloadedRow.hover();
|
||||
await reloadedRow.getByRole("button", { name: "View details" }).click();
|
||||
const reloadedDetails = page.getByRole("dialog", { name: "Work order details" });
|
||||
await expect(reloadedDetails).toBeVisible();
|
||||
await reloadedDetails.getByRole("button", { name: "Edit WO" }).click();
|
||||
await reloadedDetails.getByRole("button", { name: /Vinewood LLC/ }).click();
|
||||
await expect(
|
||||
page.getByRole("dialog").last().getByRole("button", { name: "No technician" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("cancelling the wizard after staging a technician writes no vendor (SH-366)", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,8 @@ test.describe("wizard vendor company notes (SH-321)", () => {
|
|||
.first()
|
||||
.click();
|
||||
|
||||
await page.getByRole("button", { name: /Select technician/ }).click();
|
||||
// Picking a company assigns it with no technician until one is chosen.
|
||||
await page.getByRole("button", { name: "No technician" }).click();
|
||||
await page
|
||||
.getByRole("button", { name: /Adam Whyte/ })
|
||||
.last()
|
||||
|
|
|
|||
|
|
@ -31,6 +31,14 @@ type UseCompDocDialogArgs = {
|
|||
onDateCompletedChange?: (date: string) => void | Promise<void>;
|
||||
};
|
||||
|
||||
function completionDocTechnician(
|
||||
assigned: boolean | null | undefined,
|
||||
frozenAssigned: boolean | null | undefined,
|
||||
contactName: string | undefined,
|
||||
): string {
|
||||
return assigned === false || frozenAssigned === false ? "" : contactName || "";
|
||||
}
|
||||
|
||||
function notifyDraftSaved() {
|
||||
toast(
|
||||
<div
|
||||
|
|
@ -116,7 +124,11 @@ export function useCompDocDialog({
|
|||
amazonRep,
|
||||
seaRep,
|
||||
siteCode: site.code,
|
||||
tech: company.contactName || "",
|
||||
tech: completionDocTechnician(
|
||||
info.technicianAssigned,
|
||||
company.technicianAssigned,
|
||||
company.contactName,
|
||||
),
|
||||
company: company.name || "",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -213,9 +213,11 @@ export function SlideOverInfoTabEditView({
|
|||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Vendor">
|
||||
<VendorCell
|
||||
vendorId={draft.vendorId}
|
||||
company={draft.company}
|
||||
tech={draft.tech}
|
||||
techPhone={draft.techPhone}
|
||||
technicianAssigned={draft.technicianAssigned}
|
||||
vendors={vendors}
|
||||
onSave={(patch) => onDraftChange(toVendorTablePatch(patch))}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||
import type { VendorFormValues } from "@/domain/vendors/schemas/vendor-schema";
|
||||
import { useAddTechniciansToCompany } from "@/domain/vendors/use-cases/use-add-technicians-to-company";
|
||||
|
|
@ -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,10 +26,74 @@ type UseVendorDialogFormArgs = {
|
|||
company: string;
|
||||
tech: string;
|
||||
techPhone: string;
|
||||
technicianAssigned?: boolean | null;
|
||||
vendors: VendorDropdownItem[];
|
||||
onSave: (patch: VendorPatch) => void;
|
||||
};
|
||||
|
||||
type TechnicianKeyArgs = Pick<
|
||||
UseVendorDialogFormArgs,
|
||||
"technicianAssigned" | "vendorId" | "tech" | "company" | "vendors"
|
||||
>;
|
||||
|
||||
function resolveInitialTechnicianKey(args: TechnicianKeyArgs): string {
|
||||
if (args.technicianAssigned === false) return NO_TECHNICIAN_KEY;
|
||||
return resolveTechnicianKey(args.vendorId, args.tech, args.company, args.vendors);
|
||||
}
|
||||
|
||||
function buildSubmitBaseline(args: TechnicianKeyArgs & { techPhone: string }) {
|
||||
const options = buildTechnicianOptions(args.vendors, args.company, args.tech, args.company);
|
||||
return {
|
||||
company: args.company,
|
||||
selectedKey: resolveInitialTechnicianKey(args),
|
||||
phone: args.techPhone || phoneForOption(args.tech, options),
|
||||
notes: "",
|
||||
pendingCreateCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function createCompanyPickHandler(args: {
|
||||
selectedKey: string;
|
||||
technicianOptions: ReturnType<typeof buildTechnicianOptions>;
|
||||
setSelectedCompany: (company: string) => void;
|
||||
setAddedVendors: Dispatch<SetStateAction<VendorDropdownItem[]>>;
|
||||
setPendingCreates: Dispatch<SetStateAction<Record<string, VendorFormValues>>>;
|
||||
setSelectedKey: (key: string) => void;
|
||||
setT: (value: string) => void;
|
||||
setPhone: (value: string) => void;
|
||||
}) {
|
||||
return (nextCompany: string) => {
|
||||
const selectedOption = args.technicianOptions.find((option) => option.key === args.selectedKey);
|
||||
args.setSelectedCompany(nextCompany);
|
||||
if (selectedOption?.company === nextCompany) return;
|
||||
args.setAddedVendors([]);
|
||||
args.setPendingCreates({});
|
||||
args.setSelectedKey("");
|
||||
args.setT("");
|
||||
args.setPhone("");
|
||||
};
|
||||
}
|
||||
|
||||
function createTechnicianPickHandler(args: {
|
||||
technicianOptions: ReturnType<typeof buildTechnicianOptions>;
|
||||
setSelectedKey: (key: string) => void;
|
||||
setT: (value: string) => void;
|
||||
setPhone: (value: string) => void;
|
||||
}) {
|
||||
return (key: string) => {
|
||||
if (key === NO_TECHNICIAN_KEY) {
|
||||
args.setSelectedKey(key);
|
||||
args.setT("");
|
||||
args.setPhone("");
|
||||
return;
|
||||
}
|
||||
const option = args.technicianOptions.find((item) => item.key === key);
|
||||
args.setSelectedKey(key);
|
||||
args.setT(option?.name ?? key);
|
||||
args.setPhone(option?.phone || phoneForOption(option?.name ?? key, args.technicianOptions));
|
||||
};
|
||||
}
|
||||
|
||||
export function useVendorDialogForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
|
|
@ -34,13 +101,18 @@ export function useVendorDialogForm({
|
|||
company,
|
||||
tech,
|
||||
techPhone,
|
||||
technicianAssigned,
|
||||
vendors,
|
||||
onSave,
|
||||
}: UseVendorDialogFormArgs) {
|
||||
const addTechnicians = useAddTechniciansToCompany();
|
||||
const [selectedCompany, setSelectedCompany] = useState(company);
|
||||
const technicianKeyArgs = useMemo(
|
||||
() => ({ technicianAssigned, vendorId, tech, company, vendors }),
|
||||
[technicianAssigned, vendorId, tech, company, vendors],
|
||||
);
|
||||
const [selectedKey, setSelectedKey] = useState(() =>
|
||||
resolveTechnicianKey(vendorId, tech, company, vendors),
|
||||
resolveInitialTechnicianKey(technicianKeyArgs),
|
||||
);
|
||||
const [t, setT] = useState(tech);
|
||||
const [phone, setPhone] = useState(techPhone);
|
||||
|
|
@ -66,7 +138,7 @@ export function useVendorDialogForm({
|
|||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const key = resolveTechnicianKey(vendorId, tech, company, vendors);
|
||||
const key = resolveInitialTechnicianKey(technicianKeyArgs);
|
||||
const initialOptions = buildTechnicianOptions(vendors, company, tech, company);
|
||||
setSelectedCompany(company);
|
||||
setSelectedKey(key);
|
||||
|
|
@ -78,7 +150,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, technicianKeyArgs]);
|
||||
|
||||
const selected = technicianOptions.find((option) => option.key === selectedKey);
|
||||
const resolvedCompany = selectedCompany;
|
||||
|
|
@ -100,20 +172,15 @@ export function useVendorDialogForm({
|
|||
notes,
|
||||
pendingCreateCount: Object.keys(pendingCreates).length,
|
||||
},
|
||||
baseline: {
|
||||
company,
|
||||
selectedKey: resolveTechnicianKey(vendorId, tech, company, vendors),
|
||||
phone:
|
||||
techPhone || phoneForOption(tech, buildTechnicianOptions(vendors, company, tech, company)),
|
||||
notes: "",
|
||||
pendingCreateCount: 0,
|
||||
},
|
||||
baseline: buildSubmitBaseline({ ...technicianKeyArgs, techPhone }),
|
||||
});
|
||||
|
||||
const { handleSave } = useVendorDialogSave({
|
||||
allVendors,
|
||||
pendingCreates,
|
||||
selectedKey,
|
||||
vendorId,
|
||||
originalCompany: company,
|
||||
selectedVendor,
|
||||
resolvedCompany,
|
||||
anchorCompanyName,
|
||||
|
|
@ -126,6 +193,23 @@ export function useVendorDialogForm({
|
|||
canSubmit,
|
||||
});
|
||||
|
||||
const handleCompanyPick = createCompanyPickHandler({
|
||||
selectedKey,
|
||||
technicianOptions,
|
||||
setSelectedCompany,
|
||||
setAddedVendors,
|
||||
setPendingCreates,
|
||||
setSelectedKey,
|
||||
setT,
|
||||
setPhone,
|
||||
});
|
||||
const handlePick = createTechnicianPickHandler({
|
||||
technicianOptions,
|
||||
setSelectedKey,
|
||||
setT,
|
||||
setPhone,
|
||||
});
|
||||
|
||||
return {
|
||||
companyOptions,
|
||||
technicianOptions,
|
||||
|
|
@ -143,23 +227,8 @@ export function useVendorDialogForm({
|
|||
canSubmit,
|
||||
isPending: addTechnicians.isPending,
|
||||
handleSave,
|
||||
handleCompanyPick: (nextCompany: string) => {
|
||||
const selectedOption = technicianOptions.find((option) => option.key === selectedKey);
|
||||
setSelectedCompany(nextCompany);
|
||||
if (selectedOption?.company !== nextCompany) {
|
||||
setAddedVendors([]);
|
||||
setPendingCreates({});
|
||||
setSelectedKey("");
|
||||
setT("");
|
||||
setPhone("");
|
||||
}
|
||||
},
|
||||
handlePick: (key: string) => {
|
||||
const option = technicianOptions.find((o) => o.key === key);
|
||||
setSelectedKey(key);
|
||||
setT(option?.name ?? key);
|
||||
setPhone(option?.phone || phoneForOption(option?.name ?? key, technicianOptions));
|
||||
},
|
||||
handleCompanyPick,
|
||||
handlePick,
|
||||
handleTechnicianCreated: (
|
||||
created: VendorDropdownItem,
|
||||
createdPhone: string,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -20,6 +22,138 @@ type UseVendorDialogSaveArgs = {
|
|||
canSubmit: boolean;
|
||||
};
|
||||
|
||||
type VendorResolutionArgs = Pick<
|
||||
UseVendorDialogSaveArgs,
|
||||
"allVendors" | "selectedKey" | "selectedVendor" | "resolvedCompany" | "anchorCompanyName" | "tech"
|
||||
>;
|
||||
|
||||
function resolveSaveVendor(args: VendorResolutionArgs): VendorDropdownItem | undefined {
|
||||
return (
|
||||
(args.selectedKey
|
||||
? args.allVendors.find((vendor) => String(vendor.id) === args.selectedKey)
|
||||
: undefined) ??
|
||||
args.allVendors.find(
|
||||
(vendor) =>
|
||||
vendor.contactName === args.tech &&
|
||||
vendor.companyName === (args.resolvedCompany || args.anchorCompanyName),
|
||||
) ??
|
||||
args.selectedVendor
|
||||
);
|
||||
}
|
||||
|
||||
async function resolvePersistedVendor(args: {
|
||||
vendor: VendorDropdownItem | undefined;
|
||||
allVendors: VendorDropdownItem[];
|
||||
pendingCreates: Record<string, VendorFormValues>;
|
||||
phone: string;
|
||||
addTechnicians: ReturnType<typeof useAddTechniciansToCompany>;
|
||||
setSaveError: (error: string | null) => void;
|
||||
}): Promise<VendorDropdownItem | undefined | null> {
|
||||
const payload = args.vendor ? args.pendingCreates[String(args.vendor.id)] : undefined;
|
||||
if (!args.vendor || !isDraftVendorId(args.vendor.id) || !payload) return args.vendor;
|
||||
try {
|
||||
return await persistDraftTechnician({
|
||||
allVendors: args.allVendors,
|
||||
pendingPayload: payload,
|
||||
phone: args.phone,
|
||||
addTechnicians: args.addTechnicians,
|
||||
});
|
||||
} catch (error) {
|
||||
args.setSaveError(error instanceof Error ? error.message : "Failed to create technician");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVendorForSave(args: {
|
||||
vendor: VendorDropdownItem | undefined;
|
||||
allVendors: VendorDropdownItem[];
|
||||
pendingCreates: Record<string, VendorFormValues>;
|
||||
phone: string;
|
||||
addTechnicians: ReturnType<typeof useAddTechniciansToCompany>;
|
||||
setSaveError: (error: string | null) => void;
|
||||
}): VendorDropdownItem | undefined | Promise<VendorDropdownItem | undefined | null> {
|
||||
const payload = args.vendor ? args.pendingCreates[String(args.vendor.id)] : undefined;
|
||||
if (!args.vendor || !isDraftVendorId(args.vendor.id) || !payload) return args.vendor;
|
||||
return resolvePersistedVendor(args);
|
||||
}
|
||||
|
||||
function isNoTechnician(key: string): boolean {
|
||||
return key === "" || key === "__no_technician__";
|
||||
}
|
||||
|
||||
function resolveAssignmentVendorId(args: {
|
||||
noTechnician: boolean;
|
||||
saveVendor: VendorDropdownItem | undefined;
|
||||
vendorId: string | number;
|
||||
resolvedCompany: string;
|
||||
originalCompany: string;
|
||||
}): string {
|
||||
if (!args.noTechnician && args.saveVendor) return String(args.saveVendor.id);
|
||||
if (
|
||||
args.noTechnician &&
|
||||
args.resolvedCompany === args.originalCompany &&
|
||||
args.vendorId !== "" &&
|
||||
args.vendorId != null
|
||||
) {
|
||||
return String(args.vendorId);
|
||||
}
|
||||
return args.noTechnician && args.saveVendor ? String(args.saveVendor.id) : "";
|
||||
}
|
||||
|
||||
function buildVendorPatch(args: {
|
||||
selectedKey: string;
|
||||
saveVendor: VendorDropdownItem | undefined;
|
||||
resolvedCompany: string;
|
||||
anchorCompanyName: string;
|
||||
tech: string;
|
||||
phone: string;
|
||||
vendorId: string | number;
|
||||
originalCompany: string;
|
||||
}): VendorPatch {
|
||||
const assigned = args.selectedKey !== "" && !isNoTechnician(args.selectedKey);
|
||||
const noTechnician = isNoTechnician(args.selectedKey);
|
||||
return {
|
||||
vendorId: resolveAssignmentVendorId({
|
||||
noTechnician,
|
||||
saveVendor: args.saveVendor,
|
||||
vendorId: args.vendorId,
|
||||
resolvedCompany: args.resolvedCompany,
|
||||
originalCompany: args.originalCompany,
|
||||
}),
|
||||
company: args.resolvedCompany || args.anchorCompanyName,
|
||||
tech: assigned ? (args.saveVendor?.contactName ?? args.tech) : "",
|
||||
techPhone: assigned ? args.phone : "",
|
||||
technicianAssigned: assigned,
|
||||
};
|
||||
}
|
||||
|
||||
function commitVendorPatch(args: {
|
||||
saveVendor: VendorDropdownItem | undefined;
|
||||
selectedKey: string;
|
||||
resolvedCompany: string;
|
||||
anchorCompanyName: string;
|
||||
tech: string;
|
||||
phone: string;
|
||||
vendorId: string | number;
|
||||
originalCompany: string;
|
||||
onSave: (patch: VendorPatch) => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}): void {
|
||||
args.onSave(
|
||||
buildVendorPatch({
|
||||
saveVendor: args.saveVendor,
|
||||
selectedKey: args.selectedKey,
|
||||
resolvedCompany: args.resolvedCompany,
|
||||
anchorCompanyName: args.anchorCompanyName,
|
||||
tech: args.tech,
|
||||
phone: args.phone,
|
||||
vendorId: args.vendorId,
|
||||
originalCompany: args.originalCompany,
|
||||
}),
|
||||
);
|
||||
args.onOpenChange(false);
|
||||
}
|
||||
|
||||
/** Persists a staged draft technician under its existing company (additive roster PATCH). */
|
||||
async function persistDraftTechnician({
|
||||
allVendors,
|
||||
|
|
@ -56,6 +190,8 @@ export function useVendorDialogSave({
|
|||
allVendors,
|
||||
pendingCreates,
|
||||
selectedKey,
|
||||
vendorId,
|
||||
originalCompany,
|
||||
selectedVendor,
|
||||
resolvedCompany,
|
||||
anchorCompanyName,
|
||||
|
|
@ -67,43 +203,54 @@ export function useVendorDialogSave({
|
|||
setSaveError,
|
||||
canSubmit,
|
||||
}: UseVendorDialogSaveArgs) {
|
||||
const resolveSaveVendor = () =>
|
||||
allVendors.find((vendor) => String(vendor.id) === selectedKey) ??
|
||||
allVendors.find(
|
||||
(vendor) =>
|
||||
vendor.contactName === tech &&
|
||||
vendor.companyName === (resolvedCompany || anchorCompanyName),
|
||||
) ??
|
||||
selectedVendor;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!canSubmit) return;
|
||||
setSaveError(null);
|
||||
let saveVendor = resolveSaveVendor();
|
||||
const pendingPayload = saveVendor ? pendingCreates[String(saveVendor.id)] : undefined;
|
||||
|
||||
if (saveVendor != null && isDraftVendorId(saveVendor.id) && pendingPayload) {
|
||||
try {
|
||||
saveVendor = await persistDraftTechnician({
|
||||
allVendors,
|
||||
pendingPayload,
|
||||
phone,
|
||||
addTechnicians,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to create technician";
|
||||
setSaveError(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSave({
|
||||
vendorId: saveVendor ? String(saveVendor.id) : "",
|
||||
company: resolvedCompany || anchorCompanyName,
|
||||
tech: selectedKey ? (saveVendor?.contactName ?? tech) : "",
|
||||
techPhone: selectedKey ? phone : "",
|
||||
const vendor = resolveSaveVendor({
|
||||
allVendors,
|
||||
selectedKey,
|
||||
selectedVendor,
|
||||
resolvedCompany,
|
||||
anchorCompanyName,
|
||||
tech,
|
||||
});
|
||||
const resolvedVendor = resolveVendorForSave({
|
||||
vendor,
|
||||
allVendors,
|
||||
pendingCreates,
|
||||
phone,
|
||||
addTechnicians,
|
||||
setSaveError,
|
||||
});
|
||||
if (resolvedVendor instanceof Promise) {
|
||||
const saveVendor = await resolvedVendor;
|
||||
if (saveVendor === null) return;
|
||||
commitVendorPatch({
|
||||
saveVendor,
|
||||
selectedKey,
|
||||
resolvedCompany,
|
||||
anchorCompanyName,
|
||||
tech,
|
||||
phone,
|
||||
vendorId,
|
||||
originalCompany,
|
||||
onSave,
|
||||
onOpenChange,
|
||||
});
|
||||
return;
|
||||
}
|
||||
commitVendorPatch({
|
||||
saveVendor: resolvedVendor,
|
||||
selectedKey,
|
||||
resolvedCompany,
|
||||
anchorCompanyName,
|
||||
tech,
|
||||
phone,
|
||||
vendorId,
|
||||
originalCompany,
|
||||
onSave,
|
||||
onOpenChange,
|
||||
});
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return { handleSave };
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ export type VendorPatch = {
|
|||
company: string;
|
||||
tech: string;
|
||||
techPhone: string;
|
||||
technicianAssigned?: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
|||
import { VendorDialogBody } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-body";
|
||||
import { VendorDialogFooter } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-footer";
|
||||
import { useVendorDialogForm } from "@/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-form";
|
||||
import { NO_TECHNICIAN_KEY } from "@/app/(protected)/workorders/_components/list/table/cells/resolve-technician-key";
|
||||
import type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
|
||||
import type { WorkOrderFrozenCompany } from "@/domain/work-orders/types/work-order-table-row";
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ type VendorDialogProps = {
|
|||
company: string;
|
||||
tech: string;
|
||||
techPhone?: string;
|
||||
technicianAssigned?: boolean | null;
|
||||
vendors: VendorDropdownItem[];
|
||||
viewOnly?: boolean;
|
||||
frozenCompany?: WorkOrderFrozenCompany;
|
||||
|
|
@ -38,6 +40,7 @@ export function VendorDialog({
|
|||
company,
|
||||
tech,
|
||||
techPhone = "",
|
||||
technicianAssigned,
|
||||
vendors,
|
||||
viewOnly = false,
|
||||
frozenCompany,
|
||||
|
|
@ -50,9 +53,24 @@ export function VendorDialog({
|
|||
company,
|
||||
tech,
|
||||
techPhone,
|
||||
technicianAssigned,
|
||||
vendors,
|
||||
onSave,
|
||||
});
|
||||
const hasDraftTechnician = form.selectedKey !== "" && form.selectedKey !== NO_TECHNICIAN_KEY;
|
||||
const draftTechnician = hasDraftTechnician ? (form.selected?.name ?? form.t) : "";
|
||||
const displayTechnician = frozenCompany
|
||||
? frozenCompany.technicianAssigned === false
|
||||
? ""
|
||||
: (frozenCompany.contactName ?? draftTechnician)
|
||||
: draftTechnician;
|
||||
const displayTechnicianPhone = frozenCompany
|
||||
? frozenCompany.technicianAssigned === false
|
||||
? ""
|
||||
: (frozenCompany.phone ?? form.phone)
|
||||
: hasDraftTechnician
|
||||
? form.phone
|
||||
: "";
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
|
@ -94,12 +112,13 @@ 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={displayTechnician}
|
||||
phone={displayTechnicianPhone}
|
||||
notes={form.notes}
|
||||
resolvedCompany={form.resolvedCompany}
|
||||
selectedVendor={form.selectedVendor}
|
||||
frozenCompany={frozenCompany}
|
||||
technicianAssigned={technicianAssigned}
|
||||
saveError={form.saveError}
|
||||
onCompanyPick={form.handleCompanyPick}
|
||||
onPick={form.handlePick}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ export function WoTableInlineRowCells({
|
|||
company={draft.company}
|
||||
tech={draft.tech}
|
||||
techPhone={draft.techPhone}
|
||||
technicianAssigned={draft.technicianAssigned}
|
||||
vendors={vendors}
|
||||
onSave={(p) => onPatch(toVendorTablePatch(p))}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,15 @@ 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.technicianSelectionCleared !== true &&
|
||||
(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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
@ -66,6 +76,7 @@ export function WizardVendorCompanyFields({
|
|||
techPhone: "",
|
||||
vendorNotes: "",
|
||||
newTechnicians: [],
|
||||
technicianSelectionCleared: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
|
|
@ -77,20 +88,23 @@ 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,
|
||||
technicianSelectionCleared: false,
|
||||
techPhone: "",
|
||||
vendorNotes: "",
|
||||
newTechnicians: [],
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -98,15 +112,27 @@ 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,
|
||||
technicianSelectionCleared: true,
|
||||
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 +143,25 @@ 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,
|
||||
technicianSelectionCleared: true,
|
||||
techPhone: "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const vendor = vendors.find((v) => String(v.id) === id);
|
||||
onPatch({
|
||||
vendorCompany: vendor?.companyName ?? company,
|
||||
vendorId: id,
|
||||
vendorName: vendor?.companyName ?? "",
|
||||
technicianAssigned: true,
|
||||
technicianSelectionCleared: false,
|
||||
techPhone: "",
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -72,6 +72,38 @@ 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 resolveTechnicianAssignment(
|
||||
rowAssigned: boolean | null | undefined,
|
||||
frozenAssigned: boolean | null | undefined,
|
||||
): boolean | null | undefined {
|
||||
if (rowAssigned === false || frozenAssigned === false) return false;
|
||||
if (rowAssigned !== undefined) return rowAssigned;
|
||||
return frozenAssigned;
|
||||
}
|
||||
|
||||
function readEffectiveTechnicianFields(
|
||||
item: Record<string, unknown>,
|
||||
assigned: boolean | null | undefined,
|
||||
): { tech: string; techPhone: string } {
|
||||
if (assigned === false) return { tech: "", techPhone: "" };
|
||||
return {
|
||||
tech: readString(item, "techName", "TechName", "tech", "Tech", "technician", "Technician"),
|
||||
techPhone: readString(item, "techPhone", "TechPhone"),
|
||||
};
|
||||
}
|
||||
|
||||
function readStringArray(record: Record<string, unknown>, ...keys: string[]): string[] {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
|
|
@ -293,9 +325,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 +414,13 @@ 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 = resolveTechnicianAssignment(
|
||||
rowTechnicianAssigned,
|
||||
frozenCompany?.technicianAssigned,
|
||||
);
|
||||
const technician = readEffectiveTechnicianFields(item, technicianAssigned);
|
||||
|
||||
const row: WorkOrderTableRow = {
|
||||
id: readId(item),
|
||||
|
|
@ -406,8 +447,9 @@ 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: technician.tech,
|
||||
techPhone: technician.techPhone,
|
||||
...(technicianAssigned !== undefined ? { technicianAssigned } : {}),
|
||||
apptStart,
|
||||
apptEnd,
|
||||
apptTime,
|
||||
|
|
@ -418,7 +460,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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface WorkOrderAuditEntry {
|
|||
}
|
||||
|
||||
export interface WorkOrderDetail {
|
||||
technicianAssigned?: boolean | null;
|
||||
id: string | number;
|
||||
internalWONumber: string;
|
||||
workOrderNumber: string;
|
||||
|
|
|
|||
|
|
@ -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 | "";
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ 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;
|
||||
/** 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;
|
||||
|
|
|
|||
|
|
@ -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 ?? "",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export const SLIDE_OVER_EDIT_KEYS = [
|
|||
"avetaRequired",
|
||||
"additionalContacts",
|
||||
"vendorId",
|
||||
"technicianAssigned",
|
||||
// company/tech are display companions; board persists vendor via vendorId only
|
||||
"status",
|
||||
"pocName",
|
||||
|
|
@ -91,6 +92,7 @@ export function buildSlideOverPatch(
|
|||
const patch: WorkOrderTablePatch = {};
|
||||
|
||||
for (const key of SLIDE_OVER_EDIT_KEYS) {
|
||||
if (key === "technicianAssigned") continue;
|
||||
if (key === "extraServices") {
|
||||
if (!sameExtraServices(draft.extraServices, baseline.extraServices)) {
|
||||
patch.extraServices = draft.extraServices;
|
||||
|
|
@ -108,5 +110,16 @@ export function buildSlideOverPatch(
|
|||
}
|
||||
}
|
||||
|
||||
const assignmentChanged = draft.technicianAssigned !== baseline.technicianAssigned;
|
||||
const vendorChanged = draft.vendorId !== baseline.vendorId;
|
||||
if (
|
||||
typeof draft.technicianAssigned === "boolean" &&
|
||||
(assignmentChanged || vendorChanged) &&
|
||||
String(draft.vendorId).trim() !== ""
|
||||
) {
|
||||
patch.vendorId = draft.vendorId;
|
||||
patch.technicianAssigned = draft.technicianAssigned;
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ describe("VendorDialog save gate", () => {
|
|||
vendorId: "11",
|
||||
tech: "Casey New",
|
||||
company: "Gateway Plumbing",
|
||||
technicianAssigned: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -122,10 +123,69 @@ 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("shows the editable phone for a newly selected technician and hides it for No technician", async () => {
|
||||
renderWithProviders(
|
||||
<VendorDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
vendorId={10}
|
||||
company="Gateway Plumbing"
|
||||
tech=""
|
||||
techPhone=""
|
||||
technicianAssigned={false}
|
||||
vendors={[jordan, casey]}
|
||||
onSave={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(document.querySelector('input[type="tel"]')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "No technician" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: /jordan lee/i }));
|
||||
|
||||
const phone = document.querySelector<HTMLInputElement>('input[type="tel"]');
|
||||
expect(phone).toBeVisible();
|
||||
fireEvent.change(phone!, { target: { value: "(314) 555-0141" } });
|
||||
expect(phone).toHaveValue("(314) 555-0141");
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: /jordan lee/i }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "No technician" }));
|
||||
expect(document.querySelector('input[type="tel"]')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("only offers technicians from the selected company", async () => {
|
||||
renderWithProviders(
|
||||
<VendorDialog
|
||||
|
|
|
|||
|
|
@ -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,132 @@ describe("WizardStepVendorTime inline technician (SH-364)", () => {
|
|||
|
||||
expect(onPatch).toHaveBeenCalledWith({
|
||||
vendorCompany: "Gateway Plumbing",
|
||||
vendorId: "",
|
||||
vendorName: "",
|
||||
vendorId: "52",
|
||||
vendorName: "Gateway Plumbing",
|
||||
technicianAssigned: false,
|
||||
technicianSelectionCleared: 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,
|
||||
technicianSelectionCleared: 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,
|
||||
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 = {
|
||||
...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,
|
||||
technicianSelectionCleared: true,
|
||||
techPhone: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the picked vendor and staged technicians when the same company is re-picked", () => {
|
||||
const onPatch = vi.fn();
|
||||
render(
|
||||
|
|
@ -192,11 +313,72 @@ 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("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(() =>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,50 @@ function currentMonthIso(day: number): string {
|
|||
}
|
||||
|
||||
describe("WoTableInlineRowCells — Schedule On range", () => {
|
||||
it("reopens a company-only inline assignment without inferring its anchor as technician", async () => {
|
||||
renderWithProviders(
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<WoTableInlineRowCells
|
||||
draft={{
|
||||
...EMPTY_TABLE_DRAFT,
|
||||
vendorId: "41",
|
||||
company: "Vinewood LLC",
|
||||
technicianAssigned: false,
|
||||
}}
|
||||
weekDefault={false}
|
||||
users={[]}
|
||||
sites={[]}
|
||||
vendors={[
|
||||
{
|
||||
id: 41,
|
||||
companyName: "Vinewood LLC",
|
||||
contactName: "Adam Whyte",
|
||||
tradeSpecialties: "HVAC",
|
||||
address: "1 Market St",
|
||||
distanceMiles: null,
|
||||
},
|
||||
]}
|
||||
pmTypes={[]}
|
||||
allRows={[]}
|
||||
siteCellRef={createRef()}
|
||||
woCellRef={createRef()}
|
||||
siteShowError={false}
|
||||
siteError=""
|
||||
woShowError={false}
|
||||
onPatch={vi.fn()}
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Vinewood LLC/ }));
|
||||
expect(await screen.findByRole("button", { name: "No technician" })).toBeVisible();
|
||||
expect(screen.queryByRole("button", { name: "Adam Whyte" })).not.toBeInTheDocument();
|
||||
}, 10_000);
|
||||
|
||||
it("patches schedMode range with start and end", async () => {
|
||||
const onPatch = vi.fn();
|
||||
renderWithProviders(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -172,6 +186,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
|
|||
scheduleWeekOnly: false,
|
||||
|
||||
vendorId: "5",
|
||||
technicianAssigned: false,
|
||||
|
||||
apptStart: "09:00",
|
||||
|
||||
|
|
@ -214,6 +229,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
|
|||
isAddOn: true,
|
||||
|
||||
vendorId: 5,
|
||||
technicianAssigned: false,
|
||||
|
||||
apptDate: "2026-06-25",
|
||||
|
||||
|
|
@ -336,6 +352,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,
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ describe("slide-over-draft", () => {
|
|||
expect(SLIDE_OVER_EDIT_KEYS).toContain("woNumber");
|
||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("status");
|
||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("vendorId");
|
||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("technicianAssigned");
|
||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("additionalContacts");
|
||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("avetaRequired");
|
||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocName");
|
||||
|
|
@ -87,6 +88,37 @@ describe("slide-over-draft", () => {
|
|||
expect(buildSlideOverPatch(vendorDraft, row)).toEqual({ vendorId: "v2" });
|
||||
});
|
||||
|
||||
it("sends a same-vendor assignment toggle in the vendorId envelope", () => {
|
||||
const row = baseRow({ technicianAssigned: true });
|
||||
const draft = baseRow({ technicianAssigned: false });
|
||||
|
||||
expect(isSlideOverDraftDirty(draft, row)).toBe(true);
|
||||
expect(buildSlideOverPatch(draft, row)).toEqual({
|
||||
vendorId: row.vendorId,
|
||||
technicianAssigned: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("includes assignment state when the slide-over changes company", () => {
|
||||
const row = baseRow({ technicianAssigned: false });
|
||||
const draft = baseRow({ vendorId: "v2", company: "Other Co", technicianAssigned: false });
|
||||
|
||||
expect(isSlideOverDraftDirty(draft, row)).toBe(true);
|
||||
expect(buildSlideOverPatch(draft, row)).toEqual({
|
||||
vendorId: "v2",
|
||||
technicianAssigned: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps an unchanged legacy assignment clean", () => {
|
||||
const legacy = baseRow();
|
||||
delete (legacy as Partial<WorkOrderTableRow>).technicianAssigned;
|
||||
const draft = { ...legacy };
|
||||
|
||||
expect(isSlideOverDraftDirty(draft, legacy)).toBe(false);
|
||||
expect(buildSlideOverPatch(draft, legacy)).toEqual({});
|
||||
});
|
||||
|
||||
it("tracks POC edits for dirty state and patch", () => {
|
||||
const row = baseRow();
|
||||
const draft = baseRow({ pocName: "Jordan", pocPhone: "555-0100" });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue