import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPlus, faTrash } from '@fortawesome/pro-duotone-svg-icons'; import { parseDate, getLocalTimeZone, today } from '@internationalized/date'; import type { DateValue } from 'react-aria-components'; import { Input } from '@/components/base/input/input'; import { Select } from '@/components/base/select/select'; import { Toggle } from '@/components/base/toggle/toggle'; import { Button } from '@/components/base/buttons/button'; import { DatePicker } from '@/components/application/date-picker/date-picker'; import { TimePicker } from '@/components/application/date-picker/time-picker'; import { DaySelector, defaultDaySelection, type DaySelection, } from '@/components/application/day-selector/day-selector'; // Reusable clinic form used by /settings/clinics slideout and the /setup // wizard step. The parent owns form state + the save flow so it can decide // how to orchestrate the multi-step create chain (one createClinic, then one // createHoliday per holiday, then one createClinicRequiredDocument per doc). // // Schema (matches the Clinic entity in // FortyTwoApps/helix-engage/src/objects/clinic.object.ts, column names // derived from SDK labels — that's why opensAt/closesAt and not openTime/ // closeTime): // - clinicName (TEXT) // - address (ADDRESS → addressCustomAddress*) // - phone (PHONES) // - email (EMAILS) // - openMonday..openSunday (7 BOOLEANs) // - opensAt / closesAt (TEXT, HH:MM) // - status (SELECT enum) // - walkInAllowed / onlineBooking (BOOLEAN) // - cancellationWindowHours / arriveEarlyMin (NUMBER) // // Plus two child entities populated separately: // - Holiday (one record per closure date) // - ClinicRequiredDocument (one record per required doc type) export type ClinicStatus = 'ACTIVE' | 'TEMPORARILY_CLOSED' | 'PERMANENTLY_CLOSED'; // Matches the SELECT enum on ClinicRequiredDocument. Keep in sync with // FortyTwoApps/helix-engage/src/objects/clinic-required-document.object.ts. export type DocumentType = | 'ID_PROOF' | 'AADHAAR' | 'PAN' | 'REFERRAL_LETTER' | 'PRESCRIPTION' | 'INSURANCE_CARD' | 'PREVIOUS_REPORTS' | 'PHOTO' | 'OTHER'; const DOCUMENT_TYPE_LABELS: Record = { ID_PROOF: 'Government ID', AADHAAR: 'Aadhaar Card', PAN: 'PAN Card', REFERRAL_LETTER: 'Referral Letter', PRESCRIPTION: 'Prescription', INSURANCE_CARD: 'Insurance Card', PREVIOUS_REPORTS: 'Previous Reports', PHOTO: 'Passport Photo', OTHER: 'Other', }; const DOCUMENT_TYPE_ORDER: DocumentType[] = [ 'ID_PROOF', 'AADHAAR', 'PAN', 'REFERRAL_LETTER', 'PRESCRIPTION', 'INSURANCE_CARD', 'PREVIOUS_REPORTS', 'PHOTO', 'OTHER', ]; export type ClinicHolidayEntry = { // Populated on the existing record when editing; undefined for freshly // added holidays the user hasn't saved yet. Used by the parent to // decide create vs update vs delete on save. id?: string; date: string; // ISO yyyy-MM-dd label: string; }; export type ClinicFormValues = { // Core clinic fields clinicName: string; addressStreet1: string; addressStreet2: string; addressCity: string; addressState: string; addressPostcode: string; phone: string; email: string; // Schedule — simple pattern openDays: DaySelection; opensAt: string | null; closesAt: string | null; // Status + booking policy status: ClinicStatus; walkInAllowed: boolean; onlineBooking: boolean; cancellationWindowHours: string; arriveEarlyMin: string; // Children (persisted via separate mutations) requiredDocumentTypes: DocumentType[]; holidays: ClinicHolidayEntry[]; }; export const emptyClinicFormValues = (): ClinicFormValues => ({ clinicName: '', addressStreet1: '', addressStreet2: '', addressCity: '', addressState: '', addressPostcode: '', phone: '', email: '', openDays: defaultDaySelection(), opensAt: '09:00', closesAt: '18:00', status: 'ACTIVE', walkInAllowed: true, onlineBooking: true, cancellationWindowHours: '24', arriveEarlyMin: '15', requiredDocumentTypes: [], holidays: [], }); const STATUS_ITEMS = [ { id: 'ACTIVE', label: 'Active' }, { id: 'TEMPORARILY_CLOSED', label: 'Temporarily closed' }, { id: 'PERMANENTLY_CLOSED', label: 'Permanently closed' }, ]; // Build the payload for `createClinic` / `updateClinic`. Holidays and // required-documents are NOT included here — they're child records with // their own mutations, orchestrated by the parent component after the // clinic itself has been created and its id is known. export const clinicCoreToGraphQLInput = (v: ClinicFormValues): Record => { const input: Record = { clinicName: v.clinicName.trim(), status: v.status, walkInAllowed: v.walkInAllowed, onlineBooking: v.onlineBooking, openMonday: v.openDays.monday, openTuesday: v.openDays.tuesday, openWednesday: v.openDays.wednesday, openThursday: v.openDays.thursday, openFriday: v.openDays.friday, openSaturday: v.openDays.saturday, openSunday: v.openDays.sunday, }; // Column names on the platform come from the SDK `label`, not // `name`. "Opens At" → opensAt, "Closes At" → closesAt. if (v.opensAt) input.opensAt = v.opensAt; if (v.closesAt) input.closesAt = v.closesAt; const hasAddress = v.addressStreet1 || v.addressCity || v.addressState || v.addressPostcode; if (hasAddress) { input.addressCustom = { addressStreet1: v.addressStreet1 || null, addressStreet2: v.addressStreet2 || null, addressCity: v.addressCity || null, addressState: v.addressState || null, addressPostcode: v.addressPostcode || null, addressCountry: 'India', }; } if (v.phone.trim()) { input.phone = { primaryPhoneNumber: v.phone.trim(), primaryPhoneCountryCode: 'IN', primaryPhoneCallingCode: '+91', additionalPhones: null, }; } if (v.email.trim()) { input.email = { primaryEmail: v.email.trim(), additionalEmails: null, }; } if (v.cancellationWindowHours.trim()) { const n = Number(v.cancellationWindowHours); if (!Number.isNaN(n)) input.cancellationWindowHours = n; } if (v.arriveEarlyMin.trim()) { const n = Number(v.arriveEarlyMin); if (!Number.isNaN(n)) input.arriveEarlyMin = n; } return input; }; // Helper: build HolidayCreateInput payloads. Use after the clinic has // been created and its id is known. export const holidayInputsFromForm = ( v: ClinicFormValues, clinicId: string, ): Array> => v.holidays.map((h) => ({ date: h.date, reasonLabel: h.label.trim() || null, // column name matches the SDK label "Reason / Label" clinicId, })); // Helper: build ClinicRequiredDocumentCreateInput payloads. One per // selected document type. export const requiredDocInputsFromForm = ( v: ClinicFormValues, clinicId: string, ): Array> => v.requiredDocumentTypes.map((t) => ({ documentType: t, clinicId, })); type ClinicFormProps = { value: ClinicFormValues; onChange: (value: ClinicFormValues) => void; }; export const ClinicForm = ({ value, onChange }: ClinicFormProps) => { const patch = (updates: Partial) => onChange({ ...value, ...updates }); // Required-docs add/remove handlers. The user picks a type from the // dropdown; it gets added to the list; the pill row below shows // selected types with an X to remove. Dropdown filters out // already-selected types so the user can't pick duplicates. const availableDocTypes = DOCUMENT_TYPE_ORDER.filter( (t) => !value.requiredDocumentTypes.includes(t), ).map((t) => ({ id: t, label: DOCUMENT_TYPE_LABELS[t] })); const addDocType = (type: DocumentType) => { if (value.requiredDocumentTypes.includes(type)) return; patch({ requiredDocumentTypes: [...value.requiredDocumentTypes, type] }); }; const removeDocType = (type: DocumentType) => { patch({ requiredDocumentTypes: value.requiredDocumentTypes.filter((t) => t !== type), }); }; // Holiday add/remove handlers. Freshly-added entries have no `id` // field; the parent's save flow treats those as "create". const addHoliday = () => { const todayIso = today(getLocalTimeZone()).toString(); patch({ holidays: [...value.holidays, { date: todayIso, label: '' }] }); }; const updateHoliday = (index: number, updates: Partial) => { const next = [...value.holidays]; next[index] = { ...next[index], ...updates }; patch({ holidays: next }); }; const removeHoliday = (index: number) => { patch({ holidays: value.holidays.filter((_, i) => i !== index) }); }; return (
patch({ clinicName: v })} /> {/* Address */}

Address

patch({ addressStreet1: v })} /> patch({ addressStreet2: v })} />
patch({ addressCity: v })} /> patch({ addressState: v })} />
patch({ addressPostcode: v })} />
{/* Contact */}

Contact

patch({ phone: v })} /> patch({ email: v })} />
{/* Visiting hours — day pills + single time range */}

Visiting hours

patch({ openDays })} />
patch({ opensAt })} /> patch({ closesAt })} />
{/* Holiday closures */}

Holiday closures (optional)

{value.holidays.length === 0 && (

No holidays configured. Add dates when this clinic is closed (Diwali, Republic Day, maintenance days, etc.).

)} {value.holidays.map((h, idx) => (
Date updateHoliday(idx, { date: dv ? dv.toString() : '' }) } />
updateHoliday(idx, { label })} />
))}
{/* Booking policy */}

Booking policy

patch({ walkInAllowed: checked })} /> patch({ onlineBooking: checked })} />
patch({ cancellationWindowHours: v })} /> patch({ arriveEarlyMin: v })} />
{/* Required documents — multi-select → pills */}

Required documents (optional)

{availableDocTypes.length > 0 && ( )} {value.requiredDocumentTypes.length > 0 && (
{value.requiredDocumentTypes.map((t) => ( ))}
)} {value.requiredDocumentTypes.length === 0 && (

No required documents. Patients won't be asked to bring anything.

)}
); };