// src/modals/VirtualLocationModal.tsx // Telehealth booking S1: create/edit the org's VIRTUAL location - a FHIR Location tagged // `physicalType = vi` (virtualPhysicalType(), src/utils/roomLocation.ts). This is the third kind of // Location, alongside sites and exam rooms. // // Mirrors RoomModal's shape and service wiring (useLocationServiceContext().createLocation / // updateLocation) with an even smaller form: a virtual location has no address, no hours, and no parent // site, so name + description is the whole of it. // // This creates the location; it does NOT make anything bookable by video. The org, per-service and // schedule-block gates are. Do not add a telehealth gate here. // // House conventions, same as RoomModal: Radix ui/dialog (zero WordPress in new code), Save = green and // bottom-right, no Cancel button (the dialog X closes it), and delete lives on the row in LocationsPage // behind 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 { Input } from '../components/ui/input'; import { Textarea } from '../components/ui/textarea'; import { useLocationServiceContext } from '../services'; import { virtualPhysicalType } from '../utils/roomLocation'; import { relayServerFailure } from '../utils/fetchFailureMessage'; export interface VirtualLocationModalProps { isOpen: boolean; onClose: () => void; virtualLocation?: Location; // undefined = create organizationId: string; onSaved: (virtualLocation: Location) => void; } const VirtualLocationModal = ({ isOpen, onClose, virtualLocation, organizationId, onSaved, }: VirtualLocationModalProps) => { const { createLocation, updateLocation } = useLocationServiceContext(); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(); const isEdit = Boolean(virtualLocation?.id); useEffect(() => { if (!isOpen) return; setName(virtualLocation?.name ?? ''); setDescription(virtualLocation?.description ?? ''); setError(undefined); }, [isOpen, virtualLocation]); if (!isOpen) return null; const handleSave = async () => { const trimmedName = name.trim(); if (!trimmedName) { setError('Enter a name for the telehealth location.'); return; } setSaving(true); setError(undefined); try { const body: Location = { resourceType: 'Location', ...(virtualLocation?.id ? { id: virtualLocation.id } : {}), name: trimmedName, status: 'active', physicalType: virtualPhysicalType(), managingOrganization: { reference: `Organization/${organizationId}` }, ...(description.trim() ? { description: description.trim() } : {}), }; // An edit is written only over the version of the location this was opened with. const saved = virtualLocation?.id ? await updateLocation(body, virtualLocation.meta?.versionId) : await createLocation(body); onSaved(saved); } catch (err) { console.error(`Failed to ${isEdit ? 'update' : 'create'} the telehealth location:`, err); setError( relayServerFailure( err, `Failed to ${isEdit ? 'update' : 'create'} this telehealth location. Please try again.`, ), ); } finally { setSaving(false); } }; return ( { if (!next && !saving) onClose(); }} > {isEdit ? 'Edit telehealth location' : 'Add telehealth location'}

A location for video visits. Adding it does not enable video booking for your services.

{/* 🚨 THIS FIELD IS PATIENT-FACING, WHICH THE REST OF THIS MODAL IS NOT. Whatever the org types here becomes the `Location.name` shown as a choice in the booking pickers, sitting beside real site names ("West Burlington Hospital"). So it wants the patient's word, not the administrator's. The placeholder used to read "e.g. Telehealth" — the one place in the product that steered orgs into putting billing/industry vocabulary in front of patients. Everything else already splits the register correctly: admin surfaces say telehealth (this modal's own title, the org policy section), patient-facing copy says video visit (bookabilityCopy, the join banner). Spencer, 2026-08-04. */} setName(event.target.value)} />