diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx
index f7eef6e6..1b4b2abf 100644
--- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx
+++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx
@@ -127,8 +127,8 @@ function SlideOverOpenService({
if (!source.pm) {
return —;
}
- const name =
- services.find((service) => String(service.id) === String(source.serviceId))?.name ?? source.pm;
+ const linkedService = services.find((service) => String(service.id) === String(source.serviceId));
+ const name = linkedService?.isActive ? linkedService.name : source.pm;
return (
diff --git a/src/app/(protected)/workorders/_components/list/table/cells/pm-type-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/pm-type-cell.tsx
index e5152aa7..cbb1f9e3 100644
--- a/src/app/(protected)/workorders/_components/list/table/cells/pm-type-cell.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/cells/pm-type-cell.tsx
@@ -60,8 +60,8 @@ export function PMTypeCell({
const filtered = search
? serviceNames.filter((p) => p.toLowerCase().includes(search.toLowerCase()))
: serviceNames;
- const displayValue =
- services?.find((service) => String(service.id) === String(serviceId))?.name ?? value;
+ const linkedService = services?.find((service) => String(service.id) === String(serviceId));
+ const displayValue = linkedService?.isActive ? linkedService.name : value;
const extras = extraServices ?? [];
const hasDetails = hasServiceNotesDetails(serviceNotes, extras, avetaRequired);
diff --git a/src/domain/work-orders/mappers/expand-scalar-board-ops.ts b/src/domain/work-orders/mappers/expand-scalar-board-ops.ts
index 72d6abe5..f6dd5d04 100644
--- a/src/domain/work-orders/mappers/expand-scalar-board-ops.ts
+++ b/src/domain/work-orders/mappers/expand-scalar-board-ops.ts
@@ -21,7 +21,8 @@ type ScalarBoardOpRule = {
/** One row per scalar patch field, in emission order. */
const SCALAR_BOARD_OP_RULES: ScalarBoardOpRule[] = [
{ 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",
field: "severity",
diff --git a/src/domain/work-orders/utils/slide-over-draft.ts b/src/domain/work-orders/utils/slide-over-draft.ts
index af8c2679..8fa3ac57 100644
--- a/src/domain/work-orders/utils/slide-over-draft.ts
+++ b/src/domain/work-orders/utils/slide-over-draft.ts
@@ -15,6 +15,7 @@ export const SLIDE_OVER_EDIT_KEYS = [
"woNumber",
"site",
"type",
+ "severity",
"dispatcherId",
"dispatcherName",
"scheduledOn",
@@ -26,6 +27,7 @@ export const SLIDE_OVER_EDIT_KEYS = [
"completedDate",
"apptTime",
"pm",
+ "serviceId",
"serviceNotes",
"extraServices",
"avetaRequired",
diff --git a/src/test/app/(protected)/workorders/aveta-service-notes.test.tsx b/src/test/app/(protected)/workorders/aveta-service-notes.test.tsx
index 389f58ae..ff77c95b 100644
--- a/src/test/app/(protected)/workorders/aveta-service-notes.test.tsx
+++ b/src/test/app/(protected)/workorders/aveta-service-notes.test.tsx
@@ -2,6 +2,7 @@ import { fireEvent, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
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 { serviceIdentityPatch } from "@/domain/work-orders/utils/service-identity-patch";
import { renderWithProviders } from "@/test/test-utils";
describe("Aveta Service & Notes", () => {
@@ -49,6 +50,76 @@ describe("Aveta Service & Notes", () => {
expect(onSave).toHaveBeenCalledWith("QA Test Service", 701);
});
+ it("keeps the historical label when its linked registry service is inactive", () => {
+ renderWithProviders(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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", () => {
renderWithProviders(
({
- TypeCell: () => null,
+ TypeCell: ({ onSave }: { onSave: (type: "Reactive", severity: number) => void }) => (
+
+ ),
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({
AssignedToCell: () => null,
@@ -121,6 +127,85 @@ function baseRow(overrides: Partial = {}): WorkOrderTableRow
}
describe("SlideOverInfoTabEditView additional contacts", () => {
+ it("keeps the historic service label in slide-over display for an inactive service", () => {
+ renderWithProviders(
+ ,
+ { 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 (
+ <>
+ setDraft((current) => ({ ...current, ...patch }))}
+ />
+
+ >
+ );
+ }
+
+ renderWithProviders(, { 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", () => {
const onDraftChange = vi.fn();
const contacts = [{ name: "Existing", phone: "555-0000" }];
diff --git a/src/test/domain/work-orders/utils/slide-over-draft.test.ts b/src/test/domain/work-orders/utils/slide-over-draft.test.ts
index d0bae108..358f85b3 100644
--- a/src/test/domain/work-orders/utils/slide-over-draft.test.ts
+++ b/src/test/domain/work-orders/utils/slide-over-draft.test.ts
@@ -7,6 +7,7 @@ import {
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 { 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 {
return {
@@ -68,6 +69,7 @@ describe("slide-over-draft", () => {
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("serviceId");
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocName");
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocPhone");
expect(SLIDE_OVER_EDIT_KEYS).toContain("pocNotes");
@@ -77,6 +79,20 @@ describe("slide-over-draft", () => {
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)", () => {
const row = baseRow();
const draft = baseRow({ company: "Other Co", tech: "Pat" });