mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 09:13:11 +00:00
fix(work-orders): retain technician state across editors
This commit is contained in:
parent
5adde3723b
commit
bfdc2dbdec
8 changed files with 206 additions and 13 deletions
|
|
@ -121,6 +121,18 @@ async function mockApi(
|
|||
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") {
|
||||
return fulfillJson(route, { data: { id: 900, woNumber: "SH-900" } });
|
||||
}
|
||||
|
|
@ -147,6 +159,7 @@ async function mockApi(
|
|||
? {
|
||||
technicianAssigned: true,
|
||||
techName: vendorId === 50 ? "Avery Chen" : "Adam Whyte",
|
||||
techPhone: "(314) 555-0141",
|
||||
}
|
||||
: {}),
|
||||
rowVersion: "rv-2",
|
||||
|
|
@ -294,6 +307,7 @@ test.describe("Inline technician registration", () => {
|
|||
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 })
|
||||
|
|
@ -395,6 +409,7 @@ test.describe("Inline technician registration", () => {
|
|||
const dialog = page.getByRole("dialog");
|
||||
await dialog.getByRole("button", { name: "No technician" }).click();
|
||||
await dialog.getByRole("button", { name: "Adam Whyte" }).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");
|
||||
|
|
@ -406,9 +421,9 @@ test.describe("Inline technician registration", () => {
|
|||
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();
|
||||
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 })
|
||||
|
|
@ -432,6 +447,56 @@ test.describe("Inline technician registration", () => {
|
|||
).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 assignment.getByRole("button", { name: "Vinewood LLC" }).click();
|
||||
await assignment.getByRole("button", { name: /select technician/i }).click();
|
||||
await assignment.getByRole("button", { name: "No technician" }).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: /Vinewood LLC/ }).click();
|
||||
await expect(reloadedDetails.getByRole("button", { name: "No technician" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("cancelling the wizard after staging a technician writes no vendor (SH-366)", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ export function SlideOverInfoTabEditView({
|
|||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Vendor">
|
||||
<VendorCell
|
||||
vendorId={draft.vendorId}
|
||||
company={draft.company}
|
||||
tech={draft.tech}
|
||||
techPhone={draft.techPhone}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
@ -56,6 +57,21 @@ export function VendorDialog({
|
|||
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
|
||||
|
|
@ -97,16 +113,8 @@ export function VendorDialog({
|
|||
technicianOptions={form.technicianOptions}
|
||||
anchorCompanyName={form.anchorCompanyName}
|
||||
companyVendorForAdd={viewOnly ? undefined : (form.companyVendor ?? undefined)}
|
||||
tech={
|
||||
technicianAssigned === false || frozenCompany?.technicianAssigned === false
|
||||
? ""
|
||||
: (frozenCompany?.contactName ?? form.selected?.name ?? form.t)
|
||||
}
|
||||
phone={
|
||||
technicianAssigned === false || frozenCompany?.technicianAssigned === false
|
||||
? ""
|
||||
: (frozenCompany?.phone ?? form.phone)
|
||||
}
|
||||
tech={displayTechnician}
|
||||
phone={displayTechnicianPhone}
|
||||
notes={form.notes}
|
||||
resolvedCompany={form.resolvedCompany}
|
||||
selectedVendor={form.selectedVendor}
|
||||
|
|
|
|||
|
|
@ -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))}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,6 +157,35 @@ describe("VendorDialog save gate", () => {
|
|||
});
|
||||
});
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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