mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
Compare commits
2 commits
ad500753e1
...
c0e484275d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0e484275d | ||
|
|
7efc9e8914 |
6 changed files with 88 additions and 12 deletions
|
|
@ -10,12 +10,23 @@ type SiteDialogPocSectionProps = {
|
|||
primaryRequired?: boolean;
|
||||
/** Edits go to the site record rather than only this work order. */
|
||||
editsSiteRecord?: boolean;
|
||||
/** The site record did not load, so Save writes this work order only. */
|
||||
siteRecordUnavailable?: boolean;
|
||||
onPocNameChange: (value: string) => void;
|
||||
onPocPhoneChange: (value: string) => void;
|
||||
onPocNotesChange: (value: string) => void;
|
||||
onAdditionalContactsChange: (contacts: WorkOrderAdditionalContact[]) => void;
|
||||
};
|
||||
|
||||
function helperText(editsSiteRecord: boolean, siteRecordUnavailable: boolean): string {
|
||||
if (!editsSiteRecord) {
|
||||
return "Auto-filled from site records when empty. Additional contacts persist on save.";
|
||||
}
|
||||
return siteRecordUnavailable
|
||||
? "Site record unavailable — changes apply to this work order only."
|
||||
: "Contacts and notes are saved to the site record and apply to all its work orders.";
|
||||
}
|
||||
|
||||
export function SiteDialogPocSection({
|
||||
pocName,
|
||||
pocPhone,
|
||||
|
|
@ -24,6 +35,7 @@ export function SiteDialogPocSection({
|
|||
showPrimaryErrors = false,
|
||||
primaryRequired = false,
|
||||
editsSiteRecord = false,
|
||||
siteRecordUnavailable = false,
|
||||
onPocNameChange,
|
||||
onPocPhoneChange,
|
||||
onPocNotesChange,
|
||||
|
|
@ -41,11 +53,7 @@ export function SiteDialogPocSection({
|
|||
onPocPhoneChange={onPocPhoneChange}
|
||||
onPocNotesChange={onPocNotesChange}
|
||||
onAdditionalContactsChange={onAdditionalContactsChange}
|
||||
helperText={
|
||||
editsSiteRecord
|
||||
? "Contacts and notes are saved to the site record and apply to all its work orders."
|
||||
: "Auto-filled from site records when empty. Additional contacts persist on save."
|
||||
}
|
||||
helperText={helperText(editsSiteRecord, siteRecordUnavailable)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ export function SiteDialog({
|
|||
showPrimaryErrors={s.requiresPoc && s.showErrors}
|
||||
primaryRequired={s.requiresPoc}
|
||||
editsSiteRecord={s.editsSiteRecord}
|
||||
siteRecordUnavailable={s.siteRecordUnavailable}
|
||||
onPocNameChange={s.setPn}
|
||||
onPocPhoneChange={s.setPp}
|
||||
onPocNotesChange={s.setNotes}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ export function useSiteDialogFormFields({
|
|||
const [pocFilledFor, setPocFilledFor] = useState("");
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [contactsDirty, setContactsDirty] = useState(false);
|
||||
// The user typed into the contact fields since the last reset or site switch.
|
||||
const [userEdited, setUserEdited] = useState(false);
|
||||
const [baselineHadContacts, setBaselineHadContacts] = useState(
|
||||
() => additionalContacts.length > 0,
|
||||
);
|
||||
|
|
@ -62,6 +64,7 @@ export function useSiteDialogFormFields({
|
|||
if (!open) {
|
||||
setDirty(false);
|
||||
setContactsDirty(false);
|
||||
setUserEdited(false);
|
||||
setShowErrors(false);
|
||||
}
|
||||
}, [open]);
|
||||
|
|
@ -92,6 +95,7 @@ export function useSiteDialogFormFields({
|
|||
if (siteChanged) {
|
||||
setExtraContacts([]);
|
||||
setContactsDirty(true);
|
||||
setUserEdited(false);
|
||||
setDirty(true);
|
||||
if (createMode) {
|
||||
setPn("");
|
||||
|
|
@ -105,6 +109,7 @@ export function useSiteDialogFormFields({
|
|||
|
||||
const markPocDirty = (apply: () => void) => {
|
||||
setDirty(true);
|
||||
setUserEdited(true);
|
||||
apply();
|
||||
};
|
||||
|
||||
|
|
@ -118,6 +123,7 @@ export function useSiteDialogFormFields({
|
|||
pocFilledFor,
|
||||
dirty,
|
||||
contactsDirty,
|
||||
userEdited,
|
||||
resetVersion,
|
||||
baselineHadContacts,
|
||||
showErrors,
|
||||
|
|
@ -136,6 +142,7 @@ export function useSiteDialogFormFields({
|
|||
setNotesDirty: (next: string) => markPocDirty(() => setNotes(next)),
|
||||
setExtraContactsDirty: (contacts: WorkOrderAdditionalContact[]) => {
|
||||
setDirty(true);
|
||||
setUserEdited(true);
|
||||
setContactsDirty(true);
|
||||
setExtraContacts(contacts);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -95,6 +95,9 @@ export function useSiteDialogState({
|
|||
locationDetailError: liveLocationDetailError,
|
||||
fields,
|
||||
});
|
||||
// The site failed to load (or the user typed before it did), so Save writes this work order only.
|
||||
const siteRecordUnavailable =
|
||||
editsSiteRecord && Boolean(locId) && !siteRecord.synced && !siteRecord.loading;
|
||||
const updateSite = useUpdateSiteContactInfo();
|
||||
const pocMissing = !pn.trim() || !pp.trim();
|
||||
const requiresPoc = createMode || editsSiteRecord;
|
||||
|
|
@ -166,6 +169,7 @@ export function useSiteDialogState({
|
|||
saving: updateSite.isPending,
|
||||
siteRecordLoading: siteRecord.loading,
|
||||
editsSiteRecord,
|
||||
siteRecordUnavailable,
|
||||
requiresPoc,
|
||||
setPn: fields.setPnDirty,
|
||||
setPp: fields.setPpDirty,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ type SiteRecordFields = {
|
|||
pp: string;
|
||||
notes: string;
|
||||
extraContacts: WorkOrderAdditionalContact[];
|
||||
/** The user typed into the contact fields since the last reset or site switch. */
|
||||
userEdited: boolean;
|
||||
resetVersion: number;
|
||||
setPn: (value: string) => void;
|
||||
setPp: (value: string) => void;
|
||||
|
|
@ -105,7 +107,7 @@ export function useSiteRecordSync({
|
|||
const [syncedFor, setSyncedFor] = useState("");
|
||||
const [primaryId, setPrimaryId] = useState<number | undefined>(undefined);
|
||||
const [baseline, setBaseline] = useState<SiteContactInfoInput | null>(null);
|
||||
const { resetVersion, setPn, setPp, setNotes, setExtraContacts } = fields;
|
||||
const { userEdited, resetVersion, setPn, setPp, setNotes, setExtraContacts } = fields;
|
||||
|
||||
useEffect(() => {
|
||||
setSyncedFor("");
|
||||
|
|
@ -113,7 +115,9 @@ export function useSiteRecordSync({
|
|||
|
||||
useEffect(() => {
|
||||
const detailMatches = locationDetail !== undefined && String(locationDetail.id) === locId;
|
||||
if (!enabled || !open || !locId || !detailMatches || syncedFor === locId) {
|
||||
// Input typed while the site was unavailable (a failed load) is never replaced by a late
|
||||
// response: the dialog stays on the work order's values and saves them to the work order only.
|
||||
if (!enabled || !open || !locId || !detailMatches || syncedFor === locId || userEdited) {
|
||||
return;
|
||||
}
|
||||
const [main, ...others] = locationDetail.contacts ?? [];
|
||||
|
|
@ -137,15 +141,27 @@ export function useSiteRecordSync({
|
|||
}),
|
||||
);
|
||||
setSyncedFor(locId);
|
||||
}, [enabled, open, locId, locationDetail, syncedFor, setPn, setPp, setNotes, setExtraContacts]);
|
||||
}, [
|
||||
enabled,
|
||||
open,
|
||||
locId,
|
||||
locationDetail,
|
||||
syncedFor,
|
||||
userEdited,
|
||||
setPn,
|
||||
setPp,
|
||||
setNotes,
|
||||
setExtraContacts,
|
||||
]);
|
||||
|
||||
const request = toRequest(primaryId, fields);
|
||||
const synced = enabled && syncedFor !== "" && syncedFor === locId;
|
||||
|
||||
return {
|
||||
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,
|
||||
// Until the site loads the fields do not reflect it; a failed load falls back to the work order,
|
||||
// which it keeps once the user has typed, even if a later refetch succeeds.
|
||||
loading: enabled && Boolean(locId) && !synced && !locationDetailError && !userEdited,
|
||||
siteChanged: synced && baseline !== null && fingerprint(request) !== fingerprint(baseline),
|
||||
edits: diffEdits(synced ? baseline : null, request),
|
||||
request,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ 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 { renderWithProviders } from "@/test/test-utils";
|
||||
|
||||
const getById = vi.fn();
|
||||
|
|
@ -33,10 +34,14 @@ const dallas: Location = {
|
|||
|
||||
const sites = [{ id: "1", name: "DAL1" }];
|
||||
|
||||
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<Parameters<typeof SiteDialog>[0]> = {}) {
|
||||
const onSave = vi.fn();
|
||||
const onOpenChange = vi.fn();
|
||||
renderWithProviders(
|
||||
const { queryClient } = renderWithProviders(
|
||||
<SiteDialog
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
|
|
@ -51,7 +56,7 @@ function renderDialog(overrides: Partial<Parameters<typeof SiteDialog>[0]> = {})
|
|||
/>,
|
||||
{ withAuth: false },
|
||||
);
|
||||
return { onSave, onOpenChange };
|
||||
return { onSave, onOpenChange, queryClient };
|
||||
}
|
||||
|
||||
/** The main contact's field; additional contact rows reuse the same placeholders after it. */
|
||||
|
|
@ -86,6 +91,8 @@ describe("Work order Site dialog edits the site record", () => {
|
|||
expect(screen.getByTitle("Site location map")).toBeInTheDocument();
|
||||
expect(screen.getByText("(214) 555-0100")).toBeInTheDocument();
|
||||
expect(screen.getByText("dal1@acme.test")).toBeInTheDocument();
|
||||
expect(screen.getByText(SITE_RECORD_COPY)).toBeInTheDocument();
|
||||
expect(screen.queryByText(SITE_UNAVAILABLE_COPY)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("locks the contact fields until the site loads, so typed input is never replaced", async () => {
|
||||
|
|
@ -111,6 +118,39 @@ describe("Work order Site dialog edits the site record", () => {
|
|||
expect(saveButton()).toBeEnabled();
|
||||
});
|
||||
|
||||
it("keeps a POC typed after a failed site load when a later refetch succeeds", async () => {
|
||||
getById.mockRejectedValueOnce(new Error("Network error"));
|
||||
updateContactInfo.mockResolvedValue(undefined);
|
||||
const { onSave, queryClient } = renderDialog();
|
||||
|
||||
await waitFor(() => expect(primaryField("POC name")).toBeEnabled());
|
||||
expect(screen.getByText(SITE_UNAVAILABLE_COPY)).toBeInTheDocument();
|
||||
expect(screen.queryByText(SITE_RECORD_COPY)).not.toBeInTheDocument();
|
||||
fireEvent.change(primaryField("POC name"), { target: { value: "John" } });
|
||||
|
||||
await queryClient.refetchQueries({ queryKey: queryKeys.locations.detail("1") });
|
||||
await waitFor(() => expect(getById).toHaveBeenCalledTimes(2));
|
||||
await screen.findByText("(214) 555-0100");
|
||||
|
||||
// The typed input stays on the work order, so the copy still says so after the refetch.
|
||||
expect(screen.getByText(SITE_UNAVAILABLE_COPY)).toBeInTheDocument();
|
||||
expect(screen.queryByText(SITE_RECORD_COPY)).not.toBeInTheDocument();
|
||||
expect(primaryField("POC name")).toHaveValue("John");
|
||||
expect(primaryField("POC name")).toBeEnabled();
|
||||
expect(primaryField("POC phone")).toHaveValue("(000) 000-0000");
|
||||
expect(screen.queryByDisplayValue("Jane")).not.toBeInTheDocument();
|
||||
fireEvent.click(saveButton());
|
||||
|
||||
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
|
||||
expect(onSave.mock.calls[0][0]).toMatchObject({
|
||||
pocName: "John",
|
||||
pocPhone: "(000) 000-0000",
|
||||
pocNotes: "Old work order note",
|
||||
});
|
||||
// The site's notes and other contacts were never shown, so they are not written back.
|
||||
expect(updateContactInfo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps Save disabled until something changes", async () => {
|
||||
renderDialog();
|
||||
await screen.findByDisplayValue("Jane");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue