import { useCallback, useEffect, useMemo, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBuilding, faPlus, faPenToSquare } from '@fortawesome/pro-duotone-svg-icons'; import { Button } from '@/components/base/buttons/button'; import { Badge } from '@/components/base/badges/badges'; import { Table, TableCard } from '@/components/application/table/table'; import { SlideoutMenu } from '@/components/application/slideout-menus/slideout-menu'; import { TopBar } from '@/components/layout/top-bar'; import { ClinicForm, clinicCoreToGraphQLInput, holidayInputsFromForm, requiredDocInputsFromForm, emptyClinicFormValues, type ClinicFormValues, type ClinicStatus, type DocumentType, type ClinicHolidayEntry, } from '@/components/forms/clinic-form'; import { formatTimeLabel } from '@/components/application/date-picker/time-picker'; import { formatDaySelection, type DaySelection } from '@/components/application/day-selector/day-selector'; import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; import { markSetupStepComplete } from '@/lib/setup-state'; // /settings/clinics — list + add/edit slideout. Schema aligns with the // reworked Clinic entity in helix-engage/src/objects/clinic.object.ts: // - openMonday..openSunday (7 BOOLEANs) for the weekly pattern // - opensAt/closesAt (TEXT, HH:MM) for the shared daily time range // - two child entities: Holiday (closures) and ClinicRequiredDocument // (required-doc selection per clinic) // // Save flow: // 1. createClinic / updateClinic (main record) // 2. Fire child mutations in parallel: // - For holidays: delete-all-recreate on edit (simple, idempotent) // - For required docs: diff old vs new, delete removed, create added // -- Fetched shapes from the platform ---------------------------------------- type ClinicNode = { id: string; clinicName: string | null; status: ClinicStatus | null; addressCustom: { addressStreet1: string | null; addressStreet2: string | null; addressCity: string | null; addressState: string | null; addressPostcode: string | null; } | null; phone: { primaryPhoneNumber: string | null } | null; email: { primaryEmail: string | null } | null; openMonday: boolean | null; openTuesday: boolean | null; openWednesday: boolean | null; openThursday: boolean | null; openFriday: boolean | null; openSaturday: boolean | null; openSunday: boolean | null; opensAt: string | null; closesAt: string | null; walkInAllowed: boolean | null; onlineBooking: boolean | null; cancellationWindowHours: number | null; arriveEarlyMin: number | null; // Reverse-side collections. Platform exposes them as Relay edges. holidays?: { edges: Array<{ node: { id: string; date: string | null; reasonLabel: string | null }; }>; }; clinicRequiredDocuments?: { edges: Array<{ node: { id: string; documentType: DocumentType | null } }>; }; }; const CLINICS_QUERY = `{ clinics(first: 100) { edges { node { id clinicName status addressCustom { addressStreet1 addressStreet2 addressCity addressState addressPostcode } phone { primaryPhoneNumber } email { primaryEmail } openMonday openTuesday openWednesday openThursday openFriday openSaturday openSunday opensAt closesAt walkInAllowed onlineBooking cancellationWindowHours arriveEarlyMin holidays(first: 50) { edges { node { id date reasonLabel } } } clinicRequiredDocuments(first: 50) { edges { node { id documentType } } } } } } }`; // -- Helpers ----------------------------------------------------------------- const toDaySelection = (c: ClinicNode): DaySelection => ({ monday: !!c.openMonday, tuesday: !!c.openTuesday, wednesday: !!c.openWednesday, thursday: !!c.openThursday, friday: !!c.openFriday, saturday: !!c.openSaturday, sunday: !!c.openSunday, }); const toFormValues = (clinic: ClinicNode): ClinicFormValues => ({ clinicName: clinic.clinicName ?? '', addressStreet1: clinic.addressCustom?.addressStreet1 ?? '', addressStreet2: clinic.addressCustom?.addressStreet2 ?? '', addressCity: clinic.addressCustom?.addressCity ?? '', addressState: clinic.addressCustom?.addressState ?? '', addressPostcode: clinic.addressCustom?.addressPostcode ?? '', phone: clinic.phone?.primaryPhoneNumber ?? '', email: clinic.email?.primaryEmail ?? '', openDays: toDaySelection(clinic), opensAt: clinic.opensAt ?? null, closesAt: clinic.closesAt ?? null, status: clinic.status ?? 'ACTIVE', walkInAllowed: clinic.walkInAllowed ?? true, onlineBooking: clinic.onlineBooking ?? true, cancellationWindowHours: clinic.cancellationWindowHours != null ? String(clinic.cancellationWindowHours) : '', arriveEarlyMin: clinic.arriveEarlyMin != null ? String(clinic.arriveEarlyMin) : '', requiredDocumentTypes: clinic.clinicRequiredDocuments?.edges .map((e) => e.node.documentType) .filter((t): t is DocumentType => t !== null) ?? [], holidays: clinic.holidays?.edges .filter((e) => e.node.date) // date is required on create but platform may have nulls from earlier .map( (e): ClinicHolidayEntry => ({ id: e.node.id, date: e.node.date ?? '', label: e.node.reasonLabel ?? '', }), ) ?? [], }); const statusLabel: Record = { ACTIVE: 'Active', TEMPORARILY_CLOSED: 'Temporarily closed', PERMANENTLY_CLOSED: 'Permanently closed', }; const statusColor: Record = { ACTIVE: 'success', TEMPORARILY_CLOSED: 'warning', PERMANENTLY_CLOSED: 'gray', }; // Save-flow helpers — each mutation is a thin wrapper so handleSave // reads linearly. const createClinicMutation = (data: Record) => apiClient.graphql<{ createClinic: { id: string } }>( `mutation CreateClinic($data: ClinicCreateInput!) { createClinic(data: $data) { id } }`, { data }, ); const updateClinicMutation = (id: string, data: Record) => apiClient.graphql<{ updateClinic: { id: string } }>( `mutation UpdateClinic($id: UUID!, $data: ClinicUpdateInput!) { updateClinic(id: $id, data: $data) { id } }`, { id, data }, ); const createHolidayMutation = (data: Record) => apiClient.graphql( `mutation CreateHoliday($data: HolidayCreateInput!) { createHoliday(data: $data) { id } }`, { data }, ); const deleteHolidayMutation = (id: string) => apiClient.graphql( `mutation DeleteHoliday($id: UUID!) { deleteHoliday(id: $id) { id } }`, { id }, ); const createRequiredDocMutation = (data: Record) => apiClient.graphql( `mutation CreateClinicRequiredDocument($data: ClinicRequiredDocumentCreateInput!) { createClinicRequiredDocument(data: $data) { id } }`, { data }, ); const deleteRequiredDocMutation = (id: string) => apiClient.graphql( `mutation DeleteClinicRequiredDocument($id: UUID!) { deleteClinicRequiredDocument(id: $id) { id } }`, { id }, ); // -- Page -------------------------------------------------------------------- export const ClinicsPage = () => { const [clinics, setClinics] = useState([]); const [loading, setLoading] = useState(true); const [slideoutOpen, setSlideoutOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); const [formValues, setFormValues] = useState(emptyClinicFormValues); const [isSaving, setIsSaving] = useState(false); const fetchClinics = useCallback(async () => { try { const data = await apiClient.graphql<{ clinics: { edges: { node: ClinicNode }[] } }>( CLINICS_QUERY, ); setClinics(data.clinics.edges.map((e) => e.node)); } catch { // toast already shown by apiClient } finally { setLoading(false); } }, []); useEffect(() => { fetchClinics(); }, [fetchClinics]); const handleAdd = () => { setEditTarget(null); setFormValues(emptyClinicFormValues()); setSlideoutOpen(true); }; const handleEdit = (clinic: ClinicNode) => { setEditTarget(clinic); setFormValues(toFormValues(clinic)); setSlideoutOpen(true); }; const handleSave = async (close: () => void) => { if (!formValues.clinicName.trim()) { notify.error('Clinic name is required'); return; } setIsSaving(true); try { const coreInput = clinicCoreToGraphQLInput(formValues); // 1. Upsert the clinic itself. let clinicId: string; if (editTarget) { await updateClinicMutation(editTarget.id, coreInput); clinicId = editTarget.id; } else { const res = await createClinicMutation(coreInput); clinicId = res.createClinic.id; notify.success('Clinic added', `${formValues.clinicName} has been added.`); markSetupStepComplete('clinics').catch(() => {}); } // 2. Holidays — delete-all-recreate. Simple, always correct. if (editTarget?.holidays?.edges?.length) { await Promise.all( editTarget.holidays.edges.map((e) => deleteHolidayMutation(e.node.id)), ); } if (formValues.holidays.length > 0) { const holidayInputs = holidayInputsFromForm(formValues, clinicId); await Promise.all(holidayInputs.map((data) => createHolidayMutation(data))); } // 3. Required docs — delete-all-recreate for symmetry. if (editTarget?.clinicRequiredDocuments?.edges?.length) { await Promise.all( editTarget.clinicRequiredDocuments.edges.map((e) => deleteRequiredDocMutation(e.node.id), ), ); } if (formValues.requiredDocumentTypes.length > 0) { const docInputs = requiredDocInputsFromForm(formValues, clinicId); await Promise.all(docInputs.map((data) => createRequiredDocMutation(data))); } if (editTarget) { notify.success('Clinic updated', `${formValues.clinicName} has been updated.`); } await fetchClinics(); close(); } catch (err) { console.error('[clinics] save failed', err); } finally { setIsSaving(false); } }; const activeCount = useMemo( () => clinics.filter((c) => c.status === 'ACTIVE').length, [clinics], ); return (
( )} onClick={handleAdd} > Add clinic } /> {loading ? (

Loading clinics...

) : clinics.length === 0 ? (

No clinics yet

Add your first clinic branch to start booking appointments and assigning doctors.

) : ( {(clinic) => { const addressLine = [ clinic.addressCustom?.addressStreet1, clinic.addressCustom?.addressCity, ] .filter(Boolean) .join(', '); const status = clinic.status ?? 'ACTIVE'; const dayLabel = formatDaySelection(toDaySelection(clinic)); const hoursLabel = clinic.opensAt && clinic.closesAt ? `${formatTimeLabel(clinic.opensAt)}–${formatTimeLabel(clinic.closesAt)}` : 'Not set'; return ( {clinic.clinicName ?? 'Unnamed clinic'} {addressLine || '—'}
{clinic.phone?.primaryPhoneNumber ?? '—'} {clinic.email?.primaryEmail ?? ''}
{dayLabel} {hoursLabel}
{statusLabel[status]}
); }}
)}
{({ close }) => ( <>

{editTarget ? 'Edit clinic' : 'Add clinic'}

{editTarget ? 'Update branch details, hours, and policy' : 'Add a new hospital branch'}

)}
); };