mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
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>
This commit is contained in:
199
widget-src/src/booking.tsx
Normal file
199
widget-src/src/booking.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user