import { useState, useEffect, useMemo } from 'preact/hooks'; import { fetchSlots, submitBooking } from './api'; import { departmentIcon } from './icons'; import { IconSpan } from './icon-span'; import { useWidgetStore } from './store'; import type { Doctor, TimeSlot } from './types'; type Step = 'branch' | 'department' | 'doctor' | 'datetime' | 'details' | 'success'; export const Booking = () => { const { visitor, updateVisitor, captchaToken, bookingPrefill, setBookingPrefill, doctors, doctorsLoading, doctorsError, branches, selectedBranch, setSelectedBranch, } = useWidgetStore(); // Start on the branch step only if the visitor actually has a choice to // make. Single-branch hospitals and chat-prefilled sessions skip it. const needsBranchStep = branches.length > 1 && !selectedBranch; const [step, setStep] = useState(needsBranchStep ? 'branch' : 'department'); const [selectedDept, setSelectedDept] = useState(''); const [selectedDoctor, setSelectedDoctor] = useState(null); const [selectedDate, setSelectedDate] = useState(''); const [slots, setSlots] = useState([]); const [selectedSlot, setSelectedSlot] = useState(''); const [complaint, setComplaint] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [reference, setReference] = useState(''); // Scope the roster to the selected branch up front. Every downstream // derivation (departments list, doctor filter) works off this. const branchDoctors = useMemo(() => { if (!selectedBranch) return doctors; const needle = selectedBranch.toLowerCase(); return doctors.filter(d => String(d.clinic?.clinicName ?? '').toLowerCase().includes(needle), ); }, [doctors, selectedBranch]); // Derive department list from the branch-scoped roster. const departments = useMemo( () => [...new Set(branchDoctors.map(d => d.department).filter(Boolean))] as string[], [branchDoctors], ); const filteredDoctors = selectedDept ? branchDoctors.filter(d => d.department === selectedDept) : []; // Surface a doctors-load error if the roster failed to fetch. useEffect(() => { if (doctorsError) setError(doctorsError); }, [doctorsError]); // Consume any booking prefill from chat → jump straight to the details form. // Also locks the branch to the picked doctor's clinic so the visitor sees // the right header badge when they land here. useEffect(() => { if (!bookingPrefill || doctors.length === 0) return; const doc = doctors.find(d => d.id === bookingPrefill.doctorId); if (!doc) return; if (doc.clinic?.clinicName && !selectedBranch) { setSelectedBranch(doc.clinic.clinicName); } setSelectedDept(doc.department); setSelectedDoctor(doc); setSelectedDate(bookingPrefill.date); setSelectedSlot(bookingPrefill.time); setStep('details'); setBookingPrefill(null); }, [bookingPrefill, doctors]); const handleDoctorSelect = (doc: Doctor) => { setSelectedDoctor(doc); setSelectedDate(new Date().toISOString().split('T')[0]); setStep('datetime'); }; useEffect(() => { if (selectedDoctor && selectedDate) { fetchSlots(selectedDoctor.id, selectedDate).then(setSlots).catch(() => {}); } }, [selectedDoctor, selectedDate]); const handleBook = async () => { if (!selectedDoctor || !selectedSlot || !visitor.name.trim() || !visitor.phone.trim()) return; setLoading(true); setError(''); try { const scheduledAt = `${selectedDate}T${selectedSlot}:00`; const result = await submitBooking({ departmentId: selectedDept, doctorId: selectedDoctor.id, scheduledAt, patientName: visitor.name.trim(), patientPhone: visitor.phone.trim(), chiefComplaint: complaint, captchaToken, }); setReference(result.reference); setStep('success'); } catch { setError('Booking failed. Please try again.'); } finally { setLoading(false); } }; // Progress bar step count is dynamic: 5 dots if we need the branch step, // 4 otherwise. The current position is derived from the flow we're in. const flowSteps: Step[] = needsBranchStep ? ['branch', 'department', 'doctor', 'datetime', 'details'] : ['department', 'doctor', 'datetime', 'details']; const currentStep = flowSteps.indexOf(step); return (
{step !== 'success' && (
{flowSteps.map((_, i) => (
))}
)} {error &&
{error}
} {step === 'branch' && (
Select Branch
{doctorsLoading && branches.length === 0 && (
Loading…
)} {branches.map(branch => ( ))}
)} {step === 'department' && (
{selectedBranch && ( <> {selectedBranch} —  )} Select Department
{doctorsLoading && departments.length === 0 && (
Loading…
)} {departments.map(dept => ( ))} {branches.length > 1 && ( )}
)} {step === 'doctor' && (
{selectedDept.replace(/_/g, ' ')}
{filteredDoctors.map(doc => ( ))}
)} {step === 'datetime' && (
{selectedDoctor?.name} — Pick Date & Time
{ setSelectedDate(e.target.value); setSelectedSlot(''); }} />
{slots.length > 0 && (
{slots.map(s => ( ))}
)}
)} {step === 'details' && (
Your Details
updateVisitor({ name: e.target.value })} />
updateVisitor({ phone: e.target.value })} />