diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx
index 6ee17752..46854cb8 100644
--- a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx
@@ -85,7 +85,8 @@ export function SiteDialog({
Select a site and edit its point of contact
-
+ {/* Locked while saving: Save sends the values it had when clicked, so later edits would be lost. */}
+
Site
0) {
+ return location.contacts;
+ }
+ const name = location.contact ?? "";
+ const phone = location.phone ?? "";
+ return name.trim() || phone.trim() ? [{ name, phone }] : [];
+}
+
/** The site record's contacts and notes, in the dialog's field shape. */
function siteRecordValues(location: Location) {
- const [main, ...others] = location.contacts ?? [];
+ const [main, ...others] = siteContacts(location);
return {
primaryId: main?.id,
pn: main?.name ?? "",
diff --git a/src/domain/locations/use-cases/use-update-site-contact-info.ts b/src/domain/locations/use-cases/use-update-site-contact-info.ts
index 4f218a42..d646430c 100644
--- a/src/domain/locations/use-cases/use-update-site-contact-info.ts
+++ b/src/domain/locations/use-cases/use-update-site-contact-info.ts
@@ -29,5 +29,7 @@ export function useUpdateSiteContactInfo(): UseMutationResult<
onError: (error: Error) => {
toast.error(error.message || "Failed to update the site");
},
+ // The toast above is this failure's only message; the global mutation toast would repeat it.
+ meta: { suppressErrorToast: true },
});
}
diff --git a/src/test/app/(protected)/workorders/site-dialog-site-record.test.tsx b/src/test/app/(protected)/workorders/site-dialog-site-record.test.tsx
index edddda9b..b4a67ced 100644
--- a/src/test/app/(protected)/workorders/site-dialog-site-record.test.tsx
+++ b/src/test/app/(protected)/workorders/site-dialog-site-record.test.tsx
@@ -1,12 +1,22 @@
+import type { QueryClient } from "@tanstack/react-query";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SiteDialog } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog";
import type { Location } from "@/domain/locations/types/location";
import { queryKeys } from "@/infra/query-key/query-key";
+import { createAppQueryClient } from "@/lib/query/query-client";
import { renderWithProviders } from "@/test/test-utils";
const getById = vi.fn();
const updateContactInfo = vi.fn();
+const toastMocks = vi.hoisted(() => ({
+ error: vi.fn(),
+ success: vi.fn(),
+ info: vi.fn(),
+ warning: vi.fn(),
+}));
+
+vi.mock("react-toastify", () => ({ toast: toastMocks }));
vi.mock("@/domain/locations/api/locations-api", () => ({
locationsApi: {
@@ -38,7 +48,10 @@ const SITE_RECORD_COPY =
"Contacts and notes are saved to the site record and apply to all its work orders.";
const SITE_UNAVAILABLE_COPY = "Site record unavailable — changes apply to this work order only.";
-function renderDialog(overrides: Partial[0]> = {}) {
+function renderDialog(
+ overrides: Partial[0]> = {},
+ options: { queryClient?: QueryClient } = {},
+) {
const onSave = vi.fn();
const onOpenChange = vi.fn();
const { queryClient } = renderWithProviders(
@@ -54,7 +67,7 @@ function renderDialog(overrides: Partial[0]> = {})
onSave={onSave}
{...overrides}
/>,
- { withAuth: false },
+ { withAuth: false, ...options },
);
return { onSave, onOpenChange, queryClient };
}
@@ -72,6 +85,7 @@ describe("Work order Site dialog edits the site record", () => {
beforeEach(() => {
getById.mockReset();
updateContactInfo.mockReset();
+ toastMocks.error.mockReset();
getById.mockResolvedValue(dallas);
});
@@ -277,6 +291,96 @@ describe("Work order Site dialog edits the site record", () => {
expect(onOpenChange).not.toHaveBeenCalled();
});
+ it("shows one error toast when the site update fails, not the global one as well", async () => {
+ updateContactInfo.mockRejectedValue(new Error("Forbidden"));
+ renderDialog({}, { queryClient: createAppQueryClient() });
+ await screen.findByDisplayValue("Jane");
+
+ fireEvent.change(primaryField("Notes…"), { target: { value: "Gate code 9999" } });
+ fireEvent.click(saveButton());
+
+ await waitFor(() => expect(saveButton()).toBeEnabled());
+ expect(toastMocks.error).toHaveBeenCalledTimes(1);
+ expect(toastMocks.error).toHaveBeenCalledWith("Forbidden");
+ });
+
+ it("locks the fields while the site update is in flight, so no edit is lost", async () => {
+ let finishSiteUpdate: () => void = () => {};
+ updateContactInfo.mockReturnValue(
+ new Promise((resolve) => {
+ finishSiteUpdate = resolve;
+ }),
+ );
+ const { onSave } = renderDialog();
+ await screen.findByDisplayValue("Jane");
+
+ fireEvent.change(primaryField("Notes…"), { target: { value: "Gate code 9999" } });
+ fireEvent.click(saveButton());
+
+ await screen.findByRole("button", { name: /saving/i });
+ expect(primaryField("POC name")).toBeDisabled();
+ expect(primaryField("POC phone")).toBeDisabled();
+ expect(primaryField("Notes…")).toBeDisabled();
+ expect(screen.getByRole("button", { name: /add point of contact/i })).toBeDisabled();
+
+ finishSiteUpdate();
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
+ expect(onSave.mock.calls[0][0]).toMatchObject({ pocNotes: "" });
+ });
+
+ describe("on a site saved before the contacts list existed", () => {
+ const legacyDallas: Location = {
+ ...dallas,
+ phone: "(421) 433-0050",
+ contact: "Legacy Lee",
+ contacts: undefined,
+ };
+
+ beforeEach(() => {
+ getById.mockResolvedValue(legacyDallas);
+ });
+
+ it("shows the site's legacy contact and saves a notes-only edit without retyping it", async () => {
+ updateContactInfo.mockResolvedValue(undefined);
+ const { onSave } = renderDialog();
+
+ expect(await screen.findByDisplayValue("Legacy Lee")).toBeInTheDocument();
+ expect(primaryField("POC phone")).toHaveValue("(421) 433-0050");
+ expect(saveButton()).toBeDisabled();
+
+ fireEvent.change(primaryField("Notes…"), { target: { value: "Gate code 9999" } });
+ fireEvent.click(saveButton());
+
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
+ expect(screen.queryByText(/name is required/i)).not.toBeInTheDocument();
+ // The legacy contact becomes the site's first contact row, unchanged.
+ expect(updateContactInfo).toHaveBeenCalledWith("1", {
+ contacts: [{ name: "Legacy Lee", phone: "(421) 433-0050" }],
+ notes: "Gate code 9999",
+ });
+ // Only the notes were edited, so the work order keeps its own contact override.
+ expect(onSave.mock.calls[0][0]).toMatchObject({
+ pocName: "Someone Else",
+ pocPhone: "(000) 000-0000",
+ pocNotes: "",
+ });
+ });
+
+ it("does not report a contact edit the user never made when the work order had no POC", async () => {
+ updateContactInfo.mockResolvedValue(undefined);
+ const { onSave } = renderDialog({ pocName: "", pocPhone: "" });
+
+ expect(await screen.findByDisplayValue("Legacy Lee")).toBeInTheDocument();
+ expect(saveButton()).toBeDisabled();
+
+ fireEvent.change(primaryField("Notes…"), { target: { value: "Gate code 9999" } });
+ fireEvent.click(saveButton());
+
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
+ expect(onSave.mock.calls[0][0]).toMatchObject({ pocName: "", pocPhone: "", pocNotes: "" });
+ });
+ });
+
it("requires the first contact before saving", async () => {
const { onSave } = renderDialog();
await screen.findByDisplayValue("Jane");