mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 16:13:12 +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}
|
||||
pocPhone={draft.pocPhone}
|
||||
pocNotes={draft.pocNotes}
|
||||
additionalContacts={draft.additionalContacts}
|
||||
sites={sites}
|
||||
onSave={(patch) =>
|
||||
onDraftChange({
|
||||
site: patch.site,
|
||||
location: patch.location,
|
||||
locationId: patch.locationId,
|
||||
pocName: patch.pocName,
|
||||
pocPhone: patch.pocPhone,
|
||||
pocNotes: patch.pocNotes,
|
||||
...(patch.additionalContacts !== undefined
|
||||
? { additionalContacts: patch.additionalContacts }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SlideOverInfoRow>
|
||||
<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 label="Assigned To">
|
||||
<AssignedToCell
|
||||
|
|
|
|||
|
|
@ -8,42 +8,7 @@ function trimOrUndefined(value: string | undefined): string | undefined {
|
|||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function normalizeExtraServices(
|
||||
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. */
|
||||
/** Mutates `payload` with primary service, service notes, POC, tech phone, and vendor notes. */
|
||||
export function assignWizardServicePocFields(
|
||||
payload: WorkOrderBoardCreatePayload,
|
||||
draft: WorkOrderWizardDraft,
|
||||
|
|
@ -54,12 +19,6 @@ export function assignWizardServicePocFields(
|
|||
payload.primaryService = primaryService;
|
||||
}
|
||||
|
||||
const extraServices = normalizeExtraServices(primaryService, draft.extraServices);
|
||||
|
||||
if (extraServices) {
|
||||
payload.extraServices = extraServices;
|
||||
}
|
||||
|
||||
const serviceNotes = trimOrUndefined(draft.pmNote);
|
||||
|
||||
if (serviceNotes) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact";
|
||||
import type {
|
||||
WorkOrderTablePatch,
|
||||
WorkOrderTableRow,
|
||||
|
|
@ -5,8 +6,9 @@ import type {
|
|||
|
||||
/**
|
||||
* Fields compared for dirty state and included in Save patch.
|
||||
* Only keys that map to board PATCH (or required companions like dispatcherName / vendor labels).
|
||||
* POC, locationId, and tech phone are not board-editable — exclude so Save cannot no-op.
|
||||
* Only keys that map to board PATCH (or required companions like dispatcherName).
|
||||
* 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 = [
|
||||
"woNumber",
|
||||
|
|
@ -23,6 +25,7 @@ export const SLIDE_OVER_EDIT_KEYS = [
|
|||
"pm",
|
||||
"serviceNotes",
|
||||
"extraServices",
|
||||
"additionalContacts",
|
||||
"vendorId",
|
||||
// company/tech are display companions; board persists vendor via vendorId only
|
||||
"status",
|
||||
|
|
@ -37,6 +40,23 @@ function sameExtraServices(a: string[] | undefined, b: string[] | undefined): bo
|
|||
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(
|
||||
draft: WorkOrderTableRow,
|
||||
baseline: WorkOrderTableRow,
|
||||
|
|
@ -46,6 +66,12 @@ export function isSlideOverDraftDirty(
|
|||
if (!sameExtraServices(draft.extraServices, baseline.extraServices)) return true;
|
||||
continue;
|
||||
}
|
||||
if (key === "additionalContacts") {
|
||||
if (!sameAdditionalContacts(draft.additionalContacts, baseline.additionalContacts)) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (draft[key] !== baseline[key]) return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -64,6 +90,12 @@ export function buildSlideOverPatch(
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if (key === "additionalContacts") {
|
||||
if (!sameAdditionalContacts(draft.additionalContacts, baseline.additionalContacts)) {
|
||||
patch.additionalContacts = draft.additionalContacts;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (draft[key] !== baseline[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",
|
||||
|
||||
extraServices: ["Filter change", "Coil clean"],
|
||||
|
||||
serviceNotes: "Check unit on roof",
|
||||
|
||||
pocName: "Jane Site Lead",
|
||||
|
|
@ -327,7 +325,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
|
|||
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({
|
||||
...EMPTY_WIZARD_DRAFT,
|
||||
|
||||
|
|
@ -344,7 +342,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
|
|||
|
||||
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");
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,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("additionalContacts");
|
||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("pocName");
|
||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("locationId");
|
||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("techPhone");
|
||||
|
|
@ -110,4 +111,14 @@ describe("slide-over-draft", () => {
|
|||
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