import { useState, useEffect, useCallback, useRef } from "react"; import { Spinner } from "../../../../../components/ui/spinner"; import { CheckboxControl } from "../../../../../components/ui/checkbox-control"; import { RadioControl } from "../../../../../components/ui/radio-control"; import { Scrollable } from "../../../../../components/ui/scrollable"; import { useLocation as useRouterLocation, useNavigate, useParams, } from "react-router-dom"; import { DismissibleAlert } from '../../../../../components/ui/dismissible-alert'; import { Flex, FlexItem } from '../../../../../components/ui/flex'; import { Text } from '../../../../../components/ui/text'; import { Spacer } from '../../../../../components/ui/spacer'; import { Modal } from '../../../../../components/ui/wp-modal-shim'; import { useForm, FormProvider } from "react-hook-form"; import { FileText, SquarePen, Trash2 } from 'lucide-react'; import { AppHeader, AppSpinner, ProfileLayoutContainer, HistoryNavigator, FhirLocation, operationStatusToast } from "../../../../../components"; import { IValueSet, useAuthServiceContext, useLocationServiceContext, useOrganizationServiceContext, useValueSetServiceContext, } from "../../../../../services"; import { Address, ContactPoint, Location, Organization } from "fhir/r4"; import { useFhirLocationTargetedBroadcast, useRouteAuthorization, } from "../../../../../hooks"; import { normalizeFhirAddress } from "../../../../../utils/fhirAddress"; import { PRACTICE_TYPE_SYSTEM } from "../../../../../utils/staffRoleCatalog"; import { usePracticeTypeOptions } from "../../../../../config/serviceCategories"; import { toast } from "sonner"; import { describeWriteFailure, ownFailureSentence } from '../../../../../utils/fetchFailureMessage'; import { useEditBaseline } from "../../../../../hooks/useEditBaseline"; import { isSaveRetired, isSaveTimedOut, outcomeUnknown, useSaveScope, type SaveOperation } from "../../../../../utils/saveOperation"; import { reloadBaseline, saveOverBaseline } from "../../../../../utils/saveOverBaseline"; import { StaleRecordNotice } from "../../../../../components/StaleRecordNotice/StaleRecordNotice"; import { olderThan } from "../../../../../utils/versionOrder"; import { trimmedOrUndefined } from '../../../../../utils/requiredText'; /** The primary change one save decided, held until each part is done: never worked out again from what is live. */ interface Companions { /** Make this address the practice's primary, only while its primary is still `basis`. */ promote?: { address: Address; telecom?: ContactPoint[]; basis: string | undefined }; /** The old primary location to remove, once this one is primary. */ remove?: Location; } interface EditLocationPageProps { presentation?: "page" | "modal"; locationId?: string; initialLocation?: Location | null; initialSetAsPrimaryAddress?: boolean; onRequestClose?: () => void; onSaved?: (location: Location) => void; onDeleted?: (location: Location) => void; } const EditLocationPage = ({ presentation = "page", locationId: locationIdProp, initialLocation, initialSetAsPrimaryAddress, onRequestClose, onSaved, onDeleted, }: EditLocationPageProps = {}) => { const { locationId: routeLocationId } = useParams<{ locationId: string }>(); const locationId = locationIdProp ?? routeLocationId ?? initialLocation?.id ?? "new"; const isModal = presentation === "modal"; const navigate = useNavigate(); const routerLocation = useRouterLocation(); const { wpOrganization, setWpOrganization } = useAuthServiceContext(); const { fetchLocation, createLocation, updateLocation, deleteLocation, transformToFhirLocation, transformFromFhirLocation, locationsList, setLocationsList, } = useLocationServiceContext(); const { daysOfWeek } = useValueSetServiceContext(); const { updateOrganization, transformFromFhirOrganization, transformToFhirOrganization } = useOrganizationServiceContext(); const practiceTypeOptions = usePracticeTypeOptions(); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const [location, setLocation] = useState( initialLocation ?? undefined, ); const [showDeleteModal, setShowDeleteModal] = useState(false); const [setAsPrimaryAddress, setSetAsPrimaryAddress] = useState(false); const [existingPrimaryHandling, setExistingPrimaryHandling] = useState< "secondary" | "delete" >("secondary"); const orgPracticeTypeCodes = (wpOrganization.current?.type ?? []) .flatMap((t) => t.coding ?? []) .filter((c) => c.system === PRACTICE_TYPE_SYSTEM && c.code) .map((c) => c.code as string); // A location created here stays open as that location from the moment the create is answered - before anything // that goes with it - so no later failure, timeout or retry creates it a second time. const [createdLocationId, setCreatedLocationId] = useState(); const [reloading, setReloading] = useState(false); const [loadGeneration, setLoadGeneration] = useState(0); // A create whose answer never came may exist already: not offered again, so it is never made twice. const [createUnconfirmed, setCreateUnconfirmed] = useState(false); // Something that went with a saved location may or may not have landed (no answer came): nothing more is saved from // this page until it is reloaded and what is saved is checked - the held command is not replayed blind. const [companionsUnconfirmed, setCompanionsUnconfirmed] = useState(false); const openId = createdLocationId ?? locationId; const isNew = openId === "new"; const spacerMargin = "24px"; const hasAlternativeLocation = locationsList.some( (existingLocation) => existingLocation.id !== location?.id, ); const deleteDisabled = submitting || !hasAlternativeLocation; // The hand-rolled disabled appearance that used to live here (a bespoke grey background/border/text // set) is gone: `.btn:disabled` in index.css is the house treatment, and the native `disabled` // attribute now drives it. Dropping it makes this button's disabled state match every other // converted button rather than being the one that greys out differently. // Gate on the SAME section + level as the route (`RequireStaffPermission section="organization" manage`), // not on a fixed workspace list. The old [ADMIN, GENERIC] list contradicted that route: an org that // delegated `organization: manage` to any other workspace got a route that admitted the user and a // component that redirected them to '/' (; Spencer 2026-08-10). Admin still passes - it holds // `manage` on every section by construction. useRouteAuthorization({ section: 'organization', manage: true, }); const methods = useForm({ defaultValues: { status: "", name: "", alias: [""], description: "", mode: "", type: [], telecom: [], address: [], physicalType: null, position: { longitude: "", latitude: "", altitude: "", }, managingOrganization: null, partOf: null, hoursOfOperation: daysOfWeek.map((dayOfWeek: IValueSet) => ({ available: false, daysOfWeek: [dayOfWeek.code], allDay: false, openingTime: "09:00", closingTime: "09:00", })), availabilityExceptions: "", identifier: [], }, }); const { handleSubmit, reset, watch } = methods; const routerState = routerLocation.state as { setAsPrimaryAddress?: boolean; } | null; const requestedPrimarySelection = initialSetAsPrimaryAddress ?? (!isModal ? routerState?.setAsPrimaryAddress : undefined); const hasPrimaryOrganizationLocation = !!normalizeFhirAddress( wpOrganization.current?.address?.[0], ); const primaryFor = useCallback( (location: Location): boolean => { const normalizedOrganizationAddress = normalizeFhirAddress( wpOrganization.current?.address?.[0], ); const normalizedLocationAddress = normalizeFhirAddress(location.address); const locationIsCurrentPrimary = !!normalizedOrganizationAddress && normalizedOrganizationAddress === normalizedLocationAddress; return !hasPrimaryOrganizationLocation || locationIsCurrentPrimary || !!requestedPrimarySelection; }, [requestedPrimarySelection, hasPrimaryOrganizationLocation], ); // THE PRIMARY CHOICE IS PART OF THE DRAFT. It lives outside the form library, so it is carried beside the form's // values: choosing primary alone is a change, survives a refresh, and is reviewed like any other field. type LocationDraft = ReturnType & { primaryChoice: { setAsPrimary: boolean; handling: "secondary" | "delete" } }; const primaryState = useRef({ setAsPrimary: setAsPrimaryAddress, handling: existingPrimaryHandling }); primaryState.current = { setAsPrimary: setAsPrimaryAddress, handling: existingPrimaryHandling }; const formOf = useCallback( (record: Location): LocationDraft => ({ ...transformFromFhirLocation(record), primaryChoice: { setAsPrimary: primaryFor(record), handling: "secondary" }, }), [transformFromFhirLocation, primaryFor], ); const read = useCallback( (): LocationDraft => ({ ...(methods.getValues() as LocationDraft), primaryChoice: { ...primaryState.current } }), [methods], ); const write = useCallback( (values: LocationDraft) => { const { primaryChoice, ...form } = values; reset(form); primaryState.current = { ...primaryChoice }; setSetAsPrimaryAddress(primaryChoice.setAsPrimary); setExistingPrimaryHandling(primaryChoice.handling); }, [reset], ); // THE PRACTICE'S PRIMARY ADDRESS THE PRIMARY DECISION WAS TAKEN AGAINST: captured when the form opens on a record, // when a blank editor opens, and when the person ticks "primary". Making this location primary is sent only while // the practice's primary is still that; if someone else changed it since, the decision is not sent - it is said. const primaryAtOpening = useRef(); const capturePrimaryBasis = useCallback(() => { primaryAtOpening.current = normalizeFhirAddress(wpOrganization.current?.address?.[0]); }, [wpOrganization]); const onFill = useCallback((record: Location) => { setLocation(record); capturePrimaryBasis(); }, [capturePrimaryBasis]); // The version the form was opened on, and the only one it saves against. A newer one arriving while the form holds // changes is never filled over them: the opening is marked changed, and Reload record is the way on. const baseline = useEditBaseline({ formOf, read, write, onFill }); // WHAT AN EARLIER SAVE OF THIS LOCATION LEFT UNFINISHED: the exact primary change and the exact old location to // remove, as decided then. Save again finishes these - it never works them out again from what is live now. const outstanding = useRef(); const loadInFlight = useRef(); // WHICH LOCATION THIS PAGE IS FOR. The route reuses this component for another location, so on a change of // location (or of the person signed in, or their practice) everything belonging to the last one - its record, form, // baseline, loads, saves, companion writes and busy flags - is retired, and nothing can be saved until the new one // is loaded and opened. A late answer for the last one lands nowhere, even after A -> B -> A. const saveScope = useSaveScope(locationId, () => { baseline.close(); setLocation(undefined); setCreatedLocationId(undefined); setError(undefined); setSuccess(undefined); setLoading(false); setSubmitting(false); setReloading(false); setCreateUnconfirmed(false); setCompanionsUnconfirmed(false); outstanding.current = undefined; // The location is loaded again for whoever is signed in now. setLoadGeneration((generation) => generation + 1); }); const owned = isNew || (location?.id === openId && baseline.opening?.id === openId); useEffect(() => { if (initialLocation) { baseline.arrive(initialLocation); } }, [initialLocation, baseline.arrive]); useFhirLocationTargetedBroadcast({ location, locationId, onUpdate: baseline.arrive, onDelete: () => { if (isModal) { if (location) { onDeleted?.(location); } onRequestClose?.(); return; } navigate("..", { relative: "path" }); }, }); useEffect(() => { if (!hasPrimaryOrganizationLocation) { setSetAsPrimaryAddress(true); return; } if (isNew && requestedPrimarySelection) { setSetAsPrimaryAddress(true); } }, [isNew, requestedPrimarySelection, hasPrimaryOrganizationLocation]); // A blank editor has no record to fill from, so its primary decision is taken against the practice as it opens. useEffect(() => { if (isNew) capturePrimaryBasis(); }, [isNew, locationId, capturePrimaryBasis]); // Installed here and in the shared list only when not older than what is held: a newer arrival while this save's // answer was on its way stays. const install = (saved: Location) => { setLocation((held) => (held?.id === saved.id && olderThan(saved, held) ? held : saved)); setLocationsList((currentLocations) => { const held = currentLocations.find((currentLocation) => currentLocation.id === saved.id); if (held && olderThan(saved, held)) return currentLocations; return [...currentLocations.filter((currentLocation) => currentLocation.id !== saved.id), saved]; }); }; const normalizeAddress = (address?: Address | null): string => normalizeFhirAddress(address); const getFormPrimaryAddress = (data: any): Address | null => { const formAddress = data?.address?.[0]; if (!formAddress) { return null; } // What is STORED is the trimmed value. The line filter above already dropped // whitespace-only lines but kept the padding on the ones it kept, so " Main St " reached the // wire with its spaces and every duplicate-address comparison below saw a different string from // the same address typed without them. return { line: (formAddress.line ?? []) .map((line: string) => trimmedOrUndefined(line)) .filter((line: string | undefined): line is string => line !== undefined), city: trimmedOrUndefined(formAddress.city) ?? "", state: formAddress.state ?? "", postalCode: trimmedOrUndefined(formAddress.postalCode) ?? "", country: trimmedOrUndefined(formAddress.country) ?? "US", }; }; const getDuplicateLocationAddressMessage = ( data: any, ): string | undefined => { const submittedAddress = getFormPrimaryAddress(data); const normalizedSubmittedAddress = normalizeAddress(submittedAddress); if (!normalizedSubmittedAddress) { return undefined; } const duplicateLocation = locationsList.find( (existingLocation) => existingLocation.id !== location?.id && normalizeAddress(existingLocation.address) === normalizedSubmittedAddress, ); if (duplicateLocation) { return `This address already exists for ${duplicateLocation.name || "another location"}.`; } return undefined; }; const getExistingPrimaryLocation = ( submittedAddress?: Address | null, ): Location | undefined => { const currentPrimaryAddress = wpOrganization.current?.address?.[0]; const normalizedCurrentPrimaryAddress = normalizeAddress( currentPrimaryAddress, ); if (!normalizedCurrentPrimaryAddress) { return undefined; } const normalizedSubmittedAddress = normalizeAddress(submittedAddress); return locationsList.find( (existingLocation) => existingLocation.id !== location?.id && normalizeAddress(existingLocation.address) === normalizedCurrentPrimaryAddress && normalizeAddress(existingLocation.address) !== normalizedSubmittedAddress, ); }; const shouldShowPrimaryReplacementOptions = ( submittedAddress?: Address | null, ): boolean => { const currentPrimaryAddress = wpOrganization.current?.address?.[0]; const normalizedCurrentPrimaryAddress = normalizeAddress( currentPrimaryAddress, ); const normalizedSubmittedAddress = normalizeAddress(submittedAddress); return ( !!setAsPrimaryAddress && !!normalizedCurrentPrimaryAddress && !!normalizedSubmittedAddress && normalizedCurrentPrimaryAddress !== normalizedSubmittedAddress ); }; const hasContactValue = (contactPoint: ContactPoint): boolean => typeof contactPoint.value === "string" && contactPoint.value.trim() !== ""; const mergeMissingPrimaryContacts = ( organizationTelecom: ContactPoint[] = [], locationTelecom: ContactPoint[] = [], ): ContactPoint[] => { let mergedTelecom = [...organizationTelecom]; (["phone", "email"] as const).forEach((system) => { const organizationAlreadyHasContact = mergedTelecom.some( (contactPoint) => contactPoint.system === system && hasContactValue(contactPoint), ); if (organizationAlreadyHasContact) { return; } const locationContact = locationTelecom.find( (contactPoint) => contactPoint.system === system && hasContactValue(contactPoint), ); if (!locationContact) { return; } mergedTelecom = mergedTelecom.filter( (contactPoint) => contactPoint.system !== system || hasContactValue(contactPoint), ); mergedTelecom.push({ system, use: "work", value: locationContact.value?.trim(), rank: 1, }); }); return mergedTelecom; }; const promoteSubmittedAddressToPrimary = async ( submittedAddress: Address, locationTelecom: ContactPoint[] = [], operation: SaveOperation, ): Promise => { const org = wpOrganization.current; if (!org?.id) { return; } const normalizedSubmittedAddress = normalizeAddress(submittedAddress); if (!normalizedSubmittedAddress) { setError( "A location address is required before it can be set as the primary organization address.", ); throw new Error( "A location address is required before it can be set as the primary organization address.", ); } const primaryAddress: Address = { ...submittedAddress, use: "work", type: submittedAddress.type ?? "physical", country: submittedAddress.country || "US", }; const mergedTelecom = mergeMissingPrimaryContacts( org.telecom, locationTelecom, ); const updatedOrganization: Organization = { ...org, address: [primaryAddress, ...(org.address ?? []).slice(1)], ...(mergedTelecom.length > 0 && { telecom: mergedTelecom }), }; const result = await operation.step(() => updateOrganization(updatedOrganization, operation.transport), true); const organizationForState: Organization = { ...updatedOrganization, ...result, address: updatedOrganization.address, telecom: updatedOrganization.telecom, }; setWpOrganization(organizationForState); }; useEffect(() => { if (isNew || location) { return; } const foundLocation = locationsList?.find((l) => l.id === locationId); // Arrives rather than opens: if something newer is already open, an older copy changes nothing. if (foundLocation) { baseline.arrive(foundLocation); return; } // One read at a time for this location and person: a read still under way is not started again. if (loadInFlight.current?.belongs()) return; // The read, its answer, its error and the spinner all belong to the location and the person signed in as it // starts; either changing (A -> B -> A included) retires it, and a late answer lands nowhere. const operation = saveScope.start(); loadInFlight.current = operation; setLoading(true); operation.step(() => fetchLocation(locationId as string, false, operation.transport)) .then((fetchedLocation) => baseline.arrive(fetchedLocation)) .catch((err: unknown) => { if (!operation.belongs()) return; console.error("Error loading location:", err); setError("Could not load location. Try again."); }) .finally(() => { if (loadInFlight.current === operation) loadInFlight.current = undefined; if (operation.belongs()) setLoading(false); }); }, [location, locationsList, locationId, fetchLocation, baseline.arrive, saveScope, loadGeneration]); // Reload record: what is saved now, read fresh, becomes the opening; the draft is discarded (the notice says so). const reload = async () => { const id = baseline.opening?.id; if (!id) return; const operation = saveScope.start(); setReloading(true); setError(undefined); setSuccess(undefined); try { await reloadBaseline({ baseline, operation, readNow: (transport) => fetchLocation(id, true, transport) }); } catch (reloadError) { if (isSaveRetired(reloadError)) return; setError("The record could not be read. Try Reload record again."); } finally { if (operation.belongs()) setReloading(false); } }; const onSubmit = async (data: any) => { if (!wpOrganization.current) { setError("Organization information is missing. Please try again."); return; } // Nothing is saved until the location this page is for is the one open, nor from an opening that is done. if (!owned || baseline.stale || createUnconfirmed || companionsUnconfirmed) return; // What an earlier save of this location left unfinished, if anything: finished as decided then. const pending = outstanding.current; const submittedAddress = getFormPrimaryAddress(data); const primaryRequiredForSubmit = primaryCheckboxChecked; if (!pending && primaryRequiredForSubmit && !normalizeAddress(submittedAddress)) { setError( "A location address is required before it can be set as the primary organization location.", ); return; } const duplicateAddressMessage = getDuplicateLocationAddressMessage(data); if (duplicateAddressMessage) { setError(duplicateAddressMessage); return; } setSubmitting(true); setSuccess(undefined); setError(undefined); const operationToastId = operationStatusToast.saving(); // The primary address this decision was taken against - read now, before the location save re-opens the form. const primaryDecidedAgainst = primaryAtOpening.current; const creating = isNew; // ONE OPERATION FOR THE WHOLE SAVE: the location and every organization write that goes with it. Closing the // editor, another location, or another person signed in retires all of it - nothing more is sent, installed // or announced for it. const operation = saveScope.start(); // The location itself is saved: from here, a failure is about what goes with it. let committed = false; try { const existingPrimaryLocation = getExistingPrimaryLocation(submittedAddress); let savedLocation: Location; if (creating) { savedLocation = await operation.step(() => createLocation( transformToFhirLocation(data, wpOrganization.current as Organization), operation.transport, ), true); // COMMITTED, AND KEPT: this page is that location now, before anything else is attempted. if (savedLocation.id) setCreatedLocationId(savedLocation.id); install(savedLocation); baseline.open(savedLocation); } else if (pending && !baseline.isDirty()) { // Nothing of the location itself to save again: only what the last save left unfinished. savedLocation = location as Location; } else { // Written first, and only over the version opened: a refused Location writes nothing else below. const outcome = await saveOverBaseline({ baseline, operation, write: (basedOn, transport) => updateLocation({ id: location?.id, ...transformToFhirLocation(data), }, basedOn, transport), }); if ("retired" in outcome) return; if ("refused" in outcome) { // A finished opening says so in its own notice; only a save that sent nothing says it here. if (!outcome.stale) setError(outcome.refused); return; } savedLocation = outcome.saved; install(savedLocation); } committed = true; // What the location was saved with, but did not follow it: said instead of a success. const notDone: string[] = []; // Sync any new location practice types back to the org. const org = wpOrganization.current; if (org) { const locationTypeCodes = (savedLocation.type ?? []) .flatMap((t) => t.coding ?? []) .filter((c) => c.system === PRACTICE_TYPE_SYSTEM && c.code) .map((c) => c.code as string); const orgTypeCodes = new Set( (org.type ?? []) .flatMap((t) => t.coding ?? []) .filter((c) => c.system === PRACTICE_TYPE_SYSTEM && c.code) .map((c) => c.code as string), ); const newCodes = locationTypeCodes.filter((code) => !orgTypeCodes.has(code)); if (newCodes.length > 0) { try { const { formData } = transformFromFhirOrganization(org); const existingOrgTypeCodings = (formData.type ?? []).filter( (t: any) => t?.system !== PRACTICE_TYPE_SYSTEM, ); const existingOrgPracticeTypeCodings = (formData.type ?? []).filter( (t: any) => t?.system === PRACTICE_TYPE_SYSTEM, ); const addedPracticeTypeCodings = newCodes.map((code) => { const option = practiceTypeOptions.find((pt) => pt.code === code); return { system: PRACTICE_TYPE_SYSTEM, code, display: option?.display || code }; }); const updatedOrg = await operation.step(() => updateOrganization({ id: org.id, ...transformToFhirOrganization({ ...formData, type: [...existingOrgTypeCodings, ...existingOrgPracticeTypeCodings, ...addedPracticeTypeCodings], }), }, operation.transport), true); setWpOrganization(updatedOrg); } catch (syncErr) { if (isSaveRetired(syncErr) || isSaveTimedOut(syncErr) || outcomeUnknown(syncErr)) throw syncErr; console.error("Failed to sync location practice types to org:", syncErr); notDone.push("its practice types were not added to your organization"); } } } // WHAT GOES WITH IT, AS ONE EXACT DECISION: make this location primary (taken against the practice's primary as // it was then), and remove the old primary location if that was chosen. Held until each part is done. const companions: Companions = pending ?? ( primaryCheckboxChecked && submittedAddress ? { promote: { address: submittedAddress, telecom: savedLocation.telecom, basis: primaryDecidedAgainst }, remove: existingPrimaryLocation && existingPrimaryHandling === "delete" ? existingPrimaryLocation : undefined, } : {} ); const remaining: Companions = {}; let promoted = !companions.promote; if (companions.promote) { // Only while the practice's primary is still what this decision was taken against. if (normalizeFhirAddress(wpOrganization.current?.address?.[0]) !== companions.promote.basis) { notDone.push("it was not made your primary location, because someone else changed the practice's primary location after you opened this - look at it, then choose again"); } else { try { await promoteSubmittedAddressToPrimary(companions.promote.address, companions.promote.telecom, operation); promoted = true; } catch (primaryErr) { if (isSaveRetired(primaryErr) || isSaveTimedOut(primaryErr) || outcomeUnknown(primaryErr)) throw primaryErr; console.error("Location saved, but making it primary failed:", primaryErr); remaining.promote = companions.promote; remaining.remove = companions.remove; notDone.push( describeWriteFailure(primaryErr, "it was not made your primary organization location") .replace(/^It/, "it"), ); } } } // The old primary is removed only once this one has become primary - and it is the location chosen then. if (promoted && companions.remove) { const oldPrimary = companions.remove; try { await operation.step(() => deleteLocation(oldPrimary, operation.transport), true); setLocationsList((currentLocations) => currentLocations.filter((currentLocation) => currentLocation.id !== oldPrimary.id), ); } catch (deleteErr) { if (isSaveRetired(deleteErr) || isSaveTimedOut(deleteErr) || outcomeUnknown(deleteErr)) throw deleteErr; console.error("Location made primary, but removing the old primary failed:", deleteErr); remaining.remove = oldPrimary; notDone.push( `it is your primary location now, but ${oldPrimary.name || "the old primary location"} was not removed`, ); } } outstanding.current = remaining.promote || remaining.remove ? remaining : undefined; if (notDone.length > 0) { // The location IS saved; the rest is not. Saying "saved successfully" here would hide the half that failed, // and closing the editor would hide this sentence. So it stays open on the saved location; Save again // finishes exactly what was left, and never creates the location a second time. operation.admit(); setError( `The location was ${creating ? "created" : "saved"}, but ${notDone.join("; and ")}.` + (outstanding.current ? " Save again to finish what was not saved." : ""), ); return; } operation.admit(); // Said from what was done, not from the checkbox: a retry finishing a held primary change says so too. const successMessage = companions.promote && promoted ? `Location ${creating ? "created" : "updated"} successfully and set as the primary organization location.` : `Location ${creating ? "created" : "updated"} successfully.`; if (isModal) { toast.success(successMessage, { position: "top-right" }); onSaved?.(savedLocation); onRequestClose?.(); return; } if (creating) { navigate(savedLocation.id ? `../${savedLocation.id}` : "..", { relative: "path", }); } setSuccess(successMessage); setTimeout(() => setSuccess(undefined), 3_000); } catch (err: any) { // Retired: nothing is said for a save that no longer belongs to what is on screen. if (isSaveRetired(err)) return; if (isSaveTimedOut(err) || outcomeUnknown(err)) { // NOT REPEATED BLIND: what may have landed is checked first. The location's own identity is kept (recorded // when its create was answered), and so is the command that went with it - but neither is sent again from // here: the page is reloaded, and what is saved is looked at. if (committed) { setCompanionsUnconfirmed(true); setError(`The location was ${creating ? "created" : "saved"}, but whether what went with it was saved could not be confirmed. Reload the page to check before trying again.`); } else if (outcomeUnknown(err)) { if (creating) setCreateUnconfirmed(true); setError(creating ? "Whether the location was created could not be confirmed. Reload the page to check before trying again." : (isSaveTimedOut(err) ? ownFailureSentence(err) : "Whether the location was saved could not be confirmed. Reload the page to check before trying again.")); } else { setError(isSaveTimedOut(err) ? ownFailureSentence(err) : describeWriteFailure(err, `Failed to ${creating ? "create" : "update"} location`)); } return; } console.error(`Error ${creating ? "creating" : "updating"} location:`, err); setError( describeWriteFailure(err, `Failed to ${creating ? "create" : "update"} location`), ); } finally { operationStatusToast.dismiss(operationToastId); if (operation.belongs()) setSubmitting(false); } }; const handleDeleteLocation = async () => { if (!location) return; if (!hasAlternativeLocation) { setShowDeleteModal(false); setError( "Add another location before deleting this one. Your practice must always have a primary location.", ); return; } // The delete belongs to this location and the person signed in: replaced while it is out, nothing more is // published, announced or closed for it. const operation = saveScope.start(); const deleting = location; setSubmitting(true); setShowDeleteModal(false); const operationToastId = operationStatusToast.deleting(); try { await operation.step(() => deleteLocation(deleting, operation.transport), true); setLocationsList((currentLocations) => currentLocations.filter( (currentLocation) => currentLocation.id !== deleting.id, ), ); if (isModal) { toast.success("Location deleted successfully.", { position: "top-right" }); onDeleted?.(deleting); onRequestClose?.(); } } catch (error) { if (isSaveRetired(error)) return; console.error("Failed to delete location:", error); // A refusal (staff with no other site) repeats on retry; the server's sentence names the fix. setError(isSaveTimedOut(error) ? ownFailureSentence(error) : describeWriteFailure(error, "Failed to delete location")); } finally { operationStatusToast.dismiss(operationToastId); if (operation.belongs()) setSubmitting(false); } }; const modalTitle = isNew ? "Add Location" : "Edit Location"; if (loading) { if (isModal) { return ( onRequestClose?.()} size="large" >
); } return ; } const submittedAddress = getFormPrimaryAddress({ address: watch("address") }); const normalizedCurrentPrimaryAddress = normalizeAddress( wpOrganization.current?.address?.[0], ); const normalizedSavedLocationAddress = normalizeAddress(location?.address); const isCurrentPrimaryLocation = !!normalizedCurrentPrimaryAddress && normalizedCurrentPrimaryAddress === normalizedSavedLocationAddress; const primarySelectionRequired = !hasPrimaryOrganizationLocation || !hasAlternativeLocation; const primarySelectionLocked = primarySelectionRequired || isCurrentPrimaryLocation; const primaryCheckboxChecked = primarySelectionLocked || setAsPrimaryAddress; const primarySelectionHelp = !hasPrimaryOrganizationLocation ? "You must set a primary location first." : !hasAlternativeLocation ? "An organization must always have a primary location." : isCurrentPrimaryLocation ? "This is the current primary organization location." : undefined; const showPrimaryReplacementOptions = shouldShowPrimaryReplacementOptions(submittedAddress); const editorFields = (
{ if (primarySelectionLocked) { setSetAsPrimaryAddress(true); return; } // Ticked: the decision is taken against the practice's primary as it is now, on screen. if (checked) capturePrimaryBasis(); setSetAsPrimaryAddress(!!checked); }} disabled={submitting || primarySelectionLocked} /> {showPrimaryReplacementOptions && (
setExistingPrimaryHandling(value as "secondary" | "delete") } />
)}
); const editorNotices = ( <> {success && ( <> setSuccess(undefined)}> {success} )} {error && ( <> setError(undefined)}>
{error}
)} void reload()} reloading={reloading} /> ); const deleteConfirmation = showDeleteModal ? ( setShowDeleteModal(false)} > Are you sure you want to delete the location "{location?.name}"? This action cannot be undone. ) : null; return ( resourceType="Location" resourceId={openId || ""} > {(showHistory) => ( {isModal ? ( { if (!submitting) { onRequestClose?.(); } }} size="large" headerActions={ {!isNew && ( )} } >
{editorNotices} {editorFields}
{deleteConfirmation}
) : (
{!isNew && ( )} {!isNew && ( )} {editorNotices} {editorFields} {deleteConfirmation}
)}
)} ); }; export default EditLocationPage;