mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 14:42:05 +00:00
fix(workorders): preserve service history on slide-over saves
This commit is contained in:
parent
24107ee3b2
commit
18fefef9a3
7 changed files with 181 additions and 6 deletions
|
|
@ -127,8 +127,8 @@ function SlideOverOpenService({
|
||||||
if (!source.pm) {
|
if (!source.pm) {
|
||||||
return <span style={{ color: "var(--color-text-muted)" }}>—</span>;
|
return <span style={{ color: "var(--color-text-muted)" }}>—</span>;
|
||||||
}
|
}
|
||||||
const name =
|
const linkedService = services.find((service) => String(service.id) === String(source.serviceId));
|
||||||
services.find((service) => String(service.id) === String(source.serviceId))?.name ?? source.pm;
|
const name = linkedService?.isActive ? linkedService.name : source.pm;
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-1.5 text-[12.5px]">
|
<span className="inline-flex items-center gap-1.5 text-[12.5px]">
|
||||||
<PmTypeIcon pmType={name} size={14} />
|
<PmTypeIcon pmType={name} size={14} />
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@ export function PMTypeCell({
|
||||||
const filtered = search
|
const filtered = search
|
||||||
? serviceNames.filter((p) => p.toLowerCase().includes(search.toLowerCase()))
|
? serviceNames.filter((p) => p.toLowerCase().includes(search.toLowerCase()))
|
||||||
: serviceNames;
|
: serviceNames;
|
||||||
const displayValue =
|
const linkedService = services?.find((service) => String(service.id) === String(serviceId));
|
||||||
services?.find((service) => String(service.id) === String(serviceId))?.name ?? value;
|
const displayValue = linkedService?.isActive ? linkedService.name : value;
|
||||||
|
|
||||||
const extras = extraServices ?? [];
|
const extras = extraServices ?? [];
|
||||||
const hasDetails = hasServiceNotesDetails(serviceNotes, extras, avetaRequired);
|
const hasDetails = hasServiceNotesDetails(serviceNotes, extras, avetaRequired);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ type ScalarBoardOpRule = {
|
||||||
/** One row per scalar patch field, in emission order. */
|
/** One row per scalar patch field, in emission order. */
|
||||||
const SCALAR_BOARD_OP_RULES: ScalarBoardOpRule[] = [
|
const SCALAR_BOARD_OP_RULES: ScalarBoardOpRule[] = [
|
||||||
{ key: "woNumber", field: "woNumber", toValue: (value) => value as string },
|
{ key: "woNumber", field: "woNumber", toValue: (value) => value as string },
|
||||||
// Severity before type so Emergency/Reactive never land without required severity.
|
// Severity before type, then service clear before type because the backend validates
|
||||||
|
// the currently assigned service against the requested work-order type.
|
||||||
{
|
{
|
||||||
key: "severity",
|
key: "severity",
|
||||||
field: "severity",
|
field: "severity",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export const SLIDE_OVER_EDIT_KEYS = [
|
||||||
"woNumber",
|
"woNumber",
|
||||||
"site",
|
"site",
|
||||||
"type",
|
"type",
|
||||||
|
"severity",
|
||||||
"dispatcherId",
|
"dispatcherId",
|
||||||
"dispatcherName",
|
"dispatcherName",
|
||||||
"scheduledOn",
|
"scheduledOn",
|
||||||
|
|
@ -26,6 +27,7 @@ export const SLIDE_OVER_EDIT_KEYS = [
|
||||||
"completedDate",
|
"completedDate",
|
||||||
"apptTime",
|
"apptTime",
|
||||||
"pm",
|
"pm",
|
||||||
|
"serviceId",
|
||||||
"serviceNotes",
|
"serviceNotes",
|
||||||
"extraServices",
|
"extraServices",
|
||||||
"avetaRequired",
|
"avetaRequired",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { fireEvent, screen } from "@testing-library/react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { PMTypeCell } from "@/app/(protected)/workorders/_components/list/table/cells/pm-type-cell";
|
import { PMTypeCell } from "@/app/(protected)/workorders/_components/list/table/cells/pm-type-cell";
|
||||||
import { ServiceNotesDialog } from "@/app/(protected)/workorders/_components/list/table/cells/service-notes-dialog";
|
import { ServiceNotesDialog } from "@/app/(protected)/workorders/_components/list/table/cells/service-notes-dialog";
|
||||||
|
import { serviceIdentityPatch } from "@/domain/work-orders/utils/service-identity-patch";
|
||||||
import { renderWithProviders } from "@/test/test-utils";
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
describe("Aveta Service & Notes", () => {
|
describe("Aveta Service & Notes", () => {
|
||||||
|
|
@ -49,6 +50,76 @@ describe("Aveta Service & Notes", () => {
|
||||||
expect(onSave).toHaveBeenCalledWith("QA Test Service", 701);
|
expect(onSave).toHaveBeenCalledWith("QA Test Service", 701);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the historical label when its linked registry service is inactive", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<PMTypeCell
|
||||||
|
value="Historic Service Name"
|
||||||
|
serviceId={701}
|
||||||
|
pmTypes={[]}
|
||||||
|
services={[
|
||||||
|
{
|
||||||
|
id: 701,
|
||||||
|
name: "Renamed Service",
|
||||||
|
trade: "Electrical",
|
||||||
|
iconKey: "wrench",
|
||||||
|
requiresCompletionDocument: false,
|
||||||
|
completionDocTemplate: null,
|
||||||
|
isActive: false,
|
||||||
|
supportedWorkOrderTypes: ["PM"],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
workOrderType="PM"
|
||||||
|
onSave={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /Historic Service Name/i })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: /Renamed Service/i })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the historic identity when only service notes change", () => {
|
||||||
|
const onSaveDetails = vi.fn();
|
||||||
|
renderWithProviders(
|
||||||
|
<PMTypeCell
|
||||||
|
value="Historic Service Name"
|
||||||
|
serviceId={701}
|
||||||
|
pmTypes={[]}
|
||||||
|
serviceNotes=""
|
||||||
|
services={[
|
||||||
|
{
|
||||||
|
id: 701,
|
||||||
|
name: "Renamed Service",
|
||||||
|
trade: "Electrical",
|
||||||
|
iconKey: "wrench",
|
||||||
|
requiresCompletionDocument: false,
|
||||||
|
completionDocTemplate: null,
|
||||||
|
isActive: false,
|
||||||
|
supportedWorkOrderTypes: ["PM"],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
workOrderType="PM"
|
||||||
|
onSave={vi.fn()}
|
||||||
|
onSaveDetails={onSaveDetails}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Historic Service Name/i }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Add service and notes/i }));
|
||||||
|
fireEvent.change(
|
||||||
|
screen.getByPlaceholderText(
|
||||||
|
"Describe the specific issue, location details, door number, etc.",
|
||||||
|
),
|
||||||
|
{ target: { value: "Changed detail only" } },
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||||
|
|
||||||
|
const saved = onSaveDetails.mock.calls[0]![0];
|
||||||
|
expect(saved).toMatchObject({ pm: "Historic Service Name", serviceId: 701 });
|
||||||
|
expect(serviceIdentityPatch(701, "Historic Service Name", saved.pm, saved.serviceId)).toEqual(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("shows the Aveta shield on the Service cell when required", () => {
|
it("shows the Aveta shield on the Service cell when required", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<PMTypeCell
|
<PMTypeCell
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { useState } from "react";
|
||||||
import { fireEvent, screen } from "@testing-library/react";
|
import { fireEvent, screen } from "@testing-library/react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { SlideOverInfoTabEditView } from "@/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view";
|
import { SlideOverInfoTabEditView } from "@/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view";
|
||||||
|
import { SlideOverInfoTabDisplayView } from "@/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view";
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
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 { expandTablePatchToBoardFieldOps } from "@/domain/work-orders/mappers/work-order-board-patch-mapper";
|
||||||
import { buildSlideOverPatch } from "@/domain/work-orders/utils/slide-over-draft";
|
import { buildSlideOverPatch } from "@/domain/work-orders/utils/slide-over-draft";
|
||||||
|
|
@ -48,7 +50,11 @@ vi.mock(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/type-cell", () => ({
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/type-cell", () => ({
|
||||||
TypeCell: () => null,
|
TypeCell: ({ onSave }: { onSave: (type: "Reactive", severity: number) => void }) => (
|
||||||
|
<button type="button" onClick={() => onSave("Reactive", 2)}>
|
||||||
|
Change type to Reactive
|
||||||
|
</button>
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({
|
||||||
AssignedToCell: () => null,
|
AssignedToCell: () => null,
|
||||||
|
|
@ -121,6 +127,85 @@ function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("SlideOverInfoTabEditView additional contacts", () => {
|
describe("SlideOverInfoTabEditView additional contacts", () => {
|
||||||
|
it("keeps the historic service label in slide-over display for an inactive service", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<SlideOverInfoTabDisplayView
|
||||||
|
source={baseRow({ pm: "Historic Service Name", serviceId: 701 })}
|
||||||
|
isCancelled={false}
|
||||||
|
sites={[]}
|
||||||
|
vendors={[]}
|
||||||
|
pmTypes={[]}
|
||||||
|
services={[
|
||||||
|
{
|
||||||
|
id: 701,
|
||||||
|
name: "Renamed Service",
|
||||||
|
trade: "Electrical",
|
||||||
|
iconKey: "wrench",
|
||||||
|
requiresCompletionDocument: false,
|
||||||
|
completionDocTemplate: null,
|
||||||
|
isActive: false,
|
||||||
|
supportedWorkOrderTypes: ["PM"],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Historic Service Name")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Renamed Service")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves an incompatible type change only after clearing the registry service", () => {
|
||||||
|
const baseline = baseRow({ type: "PM", pm: "PM only", serviceId: 701 });
|
||||||
|
const onSave = vi.fn();
|
||||||
|
const pmOnlyService = {
|
||||||
|
id: 701,
|
||||||
|
name: "PM only",
|
||||||
|
trade: "Electrical",
|
||||||
|
iconKey: "wrench",
|
||||||
|
requiresCompletionDocument: false,
|
||||||
|
completionDocTemplate: null,
|
||||||
|
isActive: true,
|
||||||
|
supportedWorkOrderTypes: ["PM" as const],
|
||||||
|
};
|
||||||
|
|
||||||
|
function SlideOverSaveHarness() {
|
||||||
|
const [draft, setDraft] = useState(baseline);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SlideOverInfoTabEditView
|
||||||
|
draft={draft}
|
||||||
|
users={[]}
|
||||||
|
sites={[]}
|
||||||
|
vendors={[]}
|
||||||
|
pmTypes={[]}
|
||||||
|
services={[pmOnlyService]}
|
||||||
|
allOrders={[]}
|
||||||
|
onDraftChange={(patch) => setDraft((current) => ({ ...current, ...patch }))}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
onSave(expandTablePatchToBoardFieldOps(buildSlideOverPatch(draft, baseline)))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Save changes
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWithProviders(<SlideOverSaveHarness />, { withAuth: false });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Change type to Reactive" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
||||||
|
|
||||||
|
expect(onSave).toHaveBeenCalledWith([
|
||||||
|
{ field: "severity", value: "2" },
|
||||||
|
{ field: "serviceId", value: "" },
|
||||||
|
{ field: "workOrderType", value: "Reactive" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("passes additionalContacts into SiteCell and persists them on save", () => {
|
it("passes additionalContacts into SiteCell and persists them on save", () => {
|
||||||
const onDraftChange = vi.fn();
|
const onDraftChange = vi.fn();
|
||||||
const contacts = [{ name: "Existing", phone: "555-0000" }];
|
const contacts = [{ name: "Existing", phone: "555-0000" }];
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift";
|
import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift";
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import { mergeAutoSchedulePatch } from "@/domain/work-orders/utils/wo-lifecycle-transitions";
|
import { mergeAutoSchedulePatch } from "@/domain/work-orders/utils/wo-lifecycle-transitions";
|
||||||
|
import { expandTablePatchToBoardFieldOps } from "@/domain/work-orders/mappers/work-order-board-patch-mapper";
|
||||||
|
|
||||||
function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
|
function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
|
||||||
return {
|
return {
|
||||||
|
|
@ -68,6 +69,7 @@ describe("slide-over-draft", () => {
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("technicianAssigned");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("technicianAssigned");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("additionalContacts");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("additionalContacts");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("avetaRequired");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("avetaRequired");
|
||||||
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("serviceId");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocName");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocName");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocPhone");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocPhone");
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocNotes");
|
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocNotes");
|
||||||
|
|
@ -77,6 +79,20 @@ describe("slide-over-draft", () => {
|
||||||
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("tech");
|
expect(SLIDE_OVER_EDIT_KEYS).not.toContain("tech");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("saves an incompatible type change after clearing its registry service", () => {
|
||||||
|
const baseline = baseRow({ type: "PM", pm: "PM only", serviceId: 701 });
|
||||||
|
const draft = baseRow({ type: "Reactive", severity: 2, pm: "", serviceId: null });
|
||||||
|
const patch = buildSlideOverPatch(draft, baseline);
|
||||||
|
|
||||||
|
expect(isSlideOverDraftDirty(draft, baseline)).toBe(true);
|
||||||
|
expect(patch).toMatchObject({ type: "Reactive", severity: 2, serviceId: null, pm: "" });
|
||||||
|
expect(expandTablePatchToBoardFieldOps(patch)).toEqual([
|
||||||
|
{ field: "severity", value: "2" },
|
||||||
|
{ field: "serviceId", value: "" },
|
||||||
|
{ field: "workOrderType", value: "Reactive" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("tracks vendor via vendorId only (company/tech are display companions)", () => {
|
it("tracks vendor via vendorId only (company/tech are display companions)", () => {
|
||||||
const row = baseRow();
|
const row = baseRow();
|
||||||
const draft = baseRow({ company: "Other Co", tech: "Pat" });
|
const draft = baseRow({ company: "Other Co", tech: "Pat" });
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue