mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 04:33:11 +00:00
fix(workorders): only edited Site dialog fields follow the site record
This commit is contained in:
parent
bea3540409
commit
cccdeceb10
6 changed files with 128 additions and 27 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
className="space-y-1.5 rounded-lg p-3"
|
||||
|
|
@ -58,9 +61,7 @@ export function SiteDialogDetailPanel({
|
|||
</a>
|
||||
)}
|
||||
{!addressPreview && <Detail icon={MapPin} text={fallbackLabel} />}
|
||||
{Boolean(locationDetail.phone) && (
|
||||
<Detail icon={Phone} text={locationDetail.phone ?? ""} />
|
||||
)}
|
||||
{Boolean(sitePhone) && <Detail icon={Phone} text={sitePhone} />}
|
||||
{Boolean(locationDetail.contactEmail) && (
|
||||
<Detail icon={Mail} text={locationDetail.contactEmail ?? ""} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<SiteRecordFields, "pn" | "pp" | "notes" | "extraContacts">,
|
||||
|
|
@ -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<number | undefined>(undefined);
|
||||
const [baseline, setBaseline] = useState("");
|
||||
const [baseline, setBaseline] = useState<SiteContactInfoInput | null>(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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue