Skip to content

Commit e20f430

Browse files
committed
UI: Booking page
1 parent 2512466 commit e20f430

5 files changed

Lines changed: 329 additions & 124 deletions

File tree

frontend/src/app/(patient)/dashboard/book/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ export default function BookPage() {
99
<h1 className="text-3xl font-semibold text-gray-900">
1010
Book Appointment
1111
</h1>
12+
1213
<p className="text-gray-500 mt-1">
1314
Choose a department, doctor and available time slot.
1415
</p>
1516
</div>
1617

17-
{/* Card Container */}
18-
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-8 max-w-3xl">
18+
{/* Full width container */}
19+
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-8">
20+
1921
<BookingForm />
22+
2023
</div>
2124

2225
</div>

frontend/src/features/appointments/components/BookingForm.tsx

Lines changed: 169 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import { useDepartments } from '@/src/features/admin/hooks/useDepartments';
55
import { useDoctors } from '../hooks/useDoctors';
66
import { useAvailableSlots } from '../hooks/useAvailableSlots';
77
import { useBookAppointment } from '../hooks/useBookAppointment';
8+
9+
import DoctorCard from './DoctorCard';
810
import SlotSelector from './SlotSelector';
11+
import BookingStepper from './BookingStepper';
912

1013
export default function BookingForm() {
14+
15+
const [date, setDate] = useState('');
1116
const [departmentId, setDepartmentId] = useState('');
1217
const [doctorId, setDoctorId] = useState('');
13-
const [date, setDate] = useState('');
1418
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
1519

1620
const { data: departments } = useDepartments();
@@ -20,128 +24,178 @@ export default function BookingForm() {
2024

2125
const mutation = useBookAppointment();
2226

23-
function handleBook() {
24-
if (!selectedSlot || !doctorId || !date) return;
27+
let step = 1;
28+
if (date) step = 2;
29+
if (departmentId) step = 3;
30+
if (doctorId) step = 4;
31+
32+
function handleBook() {
33+
34+
if (!selectedSlot || !doctorId || !date) return;
35+
36+
const fullDateTime = `${date}T${selectedSlot}:00`;
2537

26-
// Combine date + time into ISO timestamp
27-
const fullDateTime = `${date}T${selectedSlot}:00`;
38+
mutation.mutate({
39+
doctorId,
40+
appointmentTime: fullDateTime,
41+
durationMinutes: 15,
42+
});
2843

29-
mutation.mutate({
30-
doctorId,
31-
appointmentTime: fullDateTime,
32-
durationMinutes: 15,
33-
});
34-
}
44+
}
3545

3646
return (
37-
<div className="space-y-6 max-w-xl bg-white p-6 rounded-xl shadow-md">
38-
39-
<h2 className="text-xl font-semibold">
40-
Book Appointment
41-
</h2>
42-
43-
{/* Department */}
44-
<div>
45-
<label className="block mb-1 font-medium">
46-
Department
47-
</label>
48-
49-
<select
50-
className="border p-2 w-full rounded-lg"
51-
value={departmentId}
52-
onChange={(e) => {
53-
setDepartmentId(e.target.value);
54-
setDoctorId('');
55-
setSelectedSlot(null);
56-
}}
57-
>
58-
<option value="">Select Department</option>
59-
60-
{departments?.map(dep => (
61-
<option key={dep.id} value={dep.id}>
62-
{dep.name}
63-
</option>
64-
))}
65-
</select>
66-
</div>
6747

68-
{/* Doctor */}
69-
<div>
70-
<label className="block mb-1 font-medium">
71-
Doctor
72-
</label>
73-
74-
<select
75-
className="border p-2 w-full rounded-lg"
76-
value={doctorId}
77-
onChange={(e) => {
78-
setDoctorId(e.target.value);
79-
setSelectedSlot(null);
80-
}}
81-
disabled={!departmentId}
82-
>
83-
<option value="">Select Doctor</option>
84-
85-
{doctors?.map(doc => (
86-
<option key={doc.id} value={doc.id}>
87-
{doc.name}{doc.job_title}
88-
</option>
89-
))}
90-
</select>
91-
</div>
48+
<div className="w-full">
49+
50+
{/* STEP INDICATOR */}
51+
52+
<BookingStepper step={step} />
53+
54+
{/* MAIN LAYOUT */}
55+
56+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10">
57+
58+
{/* LEFT PANEL */}
59+
60+
<div className="bg-slate-50 border rounded-2xl p-6 space-y-6">
61+
62+
<h2 className="font-semibold text-lg">
63+
Appointment Details
64+
</h2>
65+
66+
{/* Date */}
67+
68+
<div>
69+
70+
<label className="block mb-1 font-medium">
71+
Select Date
72+
</label>
73+
74+
<input
75+
type="date"
76+
className="border p-3 w-full rounded-lg"
77+
value={date}
78+
onChange={(e) => {
79+
setDate(e.target.value);
80+
setDoctorId('');
81+
setSelectedSlot(null);
82+
}}
83+
min={new Date().toISOString().split('T')[0]}
84+
/>
85+
86+
</div>
87+
88+
{/* Department */}
89+
90+
<div>
91+
92+
<label className="block mb-1 font-medium">
93+
Choose Department
94+
</label>
95+
96+
<select
97+
className="border p-3 w-full rounded-lg"
98+
value={departmentId}
99+
onChange={(e) => {
100+
setDepartmentId(e.target.value);
101+
setDoctorId('');
102+
setSelectedSlot(null);
103+
}}
104+
disabled={!date}
105+
>
106+
107+
<option value="">
108+
Select Department
109+
</option>
110+
111+
{departments?.map(dep => (
112+
<option key={dep.id} value={dep.id}>
113+
{dep.name}
114+
</option>
115+
))}
116+
117+
</select>
118+
119+
</div>
120+
121+
</div>
122+
123+
{/* RIGHT PANEL */}
124+
125+
<div className="lg:col-span-2 space-y-8">
126+
127+
{/* DOCTOR SECTION */}
128+
129+
<div className="bg-white border rounded-2xl p-6">
130+
131+
<h3 className="font-semibold mb-4">
132+
Select Doctor
133+
</h3>
134+
135+
<div className="grid md:grid-cols-2 gap-4">
136+
137+
{doctors?.map((doc) => (
138+
139+
<DoctorCard
140+
key={doc.id}
141+
doctor={doc}
142+
selected={doctorId === doc.id}
143+
onSelect={(id) => {
144+
setDoctorId(id);
145+
setSelectedSlot(null);
146+
}}
147+
/>
148+
149+
))}
150+
151+
</div>
152+
153+
</div>
154+
155+
{/* SLOT SECTION */}
156+
157+
<div className="bg-white border rounded-2xl p-6">
158+
159+
<h3 className="font-semibold mb-4">
160+
Available Time Slots
161+
</h3>
162+
163+
{isLoading && (
164+
<p className="text-sm text-gray-500">
165+
Loading slots...
166+
</p>
167+
)}
168+
169+
{doctorId && (
170+
<SlotSelector
171+
slots={slots}
172+
selected={selectedSlot}
173+
onSelect={setSelectedSlot}
174+
/>
175+
)}
176+
177+
</div>
178+
179+
{/* BOOK BUTTON */}
180+
181+
<button
182+
onClick={handleBook}
183+
disabled={!selectedSlot || mutation.isPending}
184+
className="bg-blue-600 hover:bg-blue-700 text-white p-4 rounded-xl w-full transition font-semibold"
185+
>
186+
187+
{mutation.isPending
188+
? 'Booking Appointment...'
189+
: 'Confirm Appointment'}
190+
191+
</button>
192+
193+
</div>
92194

93-
{/* Date */}
94-
<div>
95-
<label className="block mb-1 font-medium">
96-
Date
97-
</label>
98-
99-
<input
100-
type="date"
101-
className="border p-2 w-full rounded-lg"
102-
value={date}
103-
onChange={(e) => {
104-
setDate(e.target.value);
105-
setSelectedSlot(null);
106-
}}
107-
disabled={!doctorId}
108-
min={new Date().toISOString().split('T')[0]}
109-
/>
110195
</div>
111196

112-
{/* Slots */}
113-
{isLoading && <p>Loading available slots...</p>}
114-
115-
{slots.length > 0 && (
116-
<SlotSelector
117-
slots={slots}
118-
selected={selectedSlot}
119-
onSelect={setSelectedSlot}
120-
/>
121-
)}
122-
123-
{/* Button */}
124-
<button
125-
onClick={handleBook}
126-
disabled={!selectedSlot || mutation.isPending}
127-
className="bg-blue-600 hover:bg-blue-700 text-white p-3 rounded-lg w-full transition"
128-
>
129-
{mutation.isPending ? 'Booking...' : 'Book Appointment'}
130-
</button>
131-
132-
{/* Errors */}
133-
{mutation.isError && (
134-
<p className="text-red-500 text-sm">
135-
{(mutation.error as any)?.response?.data?.error}
136-
</p>
137-
)}
138-
139-
{/* Success */}
140-
{mutation.isSuccess && (
141-
<p className="text-green-600 text-sm">
142-
Appointment booked successfully!
143-
</p>
144-
)}
145197
</div>
198+
146199
);
200+
147201
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use client';
2+
3+
interface Props {
4+
step: number;
5+
}
6+
7+
const steps = [
8+
"Select Date",
9+
"Choose Department",
10+
"Select Doctor",
11+
"Pick Time Slot"
12+
];
13+
14+
export default function BookingStepper({ step }: Props) {
15+
16+
return (
17+
18+
<div className="w-full mb-10">
19+
20+
<div className="flex items-center justify-between relative">
21+
22+
{/* Background line */}
23+
<div className="absolute top-4 left-0 right-0 h-0.5 bg-slate-200" />
24+
25+
{steps.map((label, index) => {
26+
27+
const stepNumber = index + 1;
28+
const active = step >= stepNumber;
29+
30+
return (
31+
32+
<div
33+
key={label}
34+
className="relative flex flex-col items-center flex-1"
35+
>
36+
37+
<div
38+
className={`z-10 h-8 w-8 rounded-full flex items-center justify-center text-sm font-semibold
39+
${active
40+
? 'bg-blue-600 text-white'
41+
: 'bg-slate-200 text-slate-500'
42+
}`}
43+
>
44+
{stepNumber}
45+
</div>
46+
47+
<p className="text-xs mt-2 text-slate-500 text-center">
48+
{label}
49+
</p>
50+
51+
</div>
52+
53+
);
54+
55+
})}
56+
57+
</div>
58+
59+
</div>
60+
61+
);
62+
63+
}

0 commit comments

Comments
 (0)