mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-05 07:12:06 +00:00
fix(work-orders): omit extraServices on create and persist slide-over POCs (SH-218)
This commit is contained in:
parent
1e88c3d039
commit
0d609124d1
6 changed files with 221 additions and 49 deletions
|
|
@ -59,18 +59,29 @@ export function SlideOverInfoTabEditView({
|
||||||
pocName={draft.pocName}
|
pocName={draft.pocName}
|
||||||
pocPhone={draft.pocPhone}
|
pocPhone={draft.pocPhone}
|
||||||
pocNotes={draft.pocNotes}
|
pocNotes={draft.pocNotes}
|
||||||
|
additionalContacts={draft.additionalContacts}
|
||||||
sites={sites}
|
sites={sites}
|
||||||
onSave={(patch) =>
|
onSave={(patch) =>
|
||||||
onDraftChange({
|
onDraftChange({
|
||||||
site: patch.site,
|
site: patch.site,
|
||||||
location: patch.location,
|
location: patch.location,
|
||||||
locationId: patch.locationId,
|
locationId: patch.locationId,
|
||||||
|
pocName: patch.pocName,
|
||||||
|
pocPhone: patch.pocPhone,
|
||||||
|
pocNotes: patch.pocNotes,
|
||||||
|
...(patch.additionalContacts !== undefined
|
||||||
|
? { additionalContacts: patch.additionalContacts }
|
||||||
|
: {}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Type of WO">
|
<SlideOverInfoRow label="Type of WO">
|
||||||
<TypeCell value={draft.type} onSave={(type) => onDraftChange({ type })} />
|
<TypeCell
|
||||||
|
value={draft.type}
|
||||||
|
severity={draft.severity}
|
||||||
|
onSave={(type, severity) => onDraftChange({ type, severity })}
|
||||||
|
/>
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Assigned To">
|
<SlideOverInfoRow label="Assigned To">
|
||||||
<AssignedToCell
|
<AssignedToCell
|
||||||
|
|
|
||||||
|
|
@ -8,42 +8,7 @@ function trimOrUndefined(value: string | undefined): string | undefined {
|
||||||
return trimmed ? trimmed : undefined;
|
return trimmed ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeExtraServices(
|
/** Mutates `payload` with primary service, service notes, POC, tech phone, and vendor notes. */
|
||||||
primary: string | undefined,
|
|
||||||
extras: string[],
|
|
||||||
): string[] | undefined {
|
|
||||||
if (extras.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set<string>();
|
|
||||||
|
|
||||||
const primaryKey = primary?.toLowerCase() ?? "";
|
|
||||||
|
|
||||||
const out: string[] = [];
|
|
||||||
|
|
||||||
for (const raw of extras) {
|
|
||||||
const trimmed = raw.trim();
|
|
||||||
|
|
||||||
if (!trimmed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = trimmed.toLowerCase();
|
|
||||||
|
|
||||||
if (key === primaryKey || seen.has(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
seen.add(key);
|
|
||||||
|
|
||||||
out.push(trimmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
return out.length > 0 ? out : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Mutates `payload` with primary/extra services, service notes, POC, tech phone, and vendor notes. */
|
|
||||||
export function assignWizardServicePocFields(
|
export function assignWizardServicePocFields(
|
||||||
payload: WorkOrderBoardCreatePayload,
|
payload: WorkOrderBoardCreatePayload,
|
||||||
draft: WorkOrderWizardDraft,
|
draft: WorkOrderWizardDraft,
|
||||||
|
|
@ -54,12 +19,6 @@ export function assignWizardServicePocFields(
|
||||||
payload.primaryService = primaryService;
|
payload.primaryService = primaryService;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraServices = normalizeExtraServices(primaryService, draft.extraServices);
|
|
||||||
|
|
||||||
if (extraServices) {
|
|
||||||
payload.extraServices = extraServices;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serviceNotes = trimOrUndefined(draft.pmNote);
|
const serviceNotes = trimOrUndefined(draft.pmNote);
|
||||||
|
|
||||||
if (serviceNotes) {
|
if (serviceNotes) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact";
|
||||||
import type {
|
import type {
|
||||||
WorkOrderTablePatch,
|
WorkOrderTablePatch,
|
||||||
WorkOrderTableRow,
|
WorkOrderTableRow,
|
||||||
|
|
@ -5,8 +6,9 @@ import type {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fields compared for dirty state and included in Save patch.
|
* Fields compared for dirty state and included in Save patch.
|
||||||
* Only keys that map to board PATCH (or required companions like dispatcherName / vendor labels).
|
* Only keys that map to board PATCH (or required companions like dispatcherName).
|
||||||
* POC, locationId, and tech phone are not board-editable — exclude so Save cannot no-op.
|
* Primary POC scalars and tech phone are not board-editable — exclude so Save cannot no-op.
|
||||||
|
* additionalContacts is board-editable (SH-218 PATCH field).
|
||||||
*/
|
*/
|
||||||
export const SLIDE_OVER_EDIT_KEYS = [
|
export const SLIDE_OVER_EDIT_KEYS = [
|
||||||
"woNumber",
|
"woNumber",
|
||||||
|
|
@ -23,6 +25,7 @@ export const SLIDE_OVER_EDIT_KEYS = [
|
||||||
"pm",
|
"pm",
|
||||||
"serviceNotes",
|
"serviceNotes",
|
||||||
"extraServices",
|
"extraServices",
|
||||||
|
"additionalContacts",
|
||||||
"vendorId",
|
"vendorId",
|
||||||
// company/tech are display companions; board persists vendor via vendorId only
|
// company/tech are display companions; board persists vendor via vendorId only
|
||||||
"status",
|
"status",
|
||||||
|
|
@ -37,6 +40,23 @@ function sameExtraServices(a: string[] | undefined, b: string[] | undefined): bo
|
||||||
return left.every((value, index) => value === right[index]);
|
return left.every((value, index) => value === right[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sameAdditionalContacts(
|
||||||
|
a: WorkOrderAdditionalContact[] | undefined,
|
||||||
|
b: WorkOrderAdditionalContact[] | undefined,
|
||||||
|
): boolean {
|
||||||
|
const left = a ?? [];
|
||||||
|
const right = b ?? [];
|
||||||
|
if (left.length !== right.length) return false;
|
||||||
|
return left.every((contact, index) => {
|
||||||
|
const other = right[index];
|
||||||
|
return (
|
||||||
|
contact.name === other.name &&
|
||||||
|
contact.phone === other.phone &&
|
||||||
|
(contact.notes ?? "") === (other.notes ?? "")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function isSlideOverDraftDirty(
|
export function isSlideOverDraftDirty(
|
||||||
draft: WorkOrderTableRow,
|
draft: WorkOrderTableRow,
|
||||||
baseline: WorkOrderTableRow,
|
baseline: WorkOrderTableRow,
|
||||||
|
|
@ -46,6 +66,12 @@ export function isSlideOverDraftDirty(
|
||||||
if (!sameExtraServices(draft.extraServices, baseline.extraServices)) return true;
|
if (!sameExtraServices(draft.extraServices, baseline.extraServices)) return true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (key === "additionalContacts") {
|
||||||
|
if (!sameAdditionalContacts(draft.additionalContacts, baseline.additionalContacts)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (draft[key] !== baseline[key]) return true;
|
if (draft[key] !== baseline[key]) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -64,6 +90,12 @@ export function buildSlideOverPatch(
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (key === "additionalContacts") {
|
||||||
|
if (!sameAdditionalContacts(draft.additionalContacts, baseline.additionalContacts)) {
|
||||||
|
patch.additionalContacts = draft.additionalContacts;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (draft[key] !== baseline[key]) {
|
if (draft[key] !== baseline[key]) {
|
||||||
(patch as Record<EditKey, WorkOrderTableRow[EditKey]>)[key] = draft[key];
|
(patch as Record<EditKey, WorkOrderTableRow[EditKey]>)[key] = draft[key];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { fireEvent, screen } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { SlideOverInfoTabEditView } from "@/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view";
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import { expandTablePatchToBoardFieldOps } from "@/domain/work-orders/mappers/work-order-board-patch-mapper";
|
||||||
|
import { buildSlideOverPatch } from "@/domain/work-orders/utils/slide-over-draft";
|
||||||
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/site-cell", () => ({
|
||||||
|
SiteCell: ({
|
||||||
|
additionalContacts,
|
||||||
|
onSave,
|
||||||
|
}: {
|
||||||
|
additionalContacts?: { name: string; phone: string }[];
|
||||||
|
onSave: (patch: {
|
||||||
|
site: string;
|
||||||
|
location: string;
|
||||||
|
locationId: string;
|
||||||
|
pocName: string;
|
||||||
|
pocPhone: string;
|
||||||
|
pocNotes: string;
|
||||||
|
additionalContacts?: { name: string; phone: string }[];
|
||||||
|
}) => void;
|
||||||
|
}) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
onSave({
|
||||||
|
site: "SEA1",
|
||||||
|
location: "Seattle",
|
||||||
|
locationId: "loc-1",
|
||||||
|
pocName: "Pat",
|
||||||
|
pocPhone: "111",
|
||||||
|
pocNotes: "",
|
||||||
|
additionalContacts: [{ name: "Backup", phone: "555-0101" }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
save site {additionalContacts?.length ?? 0}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell", () => ({
|
||||||
|
EditableWONumberCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/type-cell", () => ({
|
||||||
|
TypeCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({
|
||||||
|
AssignedToCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/date-cell", () => ({
|
||||||
|
DateCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/appt-time-cell", () => ({
|
||||||
|
ApptTimeCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/pm-type-cell", () => ({
|
||||||
|
PMTypeCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/vendor-cell", () => ({
|
||||||
|
VendorCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/status-cell", () => ({
|
||||||
|
StatusCell: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
|
||||||
|
return {
|
||||||
|
id: 42,
|
||||||
|
woNumber: "WO-42",
|
||||||
|
site: "SEA1",
|
||||||
|
type: "PM",
|
||||||
|
severity: null,
|
||||||
|
dispatcherId: "u1",
|
||||||
|
dispatcherName: "Dispatcher",
|
||||||
|
scheduledOn: "2026-07-20",
|
||||||
|
completedDate: "",
|
||||||
|
targetWeek: "",
|
||||||
|
dayGroup: "07/20",
|
||||||
|
dayLabel: "Mon",
|
||||||
|
location: "Seattle",
|
||||||
|
locationId: "loc-1",
|
||||||
|
pm: "HVAC",
|
||||||
|
serviceNotes: "",
|
||||||
|
extraServices: [],
|
||||||
|
company: "Vendor Co",
|
||||||
|
tech: "Tech",
|
||||||
|
techPhone: "",
|
||||||
|
vendorId: "v1",
|
||||||
|
apptStart: "",
|
||||||
|
apptEnd: "",
|
||||||
|
apptTime: "",
|
||||||
|
status: "Scheduled",
|
||||||
|
docStatus: "No",
|
||||||
|
pocName: "",
|
||||||
|
pocPhone: "",
|
||||||
|
pocNotes: "",
|
||||||
|
additionalContacts: [],
|
||||||
|
dueDate: "",
|
||||||
|
rescheduleCount: 0,
|
||||||
|
carriedOver: 0,
|
||||||
|
originalWeek: "",
|
||||||
|
originalDate: "",
|
||||||
|
isPastDue: false,
|
||||||
|
scheduleWeekOnly: false,
|
||||||
|
mediaCount: 0,
|
||||||
|
title: "",
|
||||||
|
priority: "",
|
||||||
|
primaryDispatchId: null,
|
||||||
|
rowVersion: "v1",
|
||||||
|
dispatchRowVersion: "",
|
||||||
|
flagColor: null,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("SlideOverInfoTabEditView additional contacts", () => {
|
||||||
|
it("passes additionalContacts into SiteCell and persists them on save", () => {
|
||||||
|
const onDraftChange = vi.fn();
|
||||||
|
const contacts = [{ name: "Existing", phone: "555-0000" }];
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<SlideOverInfoTabEditView
|
||||||
|
draft={baseRow({ additionalContacts: contacts })}
|
||||||
|
users={[]}
|
||||||
|
sites={[]}
|
||||||
|
vendors={[]}
|
||||||
|
pmTypes={[]}
|
||||||
|
allOrders={[]}
|
||||||
|
onDraftChange={onDraftChange}
|
||||||
|
/>,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /save site 1/i })).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /save site 1/i }));
|
||||||
|
|
||||||
|
expect(onDraftChange).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
additionalContacts: [{ name: "Backup", phone: "555-0101" }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes additionalContacts in the slide-over board PATCH ops", () => {
|
||||||
|
const baseline = baseRow();
|
||||||
|
const draft = baseRow({
|
||||||
|
additionalContacts: [{ name: "Backup", phone: "555-0101", notes: "Gate" }],
|
||||||
|
});
|
||||||
|
const patch = buildSlideOverPatch(draft, baseline);
|
||||||
|
|
||||||
|
expect(expandTablePatchToBoardFieldOps(patch)).toEqual([
|
||||||
|
{
|
||||||
|
field: "additionalContacts",
|
||||||
|
value: JSON.stringify([{ name: "Backup", phone: "555-0101", notes: "Gate" }]),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -140,8 +140,6 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
|
||||||
|
|
||||||
primaryService: "HVAC PM",
|
primaryService: "HVAC PM",
|
||||||
|
|
||||||
extraServices: ["Filter change", "Coil clean"],
|
|
||||||
|
|
||||||
serviceNotes: "Check unit on roof",
|
serviceNotes: "Check unit on roof",
|
||||||
|
|
||||||
pocName: "Jane Site Lead",
|
pocName: "Jane Site Lead",
|
||||||
|
|
@ -327,7 +325,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
|
||||||
expect(payload.vendorNotes).toBe("Also not in description");
|
expect(payload.vendorNotes).toBe("Also not in description");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps service notes as structured fields and extraServices without duplicates", () => {
|
it("maps service notes as structured fields and omits extraServices on create", () => {
|
||||||
const payload = mapWizardDraftToBoardCreatePayload({
|
const payload = mapWizardDraftToBoardCreatePayload({
|
||||||
...EMPTY_WIZARD_DRAFT,
|
...EMPTY_WIZARD_DRAFT,
|
||||||
|
|
||||||
|
|
@ -344,7 +342,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
|
||||||
|
|
||||||
expect(payload.primaryService).toBe("HVAC PM");
|
expect(payload.primaryService).toBe("HVAC PM");
|
||||||
|
|
||||||
expect(payload.extraServices).toEqual(["Filter change", "Coil clean"]);
|
expect(payload.extraServices).toBeUndefined();
|
||||||
|
|
||||||
expect(payload.serviceNotes).toBe("Check unit on roof");
|
expect(payload.serviceNotes).toBe("Check unit on roof");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ describe("slide-over-draft", () => {
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("woNumber");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("woNumber");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("status");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("status");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("vendorId");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("vendorId");
|
||||||
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("additionalContacts");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("pocName");
|
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("pocName");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("locationId");
|
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("locationId");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("techPhone");
|
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("techPhone");
|
||||||
|
|
@ -110,4 +111,14 @@ describe("slide-over-draft", () => {
|
||||||
extraServices: ["HVAC"],
|
extraServices: ["HVAC"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("tracks ordered additionalContacts for dirty state and patch (SH-218)", () => {
|
||||||
|
const row = baseRow();
|
||||||
|
const contacts = [{ name: "Backup", phone: "555-0101", notes: "Gate" }];
|
||||||
|
const draft = baseRow({ additionalContacts: contacts });
|
||||||
|
|
||||||
|
expect(isSlideOverDraftDirty(draft, row)).toBe(true);
|
||||||
|
expect(buildSlideOverPatch(draft, row)).toEqual({ additionalContacts: contacts });
|
||||||
|
expect(isSlideOverDraftDirty(baseRow({ additionalContacts: [] }), row)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue