// src/modals/RoomModal.tsx // Rooms feature (Phase A), Task 3: create/edit an exam room - a FHIR Location scoped under a site via // `partOf`, tagged `physicalType=room` (roomPhysicalType(), from Task 1's src/utils/roomLocation.ts). // Mirrors LocationModal's create/update service wiring (useLocationServiceContext().createLocation / // updateLocation) but the form is deliberately just three fields (name, site, description) - a room has no // address, hours, or type of its own, so none of LocationModal's other tabs apply. // // Built on Radix ui/dialog + ui/select (zero WordPress in new code, per the minimize-WordPress-coupling // rule) instead of LocationModal's WP , following the same pattern as FeesCatalog's FeeFormDialog / // InventoryImportDialog. House conventions: Save = green, bottom-right, no Cancel button (the dialog X // closes it). Delete is NOT in this modal - it lives on the room row in LocationsPage, gated by its own // confirm step. import { useEffect, useState } from 'react'; import type { Location } from 'fhir/r4'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '../components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../components/ui/select'; import { Input } from '../components/ui/input'; import { Textarea } from '../components/ui/textarea'; import { useLocationServiceContext } from '../services'; import { getRoomSiteId, roomPhysicalType } from '../utils/roomLocation'; import { TIMEZONE_EXTENSION_URL, locationTimeZoneOf } from '../utils/practiceTimezone'; import { isRecordChanged } from '../utils/recordChanged'; import { ownFailureSentence } from '../utils/fetchFailureMessage'; export interface RoomModalProps { isOpen: boolean; onClose: () => void; room?: Location; // undefined = create sites: Location[]; // sites-only list for the parent picker organizationId: string; onSaved: (room: Location) => void; } const RoomModal = ({ isOpen, onClose, room, sites, organizationId, onSaved }: RoomModalProps) => { const { createLocation, updateLocation } = useLocationServiceContext(); const [name, setName] = useState(''); const [siteId, setSiteId] = useState(''); const [description, setDescription] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(); const isEdit = Boolean(room?.id); // Reset the form each time the dialog opens, seeded from `room` (edit) or blank (create). Plain state // rather than react-hook-form since this form only has three fields - matches this file's own scope, // not LocationModal's (which needs RHF for its many nested address/hours fields). useEffect(() => { if (!isOpen) return; setName(room?.name ?? ''); setSiteId((room ? getRoomSiteId(room) : undefined) ?? ''); setDescription(room?.description ?? ''); setError(undefined); }, [isOpen, room]); if (!isOpen) return null; const handleSave = async () => { const trimmedName = name.trim(); if (!trimmedName) { setError('Enter a room name.'); return; } if (!siteId) { setError('Select a site for this room.'); return; } setSaving(true); setError(undefined); // The server refuses a non-virtual Location without a zone, and a room keeps its site's clock. const site = sites.find((s) => s.id === siteId); const siteZone = locationTimeZoneOf(site); try { const body: Location = { resourceType: 'Location', ...(room?.id ? { id: room.id } : {}), name: trimmedName, status: 'active', physicalType: roomPhysicalType(), partOf: { reference: `Location/${siteId}`, display: site?.name, }, ...(siteZone ? { extension: [{ url: TIMEZONE_EXTENSION_URL, valueCode: siteZone }] } : {}), managingOrganization: { reference: `Organization/${organizationId}` }, ...(description.trim() ? { description: description.trim() } : {}), }; // An edit is written only over the version of the room this was opened with. const saved = room?.id ? await updateLocation(body, room.meta?.versionId) : await createLocation(body); onSaved(saved); } catch (err) { console.error(`Failed to ${isEdit ? 'update' : 'create'} room:`, err); // Someone else saved this room first: say so, rather than invite the same refused save again. setError(isRecordChanged(err) ? ownFailureSentence(err) : `Failed to ${isEdit ? 'update' : 'create'} this room. Please try again.`); } finally { setSaving(false); } }; return ( { if (!next && !saving) onClose(); }} > {isEdit ? 'Edit room' : 'Add room'}
setName(event.target.value)} />