import { useState } from "react"; import { useDirtyGuard } from "../../../../hooks/useDirtyReport"; import { Location } from "fhir/r4"; import { FhirLocationDisplay } from "../../../../components"; import { formatFhirCodeableConcept } from "../../../../utils/fhirCodeableConcept.tsx"; import { formatSingleAddress, normalizeFhirAddress, } from "../../../../utils/fhirAddress.tsx"; import { onlyRooms, onlySites, onlyVirtual, getRoomSiteId, } from "../../../../utils/roomLocation"; import { useFhirLocationBroadcast, useFhirLocationTargetedBroadcast, useFhirLocations, } from "../../../../hooks"; import { mayMissRecentWrite } from "../../../../utils/recentWritesUnresolved"; import { useAuthServiceContext, useLocationServiceContext, useStaffPermissionServiceContext, } from "../../../../services"; import { ChevronRight, MapPin, Pencil, Trash2, DoorOpen, Plus, Video } from 'lucide-react'; import { Sheet, SheetContent, SheetTitle } from "../../../../components/ui/sheet"; import { Skeleton } from "../../../../components/ui/skeleton.tsx"; import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, } from "../../../../components/ui/alert-dialog"; import EditLocationPage from "./LocationPage/LocationPage"; import AddLocationForm from "./AddLocationForm"; import { OverlayFooter, OverlayFooterHost } from "../../../../components/ui/overlay-actions"; import DiscardChangesDialog from "../../../../components/DiscardChangesDialog/DiscardChangesDialog"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "../../../../components/ui/dialog"; import RoomModal from "../../../../modals/RoomModal"; import { PracticeRoomingSwitch } from "./PracticeRoomingSwitch"; import VirtualLocationModal from "../../../../modals/VirtualLocationModal"; const LocationsPage = () => { const { userRole, USER_ROLES, wpOrganization } = useAuthServiceContext(); const { canManage } = useStaffPermissionServiceContext(); const { deleteLocation, setLocationsList } = useLocationServiceContext(); const { loading, error, locationsList, locationsBundle, loadLocations } = useFhirLocations(); // The list may be missing a location saved moments ago: it is shown, but never as complete or as "none". const mayBeIncomplete = !loading && !error && mayMissRecentWrite(locationsBundle); const [selectedLocation, setSelectedLocation] = useState( null, ); const [locationEditor, setLocationEditor] = useState<{ resourceId: string; location?: Location | null; setAsPrimaryAddress?: boolean; } | null>(null); const [addingLocation, setAddingLocation] = useState<{ makePrimary: boolean } | null>(null); const addLocationDirty = useDirtyGuard(); const [confirmDiscardAdd, setConfirmDiscardAdd] = useState(false); const [roomEditor, setRoomEditor] = useState<{ room?: Location } | null>( null, ); const [deletingRoom, setDeletingRoom] = useState(); const [deletingRoomBusy, setDeletingRoomBusy] = useState(false); const [deleteRoomError, setDeleteRoomError] = useState< string | undefined >(); const [virtualEditor, setVirtualEditor] = useState<{ virtualLocation?: Location; } | null>(null); const [deletingVirtual, setDeletingVirtual] = useState(); const [deletingVirtualBusy, setDeletingVirtualBusy] = useState(false); const [deleteVirtualError, setDeleteVirtualError] = useState< string | undefined >(); const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes( userRole as string, ); // Three DISJOINT buckets, one per section below. `sites` is an allowlist, so a virtual location is // excluded from it (and from every booking picker) - which is exactly why `virtualLocations` has to // exist: without its own section it would be in no bucket and invisible on the only screen that // manages it. Pinned by the "exactly one bucket" test in utils/__tests__/roomLocation.test.ts. const rooms = onlyRooms(locationsList); const sites = onlySites(locationsList); const virtualLocations = onlyVirtual(locationsList); // Rooms whose `partOf` is missing or points at a site that no longer exists - grouped under // "Unassigned" instead of silently dropped. const unassignedRooms = rooms.filter((room) => { const siteId = getRoomSiteId(room); return !siteId || !sites.some((site) => site.id === siteId); }); const primaryAddress = wpOrganization.current?.address?.[0]; const primaryAddressDisplay = primaryAddress ? formatSingleAddress(primaryAddress) : ""; const normalizedPrimaryAddress = normalizeFhirAddress(primaryAddress); const primaryLocation = normalizedPrimaryAddress ? locationsList.find( (location: Location) => normalizeFhirAddress(location.address) === normalizedPrimaryAddress, ) : undefined; const secondaryLocationsList = sites.filter( (location: Location) => !normalizedPrimaryAddress || normalizeFhirAddress(location.address) !== normalizedPrimaryAddress, ); useFhirLocationBroadcast(); useFhirLocationTargetedBroadcast({ location: selectedLocation || undefined, isOpen: !!selectedLocation, onUpdate: (updatedLocation) => { setSelectedLocation(updatedLocation); }, onDelete: () => { setSelectedLocation(null); }, }); const openLocationEditor = ( location?: Location | null, setAsPrimaryAddress = false, ) => { setLocationEditor({ resourceId: location?.id || "new", location, setAsPrimaryAddress, }); }; const viewLocation = (location: Location) => { if (isAuthorizedToEdit()) { openLocationEditor(location); return; } setSelectedLocation(location); }; const editLocation = (event: React.MouseEvent, location: Location) => { event.stopPropagation(); openLocationEditor(location); }; const addLocation = () => { addLocationDirty.reset(); setAddingLocation({ makePrimary: !normalizedPrimaryAddress }); }; const closeAddLocation = () => { if (addLocationDirty.isDirty()) { setConfirmDiscardAdd(true); return; } setAddingLocation(null); }; const editPrimaryLocation = (event?: React.MouseEvent) => { event?.stopPropagation(); openLocationEditor(primaryLocation ?? null, true); }; const handleLocationEditorComplete = () => { setLocationEditor(null); void loadLocations(); }; const isAuthorizedToEdit = () => canManage("organization"); const openRoomEditor = (room?: Location) => { setRoomEditor({ room }); }; const handleRoomSaved = (savedRoom: Location) => { setLocationsList((currentLocations) => { const remainingLocations = currentLocations.filter( (currentLocation) => currentLocation.id !== savedRoom.id, ); return [...remainingLocations, savedRoom]; }); setRoomEditor(null); }; const handleDeleteRoom = async () => { if (!deletingRoom) return; setDeletingRoomBusy(true); setDeleteRoomError(undefined); try { await deleteLocation(deletingRoom); setLocationsList((currentLocations) => currentLocations.filter( (currentLocation) => currentLocation.id !== deletingRoom.id, ), ); setDeletingRoom(undefined); } catch (err) { console.error("Failed to delete room:", err); setDeleteRoomError("Failed to delete this room. Please try again."); } finally { setDeletingRoomBusy(false); } }; const handleVirtualSaved = (savedVirtualLocation: Location) => { setLocationsList((currentLocations) => { const remainingLocations = currentLocations.filter( (currentLocation) => currentLocation.id !== savedVirtualLocation.id, ); return [...remainingLocations, savedVirtualLocation]; }); setVirtualEditor(null); }; const handleDeleteVirtual = async () => { if (!deletingVirtual) return; setDeletingVirtualBusy(true); setDeleteVirtualError(undefined); try { await deleteLocation(deletingVirtual); setLocationsList((currentLocations) => currentLocations.filter( (currentLocation) => currentLocation.id !== deletingVirtual.id, ), ); setDeletingVirtual(undefined); } catch (err) { console.error("Failed to delete the telehealth location:", err); setDeleteVirtualError( "Failed to delete this telehealth location. Please try again.", ); } finally { setDeletingVirtualBusy(false); } }; // Compact row shared by the per-site groups and the "Unassigned" group below - name + description on the // left, Edit/Delete icon buttons on the right (gated the same as the site cards above), mirroring // FeesCatalog's row convention (plain icon buttons, red trash icon, confirm-gated delete). const renderRoomRow = (room: Location) => (

{room.name || "Unnamed Room"}

{room.description && (

{room.description}

)}
{isAuthorizedToEdit() && (
)}
); return (

Locations

View the primary organization address and any secondary clinic locations.

{!isPatient && isAuthorizedToEdit() && ( )}
{primaryAddressDisplay ? (
isAuthorizedToEdit() && editPrimaryLocation()} onKeyDown={(event: React.KeyboardEvent) => { if (!isAuthorizedToEdit()) return; if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); editPrimaryLocation(); }} className={`group focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 rounded flex gap-3 items-center justify-between p-1 ${isAuthorizedToEdit() ? "cursor-pointer hover:bg-muted" : ""}`} >

{primaryLocation?.name || wpOrganization.current?.name || "Primary Organization Location"}

Primary
Address: {primaryAddressDisplay}
{isAuthorizedToEdit() && ( )}
) : isAuthorizedToEdit() ? ( ) : null} {mayBeIncomplete && (

This list may be missing a location saved in the last few minutes. Reload to check again.

)} {loading ? (
{[...Array(5)].map((_, i) => (
))}
) : error ? (

Could not load locations

Try again later.

{/* Was a hand-rolled Tailwind primary button (bg-primary, own padding and hover). `btn btn-blue` is the house treatment for exactly this, so the bespoke classes go — the same call as LocationPage's deleteButtonStyle. */}
) : !loading && secondaryLocationsList.length === 0 && !mayBeIncomplete ? (

No secondary locations found

Secondary locations are optional. Add one only when this organization has another clinic address.

) : ( secondaryLocationsList.length !== 0 && (
{secondaryLocationsList.map((location: Location) => { const locationType = formatFhirCodeableConcept( location.type?.[0], ); const physicalType = formatFhirCodeableConcept( location.physicalType, ); const address = location.address ? formatSingleAddress(location.address) : ""; return (
viewLocation(location)} onKeyDown={(event: React.KeyboardEvent) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); viewLocation(location); }} className="group rounded hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 flex gap-3 items-center justify-between p-1 cursor-pointer" >

{location.name || "Unnamed Location"}

{locationType && ( Type: {locationType} · {""} )} {physicalType && ( Physical Type: {physicalType} · {""} )} {address && ( Address: {address} )}
{/* One control per row: an editor's Edit sits at the right edge, as on the primary row; only a viewer, whose row opens the read-only details, gets the chevron. */} {isAuthorizedToEdit() ? ( ) : (
)}
); })}
) )}
{!isPatient && (
{wpOrganization?.current && ( )}

Rooms

Exam rooms belong to a site and can be assigned during scheduling.

{isAuthorizedToEdit() && ( )}
{sites.length === 0 && rooms.length === 0 ? (

Add a location first, then assign rooms to it.

) : (
{sites.map((site) => { const siteRooms = rooms.filter( (room) => getRoomSiteId(room) === site.id, ); return (

{site.name || "Unnamed Location"}

{siteRooms.length === 0 ? (

No rooms yet.

) : (
{siteRooms.map((room) => renderRoomRow(room))}
)}
); })} {unassignedRooms.length > 0 && (

Unassigned

{unassignedRooms.map((room) => renderRoomRow(room))}
)}
)}
)} {!isPatient && (

Telehealth

A virtual place where visits happen by video. It shares the practitioner's existing schedule, so a video visit and an in-person one can never take the same slot.

{isAuthorizedToEdit() && virtualLocations.length === 0 && ( )}
{virtualLocations.length === 0 ? (

Not set up. Add it when this practice offers video visits.

) : (
{virtualLocations.map((virtualLocation) => (
{isAuthorizedToEdit() && (
)}
))}
)}
)} {virtualEditor && ( setVirtualEditor(null)} virtualLocation={virtualEditor.virtualLocation} organizationId={wpOrganization.current?.id ?? ""} onSaved={handleVirtualSaved} /> )} { if (!open && !deletingVirtualBusy) { setDeletingVirtual(undefined); setDeleteVirtualError(undefined); } }} > Delete “{deletingVirtual?.name || "this telehealth location"}”? Video visits could no longer be booked at this location. This can't be undone. {deleteVirtualError && (

{deleteVirtualError}

)} Keep the location
{roomEditor && ( setRoomEditor(null)} room={roomEditor.room} sites={sites} organizationId={wpOrganization.current?.id ?? ""} onSaved={handleRoomSaved} /> )} { if (!open && !deletingRoomBusy) { setDeletingRoom(undefined); setDeleteRoomError(undefined); } }} > Delete “{deletingRoom?.name || "this room"}”? This removes the room. It can no longer be assigned during scheduling. This can't be undone. {deleteRoomError && (

{deleteRoomError}

)} Keep the room
{locationEditor && ( setLocationEditor(null)} onSaved={handleLocationEditorComplete} onDeleted={handleLocationEditorComplete} /> )} !open && closeAddLocation()}> Add a location A location is a place patients are seen. {addingLocation && (
{ addLocationDirty.reset(); setAddingLocation(null); void loadLocations(); }} />
)}
{confirmDiscardAdd && ( setConfirmDiscardAdd(false)} onDiscard={() => { setConfirmDiscardAdd(false); addLocationDirty.reset(); setAddingLocation(null); }} /> )} {selectedLocation && isPatient && ( { if (!open) setSelectedLocation(null); }} > Location details setSelectedLocation(null)} asModal={false} /> )} {/* For non-patient users, show the existing modal */} {selectedLocation && !isPatient && ( { setSelectedLocation(null); }} /> )}
); }; export default LocationsPage; // import { useNavigate } from 'react-router-dom'; // import { Location } from 'fhir/r4'; // import { AppSpinner, FhirLocationDisplay, ScrollableTab } from '../../../../components'; // import { formatFhirCodeableConcept } from '../../../../utils/fhirCodeableConcept.tsx'; // import { formatSingleAddress } from '../../../../utils/fhirAddress.tsx'; // import { useFhirLocationBroadcast, useFhirLocationTargetedBroadcast, useFhirLocations } from '../../../../hooks'; // import { useAuthServiceContext, useLocationServiceContext } from '../../../../services'; // const LocationsPage = () => { // // const { userRole, USER_ROLES, wpOrganization } = useAuthServiceContext(); // const { getLocationDisplay } = useLocationServiceContext(); // const { loading, error, clearError, locationsList } = useFhirLocations(); // const [isGuideOpen, setIsGuideOpen] = useState(false); // const [selectedLocation, setSelectedLocation] = useState(null); // const [showDisplayModal, setShowDisplayModal] = useState(false); // const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string); // const spacerMargin = '24px'; // useFhirLocationBroadcast(); // useFhirLocationTargetedBroadcast({ // location: selectedLocation || undefined, // isOpen: showDisplayModal, // onUpdate: (updatedLocation) => { // setSelectedLocation(updatedLocation); // }, // onDelete: () => { // setShowDisplayModal(false); // setSelectedLocation(null); // } // }); // const viewLocation = (location: Location) => { // setSelectedLocation(location); // setShowDisplayModal(true); // }; // const editLocation = (event: React.MouseEvent, location: Location) => { // event.stopPropagation(); // navigate(location.id as string, { // relative: 'path', // state: { // location: location // } // }); // }; // const addLocation = () => { // navigate('new', { // relative: 'path', // state: { // location: null // } // }); // }; // const isAuthorizedToEdit = () => { // return userRole && [USER_ROLES.ADMIN].includes(userRole as string); // }; // const getStatusDisplay = (location: Location): { text: string, color?: string } => { // if (!location) return { text: 'Unknown' }; // switch (location.status) { // case 'active': // return { text: 'Active', color: '#00a32a' }; // case 'suspended': // return { text: 'Suspended', color: '#e39700' }; // case 'inactive': // return { text: 'Inactive', color: '#cc1818' }; // default: // return { text: location.status?.charAt(0).toUpperCase() + location.status?.slice(1) || 'Unknown' }; // } // }; // return ( //
// {!isPatient && ( // <> // setIsGuideOpen(false)} // resourceType="Location"/> // // // // // // // {isAuthorizedToEdit() && ( // // // // )} // // // // )} // {error && ( // <> // // {error} // // // // )} // {!loading && locationsList.length === 0 && ( // <> // // No locations found. // // // // )} // <> // {loading ? ( // // ): ( // // {locationsList.length !== 0 && ( // // {secondaryLocationsList.map((location: Location) => { // const displayTitle = getLocationDisplay(location); // const { text: statusText, color: statusColor } = getStatusDisplay(location); // const locationType = formatFhirCodeableConcept(location.type?.[0]); // const physicalType = formatFhirCodeableConcept(location.physicalType); // const address = location.address ? formatSingleAddress(location.address): ''; // return ( // viewLocation(location)}> // // // // // // // // // {location.name || 'Unnamed Location'} // // {locationType && ( // // Type: {locationType} // // )} // {physicalType && ( // // Physical Type: {physicalType} // // )} // {address && ( // // Address: {address} // // )} // // {isAuthorizedToEdit() && ( // // // // )} // // // // // ); // })} // // )} // // )} // // {selectedLocation && ( // { // setShowDisplayModal(false); // setSelectedLocation(null); // }}/> // )} //
// ); // }; // export default LocationsPage;