diff --git a/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts b/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts
index 8566c1cd..fc557598 100644
--- a/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts
+++ b/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts
@@ -36,10 +36,11 @@ export function buildSiteDialogPatch({
locationId: locId,
pocName,
pocPhone,
- // Notes live on the site record; an empty override lets the site's notes show through.
- pocNotes: followsSiteRecord ? "" : pocNotes,
+ pocNotes,
};
- if (followsSiteRecord || contactsDirty || baselineHadContacts) {
+ // Following the site record, the dialog's extra contacts are the site's: copy them only when edited.
+ const sendContacts = followsSiteRecord ? contactsDirty : contactsDirty || baselineHadContacts;
+ if (sendContacts) {
patch.additionalContacts = extraContacts;
}
return patch;
diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-detail-panel.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-detail-panel.tsx
index 93815ade..da15ab57 100644
--- a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-detail-panel.tsx
+++ b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-detail-panel.tsx
@@ -35,6 +35,9 @@ export function SiteDialogDetailPanel({
addressPreview,
fallbackLabel,
}: SiteDialogDetailPanelProps) {
+ // The site's own phone; a frozen snapshot carries it as `phone`.
+ const sitePhone = locationDetail?.sitePhone ?? locationDetail?.phone ?? "";
+
return (
)}
{!addressPreview && }
- {Boolean(locationDetail.phone) && (
-
- )}
+ {Boolean(sitePhone) && }
{Boolean(locationDetail.contactEmail) && (
)}
diff --git a/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts
index ecb98c03..0f7cfec1 100644
--- a/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts
+++ b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts
@@ -2,7 +2,11 @@ import { useEffect, useMemo } from "react";
import { buildSiteDialogPatch } from "@/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch";
import type { SitePatch } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-types";
import { useSiteDialogFormFields } from "@/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-form-fields";
-import { useSiteRecordSync } from "@/app/(protected)/workorders/_components/list/table/cells/use-site-record-sync";
+import { resolveLocationId } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-helpers";
+import {
+ useSiteRecordSync,
+ workOrderPocAfterSiteSave,
+} from "@/app/(protected)/workorders/_components/list/table/cells/use-site-record-sync";
import { useUpdateSiteContactInfo } from "@/domain/locations/use-cases/use-update-site-contact-info";
import { useLocationDetail } from "@/domain/locations/use-cases/use-location-detail";
import { formatLocationAddressPreview } from "@/domain/locations/mappers/location-mapper";
@@ -106,14 +110,18 @@ export function useSiteDialogState({
}, [open, locId, locationDetail, pocFilledFor, setPn, setPp, setPocFilledFor, frozenSite]);
const saveWorkOrder = () => {
+ const poc = workOrderPocAfterSiteSave(
+ siteRecord,
+ { pocName, pocPhone, pocNotes },
+ { pocName: fields.pn, pocPhone: fields.pp, pocNotes: fields.notes },
+ fields.locId !== resolveLocationId(locationId, value, sites),
+ );
onSave(
buildSiteDialogPatch({
code: fields.code,
locId: fields.locId,
selected: fields.selected,
- pocName: fields.pn,
- pocPhone: fields.pp,
- pocNotes: fields.notes,
+ ...poc,
extraContacts: fields.extraContacts,
contactsDirty: fields.contactsDirty,
baselineHadContacts: fields.baselineHadContacts,
diff --git a/src/app/(protected)/workorders/_components/list/table/cells/use-site-record-sync.ts b/src/app/(protected)/workorders/_components/list/table/cells/use-site-record-sync.ts
index 2c5bcbad..07a482f5 100644
--- a/src/app/(protected)/workorders/_components/list/table/cells/use-site-record-sync.ts
+++ b/src/app/(protected)/workorders/_components/list/table/cells/use-site-record-sync.ts
@@ -24,6 +24,10 @@ type UseSiteRecordSyncArgs = {
fields: SiteRecordFields;
};
+export type WorkOrderPoc = { pocName: string; pocPhone: string; pocNotes: string };
+
+type SiteRecordEdits = { primary: boolean; notes: boolean };
+
function toRequest(
primaryId: number | undefined,
fields: Pick,
@@ -48,9 +52,47 @@ function fingerprint(request: SiteContactInfoInput): string {
});
}
+function diffEdits(
+ baseline: SiteContactInfoInput | null,
+ request: SiteContactInfoInput,
+): SiteRecordEdits {
+ if (baseline === null) {
+ return { primary: false, notes: false };
+ }
+ const before = baseline.contacts[0];
+ const after = request.contacts[0];
+ return {
+ primary: before.name.trim() !== after.name.trim() || before.phone.trim() !== after.phone.trim(),
+ notes: baseline.notes.trim() !== request.notes.trim(),
+ };
+}
+
/**
- * Loads the selected site's contacts and notes into the Site dialog and reports whether the
- * user changed them, so Save can write them back to the site record.
+ * The work order's own POC values once the site record owns them: a field follows the site only
+ * when the user edited it (or picked another site); untouched fields keep the work order's values.
+ */
+export function workOrderPocAfterSiteSave(
+ siteRecord: { synced: boolean; edits: SiteRecordEdits },
+ original: WorkOrderPoc,
+ current: WorkOrderPoc,
+ siteSwitched: boolean,
+): WorkOrderPoc {
+ if (!siteRecord.synced) {
+ return current;
+ }
+ const primary = siteSwitched || siteRecord.edits.primary;
+ const notes = siteSwitched || siteRecord.edits.notes;
+ return {
+ pocName: primary ? current.pocName : original.pocName,
+ pocPhone: primary ? current.pocPhone : original.pocPhone,
+ // Notes live on the site record; an empty override lets the site's notes show through.
+ pocNotes: notes ? "" : original.pocNotes,
+ };
+}
+
+/**
+ * Loads the selected site's contacts and notes into the Site dialog and reports what the user
+ * changed, so Save can write them back to the site record.
*/
export function useSiteRecordSync({
enabled,
@@ -62,7 +104,7 @@ export function useSiteRecordSync({
}: UseSiteRecordSyncArgs) {
const [syncedFor, setSyncedFor] = useState("");
const [primaryId, setPrimaryId] = useState(undefined);
- const [baseline, setBaseline] = useState("");
+ const [baseline, setBaseline] = useState(null);
const { resetVersion, setPn, setPp, setNotes, setExtraContacts } = fields;
useEffect(() => {
@@ -87,14 +129,12 @@ export function useSiteRecordSync({
setNotes(notes);
setPrimaryId(main?.id);
setBaseline(
- fingerprint(
- toRequest(main?.id, {
- pn: main?.name ?? "",
- pp: main?.phone ?? "",
- notes,
- extraContacts: extras,
- }),
- ),
+ toRequest(main?.id, {
+ pn: main?.name ?? "",
+ pp: main?.phone ?? "",
+ notes,
+ extraContacts: extras,
+ }),
);
setSyncedFor(locId);
}, [enabled, open, locId, locationDetail, syncedFor, setPn, setPp, setNotes, setExtraContacts]);
@@ -106,7 +146,8 @@ export function useSiteRecordSync({
synced,
// Until the site loads the fields do not reflect it; a failed load falls back to the work order.
loading: enabled && Boolean(locId) && !synced && !locationDetailError,
- siteChanged: synced && fingerprint(request) !== baseline,
+ siteChanged: synced && baseline !== null && fingerprint(request) !== fingerprint(baseline),
+ edits: diffEdits(synced ? baseline : null, request),
request,
};
}
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 0f677c6b..4f218a42 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
@@ -8,7 +8,7 @@ interface UpdateSiteContactInfoInput extends SiteContactInfoInput {
id: string | number;
}
-/** Writes contacts and notes to the site record, then refetches every site query. */
+/** Writes contacts and notes to the site record, then refetches sites and work orders. */
export function useUpdateSiteContactInfo(): UseMutationResult<
void,
Error,
@@ -20,7 +20,11 @@ export function useUpdateSiteContactInfo(): UseMutationResult<
mutationFn: ({ id, contacts, notes }: UpdateSiteContactInfoInput) =>
locationsApi.updateContactInfo(id, { contacts, notes }),
onSuccess: async () => {
- await queryClient.invalidateQueries({ queryKey: queryKeys.locations.all });
+ // Other work orders at this site show its contacts and notes too.
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: queryKeys.locations.all }),
+ queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all }),
+ ]);
},
onError: (error: Error) => {
toast.error(error.message || "Failed to update the site");
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 52d52fe8..c77f15d2 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
@@ -21,7 +21,8 @@ const dallas: Location = {
city: "Dallas",
state: "TX",
zipCode: "75201",
- phone: "(214) 555-0100",
+ sitePhone: "(214) 555-0100",
+ phone: "(421) 433-0032",
contactEmail: "dal1@acme.test",
notes: "Gate code 1234",
contacts: [
@@ -83,6 +84,8 @@ describe("Work order Site dialog edits the site record", () => {
);
expect(address).toHaveAttribute("target", "_blank");
expect(screen.getByTitle("Site location map")).toBeInTheDocument();
+ expect(screen.getByText("(214) 555-0100")).toBeInTheDocument();
+ expect(screen.getByText("dal1@acme.test")).toBeInTheDocument();
});
it("keeps Save disabled until something changes", async () => {
@@ -107,13 +110,16 @@ describe("Work order Site dialog edits the site record", () => {
fireEvent.change(primaryField("Notes…"), {
target: { value: "Gate code 9999" },
});
+ fireEvent.change(screen.getAllByPlaceholderText("POC phone")[1], {
+ target: { value: "(421) 433-7777" },
+ });
fireEvent.click(saveButton());
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
expect(updateContactInfo).toHaveBeenCalledWith("1", {
contacts: [
{ id: 31, name: "Jane", phone: "(421) 433-9999" },
- { id: 32, name: "Bob", phone: "(421) 433-0033" },
+ { id: 32, name: "Bob", phone: "(421) 433-7777" },
],
notes: "Gate code 9999",
});
@@ -126,12 +132,52 @@ describe("Work order Site dialog edits the site record", () => {
pocName: "Jane",
pocPhone: "(421) 433-9999",
pocNotes: "",
- additionalContacts: [{ name: "Bob", phone: "(421) 433-0033", siteContactId: 32 }],
+ additionalContacts: [{ name: "Bob", phone: "(421) 433-7777", siteContactId: 32 }],
}),
);
expect(onOpenChange).toHaveBeenCalledWith(false);
});
+ it("a phone-only edit keeps the work order's own notes and extra contacts", async () => {
+ updateContactInfo.mockResolvedValue(undefined);
+ const { onSave } = renderDialog({
+ additionalContacts: [{ name: "Night guard", phone: "(555) 000-1111" }],
+ });
+ await screen.findByDisplayValue("Jane");
+
+ fireEvent.change(primaryField("POC phone"), { target: { value: "(421) 433-9999" } });
+ fireEvent.click(saveButton());
+
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
+ expect(updateContactInfo).toHaveBeenCalledWith(
+ "1",
+ expect.objectContaining({ notes: "Gate code 1234" }),
+ );
+ const patch = onSave.mock.calls[0][0];
+ expect(patch).toMatchObject({
+ pocName: "Jane",
+ pocPhone: "(421) 433-9999",
+ pocNotes: "Old work order note",
+ });
+ expect(patch).not.toHaveProperty("additionalContacts");
+ });
+
+ it("a notes-only edit keeps the work order's own contact override", async () => {
+ updateContactInfo.mockResolvedValue(undefined);
+ const { onSave } = renderDialog();
+ await screen.findByDisplayValue("Jane");
+
+ 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: "Someone Else",
+ pocPhone: "(000) 000-0000",
+ pocNotes: "",
+ });
+ });
+
it("keeps the dialog open and the work order untouched when the site update fails", async () => {
updateContactInfo.mockRejectedValue(new Error("Forbidden"));
const { onSave, onOpenChange } = renderDialog();