Files
helix-engage-server/widget-src/src/booking.tsx
saridsa2 517b2661b0 chore: move widget source into sidecar repo (widget-src/)
Widget builds from widget-src/ → public/widget.js
Vite outDir updated to ../public
.gitignore excludes node_modules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 06:59:54 +05:30

200 lines
9.3 KiB
TypeScript

import { useState, useEffect } from 'preact/hooks';
import { fetchDoctors, fetchSlots, submitBooking } from './api';
import type { Doctor, TimeSlot } from './types';
type Step = 'department' | 'doctor' | 'datetime' | 'details' | 'success';
export const Booking = () => {
const [step, setStep] = useState<Step>('department');
const [doctors, setDoctors] = useState<Doctor[]>([]);
const [departments, setDepartments] = useState<string[]>([]);
const [selectedDept, setSelectedDept] = useState('');
const [selectedDoctor, setSelectedDoctor] = useState<Doctor | null>(null);
const [selectedDate, setSelectedDate] = useState('');
const [slots, setSlots] = useState<TimeSlot[]>([]);
const [selectedSlot, setSelectedSlot] = useState('');
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [complaint, setComplaint] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [reference, setReference] = useState('');
useEffect(() => {
fetchDoctors().then(docs => {
setDoctors(docs);
setDepartments([...new Set(docs.map(d => d.department).filter(Boolean))]);
}).catch(() => setError('Failed to load doctors'));
}, []);
const filteredDoctors = selectedDept ? doctors.filter(d => d.department === selectedDept) : [];
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 || !name || !phone) return;
setLoading(true);
setError('');
try {
const scheduledAt = `${selectedDate}T${selectedSlot}:00`;
const result = await submitBooking({
departmentId: selectedDept,
doctorId: selectedDoctor.id,
scheduledAt,
patientName: name,
patientPhone: phone,
chiefComplaint: complaint,
captchaToken: 'dev-bypass',
});
setReference(result.reference);
setStep('success');
} catch {
setError('Booking failed. Please try again.');
} finally {
setLoading(false);
}
};
const stepIndex = { department: 0, doctor: 1, datetime: 2, details: 3, success: 4 };
const currentStep = stepIndex[step];
return (
<div>
{step !== 'success' && (
<div class="widget-steps">
{[0, 1, 2, 3].map(i => (
<div key={i} class={`widget-step ${i < currentStep ? 'done' : i === currentStep ? 'active' : ''}`} />
))}
</div>
)}
{error && <div style={{ color: '#dc2626', fontSize: '12px', marginBottom: '8px' }}>{error}</div>}
{step === 'department' && (
<div>
<div class="widget-label" style={{ marginBottom: '8px', fontSize: '13px', fontWeight: 600 }}>Select Department</div>
{departments.map(dept => (
<button
key={dept}
class="widget-btn widget-btn-secondary"
style={{ marginBottom: '6px', textAlign: 'left' }}
onClick={() => { setSelectedDept(dept); setStep('doctor'); }}
>
{dept.replace(/_/g, ' ')}
</button>
))}
</div>
)}
{step === 'doctor' && (
<div>
<div class="widget-label" style={{ marginBottom: '8px', fontSize: '13px', fontWeight: 600 }}>
Select Doctor {selectedDept.replace(/_/g, ' ')}
</div>
{filteredDoctors.map(doc => (
<button
key={doc.id}
class="widget-btn widget-btn-secondary"
style={{ marginBottom: '6px', textAlign: 'left' }}
onClick={() => handleDoctorSelect(doc)}
>
<div style={{ fontWeight: 600 }}>{doc.name}</div>
<div style={{ fontSize: '11px', color: '#6b7280' }}>
{doc.visitingHours ?? ''} {doc.clinic?.clinicName ? `${doc.clinic.clinicName}` : ''}
</div>
</button>
))}
<button class="widget-btn widget-btn-secondary" style={{ marginTop: '8px' }} onClick={() => setStep('department')}>
Back
</button>
</div>
)}
{step === 'datetime' && (
<div>
<div class="widget-label" style={{ marginBottom: '8px', fontSize: '13px', fontWeight: 600 }}>
{selectedDoctor?.name} Pick Date & Time
</div>
<div class="widget-field">
<label class="widget-label">Date</label>
<input
class="widget-input"
type="date"
value={selectedDate}
min={new Date().toISOString().split('T')[0]}
onInput={(e: any) => { setSelectedDate(e.target.value); setSelectedSlot(''); }}
/>
</div>
{slots.length > 0 && (
<div>
<label class="widget-label">Available Slots</label>
<div class="widget-slots">
{slots.map(s => (
<button
key={s.time}
class={`widget-slot ${s.time === selectedSlot ? 'selected' : ''} ${!s.available ? 'unavailable' : ''}`}
onClick={() => s.available && setSelectedSlot(s.time)}
disabled={!s.available}
>
{s.time}
</button>
))}
</div>
</div>
)}
<div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
<button class="widget-btn widget-btn-secondary" style={{ flex: 1 }} onClick={() => setStep('doctor')}> Back</button>
<button class="widget-btn" style={{ flex: 1 }} disabled={!selectedSlot} onClick={() => setStep('details')}>Next </button>
</div>
</div>
)}
{step === 'details' && (
<div>
<div class="widget-label" style={{ marginBottom: '8px', fontSize: '13px', fontWeight: 600 }}>Your Details</div>
<div class="widget-field">
<label class="widget-label">Full Name *</label>
<input class="widget-input" placeholder="Your name" value={name} onInput={(e: any) => setName(e.target.value)} />
</div>
<div class="widget-field">
<label class="widget-label">Phone Number *</label>
<input class="widget-input" placeholder="+91 9876543210" value={phone} onInput={(e: any) => setPhone(e.target.value)} />
</div>
<div class="widget-field">
<label class="widget-label">Chief Complaint</label>
<textarea class="widget-input widget-textarea" placeholder="Describe your concern..." value={complaint} onInput={(e: any) => setComplaint(e.target.value)} />
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button class="widget-btn widget-btn-secondary" style={{ flex: 1 }} onClick={() => setStep('datetime')}> Back</button>
<button class="widget-btn" style={{ flex: 1 }} disabled={!name || !phone || loading} onClick={handleBook}>
{loading ? 'Booking...' : 'Book Appointment'}
</button>
</div>
</div>
)}
{step === 'success' && (
<div class="widget-success">
<div class="widget-success-icon"></div>
<div class="widget-success-title">Appointment Booked!</div>
<div class="widget-success-text">
Reference: <strong>{reference}</strong><br />
{selectedDoctor?.name} {selectedDate} at {selectedSlot}<br /><br />
We'll send a confirmation SMS to your phone.
</div>
</div>
)}
</div>
);
};