mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
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>
200 lines
9.3 KiB
TypeScript
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>
|
|
);
|
|
};
|