Skip to content

Commit 036378a

Browse files
authored
Merge pull request #71 from genzz-dev/modularize/patient-bookappointment
modularize book appointment
2 parents 201e2d8 + a9848c0 commit 036378a

6 files changed

Lines changed: 453 additions & 368 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { AlertCircle, CheckCircle } from 'lucide-react';
2+
import DateSelection from './DateSelection';
3+
import TimeSlotSelection from './TimeSlotSelection';
4+
import BookingSummary from './BookingSummary';
5+
6+
const BookingForm = ({
7+
schedule,
8+
selectedDate,
9+
setSelectedDate,
10+
availableSlots,
11+
selectedSlot,
12+
setSelectedSlot,
13+
reason,
14+
setReason,
15+
isTeleconsultation,
16+
setIsTeleconsultation,
17+
loading,
18+
error,
19+
success,
20+
dateAvailability,
21+
doctor,
22+
onBooking,
23+
}) => {
24+
return (
25+
<div className="lg:col-span-2">
26+
<div className="bg-white rounded-lg shadow-md p-6">
27+
<h3 className="text-lg font-semibold text-gray-900 mb-6">Select Date & Time</h3>
28+
29+
{/* Success/Error Messages */}
30+
{error && (
31+
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
32+
<div className="flex items-center">
33+
<AlertCircle className="h-5 w-5 text-red-500 mr-2" />
34+
<p className="text-red-700">{error}</p>
35+
</div>
36+
</div>
37+
)}
38+
39+
{success && (
40+
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
41+
<div className="flex items-center">
42+
<CheckCircle className="h-5 w-5 text-green-500 mr-2" />
43+
<p className="text-green-700">{success}</p>
44+
</div>
45+
</div>
46+
)}
47+
48+
<form onSubmit={onBooking} className="space-y-6">
49+
{/* Date Selection */}
50+
<DateSelection
51+
selectedDate={selectedDate}
52+
setSelectedDate={setSelectedDate}
53+
schedule={schedule}
54+
dateAvailability={dateAvailability}
55+
/>
56+
57+
{/* Time Slot Selection */}
58+
{selectedDate && availableSlots.length > 0 && (
59+
<TimeSlotSelection
60+
availableSlots={availableSlots}
61+
selectedSlot={selectedSlot}
62+
setSelectedSlot={setSelectedSlot}
63+
/>
64+
)}
65+
66+
{/* Reason for Visit */}
67+
<div>
68+
<label htmlFor="reason" className="block text-sm font-medium text-gray-700 mb-2">
69+
Reason for Visit *
70+
</label>
71+
<textarea
72+
id="reason"
73+
value={reason}
74+
onChange={(e) => setReason(e.target.value)}
75+
rows={3}
76+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
77+
placeholder="Please describe your symptoms or reason for consultation..."
78+
required
79+
/>
80+
</div>
81+
82+
{/* Teleconsultation Option */}
83+
{doctor?.availableForTeleconsultation && (
84+
<div className="flex items-center">
85+
<input
86+
id="teleconsultation"
87+
type="checkbox"
88+
checked={isTeleconsultation}
89+
onChange={(e) => setIsTeleconsultation(e.target.checked)}
90+
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
91+
/>
92+
<label htmlFor="teleconsultation" className="ml-2 text-sm text-gray-700">
93+
Request Teleconsultation (Video Call)
94+
</label>
95+
</div>
96+
)}
97+
98+
{/* Booking Summary */}
99+
{selectedDate && selectedSlot && (
100+
<BookingSummary
101+
selectedDate={selectedDate}
102+
selectedSlot={selectedSlot}
103+
isTeleconsultation={isTeleconsultation}
104+
consultationFee={doctor?.consultationFee}
105+
/>
106+
)}
107+
108+
{/* Submit Button */}
109+
<button
110+
type="submit"
111+
disabled={loading || !selectedDate || !selectedSlot || !reason.trim()}
112+
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-blue-700 focus:ring-4 focus:ring-blue-200 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
113+
>
114+
{loading ? (
115+
<div className="flex items-center justify-center">
116+
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
117+
Booking Appointment...
118+
</div>
119+
) : (
120+
'Book Appointment'
121+
)}
122+
</button>
123+
</form>
124+
</div>
125+
</div>
126+
);
127+
};
128+
129+
export default BookingForm;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const BookingSummary = ({ selectedDate, selectedSlot, isTeleconsultation, consultationFee }) => {
2+
return (
3+
<div className="bg-gray-50 p-4 rounded-lg">
4+
<h4 className="font-medium text-gray-900 mb-2">Booking Summary</h4>
5+
<div className="text-sm space-y-1">
6+
<div className="flex justify-between">
7+
<span>Date:</span>
8+
<span>
9+
{new Date(selectedDate).toLocaleDateString('en-US', {
10+
weekday: 'long',
11+
year: 'numeric',
12+
month: 'long',
13+
day: 'numeric',
14+
})}
15+
</span>
16+
</div>
17+
<div className="flex justify-between">
18+
<span>Time:</span>
19+
<span>{selectedSlot}</span>
20+
</div>
21+
<div className="flex justify-between">
22+
<span>Type:</span>
23+
<span>{isTeleconsultation ? 'Teleconsultation' : 'In-person'}</span>
24+
</div>
25+
<div className="flex justify-between font-medium">
26+
<span>Fee:</span>
27+
<span>{consultationFee}</span>
28+
</div>
29+
</div>
30+
</div>
31+
);
32+
};
33+
34+
export default BookingSummary;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Calendar, AlertCircle } from 'lucide-react';
2+
3+
const DateSelection = ({ selectedDate, setSelectedDate, schedule, dateAvailability }) => {
4+
// Generate next 30 days for date selection
5+
const generateAvailableDates = () => {
6+
const dates = [];
7+
const today = new Date();
8+
9+
for (let i = 1; i <= 30; i++) {
10+
const date = new Date(today);
11+
date.setDate(today.getDate() + i);
12+
dates.push(date);
13+
}
14+
15+
return dates;
16+
};
17+
18+
// Check if a date is a working day
19+
const isWorkingDay = (date) => {
20+
if (!schedule?.workingDays) return false;
21+
22+
const dayName = date.toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase();
23+
const workingDay = schedule.workingDays.find((day) => day.day === dayName && day.isWorking);
24+
25+
return !!workingDay;
26+
};
27+
28+
// Check if doctor is on vacation
29+
const isOnVacation = (date) => {
30+
if (!schedule?.vacations) return false;
31+
32+
return schedule.vacations.some((vacation) => {
33+
const vacationStart = new Date(vacation.startDate);
34+
const vacationEnd = new Date(vacation.endDate);
35+
return date >= vacationStart && date <= vacationEnd;
36+
});
37+
};
38+
39+
return (
40+
<div>
41+
<label className="block text-sm font-medium text-gray-700 mb-3">
42+
<Calendar className="h-4 w-4 inline mr-1" />
43+
Select Date
44+
</label>
45+
<div className="grid grid-cols-7 gap-2">
46+
{generateAvailableDates()
47+
.slice(0, 21)
48+
.map((date) => {
49+
const dateStr = date.toISOString().split('T')[0];
50+
const isWorking = isWorkingDay(date);
51+
const onVacation = isOnVacation(date);
52+
const isDisabled = !isWorking || onVacation;
53+
54+
return (
55+
<button
56+
key={dateStr}
57+
type="button"
58+
onClick={() => !isDisabled && setSelectedDate(dateStr)}
59+
disabled={isDisabled}
60+
className={`p-3 text-sm rounded-lg border transition-colors ${
61+
selectedDate === dateStr
62+
? 'bg-blue-600 text-white border-blue-600'
63+
: isDisabled
64+
? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed'
65+
: 'bg-white text-gray-900 border-gray-300 hover:border-blue-300 hover:bg-blue-50'
66+
}`}
67+
>
68+
<div className="font-medium">
69+
{date.toLocaleDateString('en-US', {
70+
day: 'numeric',
71+
})}
72+
</div>
73+
<div className="text-xs">
74+
{date.toLocaleDateString('en-US', {
75+
weekday: 'short',
76+
})}
77+
</div>
78+
</button>
79+
);
80+
})}
81+
</div>
82+
83+
{selectedDate && dateAvailability && !dateAvailability.available && (
84+
<div className="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
85+
<p className="text-sm text-yellow-800">
86+
<AlertCircle className="h-4 w-4 inline mr-1" />
87+
{dateAvailability.reason}
88+
</p>
89+
</div>
90+
)}
91+
</div>
92+
);
93+
};
94+
95+
export default DateSelection;

0 commit comments

Comments
 (0)