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");