Implementing Specialist Booking (Doctor, Master, Trainer) on a Website
Booking a specialist is more complex than booking a hall. A specialist may have an irregular schedule, breaks between appointments, vacations, substitutions by another specialist. Clients want to see the next available slot, not manually browse dates.
Specialist Schedule Model
Schedule is several layers with different priority:
-- Basic weekly template (repeats every week)
CREATE TABLE specialist_schedules (
id SERIAL PRIMARY KEY,
specialist_id INTEGER NOT NULL,
weekday SMALLINT NOT NULL, -- 0=Mon ... 6=Sun
start_time TIME NOT NULL,
end_time TIME NOT NULL,
slot_duration INTERVAL DEFAULT '60 minutes',
break_after INTERVAL DEFAULT '0', -- break after each slot
valid_from DATE,
valid_until DATE
);
-- Overrides for specific dates (days off, shifts, vacations)
CREATE TABLE specialist_overrides (
id SERIAL PRIMARY KEY,
specialist_id INTEGER NOT NULL,
override_date DATE NOT NULL,
override_type VARCHAR(20), -- 'day_off', 'custom_hours', 'vacation'
start_time TIME,
end_time TIME,
reason VARCHAR(255)
);
-- Blocked intervals (lunch, internal meeting)
CREATE TABLE specialist_blocks (
id SERIAL PRIMARY KEY,
specialist_id INTEGER NOT NULL,
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
reason VARCHAR(100)
);
Available Slots Generation Algorithm
def get_available_slots(specialist_id: int, date: date) -> list[TimeSlot]:
# 1. Check override for this date
override = get_override(specialist_id, date)
if override and override.type == 'day_off':
return []
if override and override.type == 'vacation':
return []
# 2. Get base schedule
if override and override.type == 'custom_hours':
work_start, work_end = override.start_time, override.end_time
slot_duration = override.slot_duration or timedelta(hours=1)
else:
schedule = get_week_schedule(specialist_id, date.weekday())
if not schedule:
return []
work_start = schedule.start_time
work_end = schedule.end_time
slot_duration = schedule.slot_duration
break_after = schedule.break_after
# 3. Generate theoretical slots
slots = []
current = datetime.combine(date, work_start)
end_dt = datetime.combine(date, work_end)
while current + slot_duration <= end_dt:
slots.append(TimeSlot(start=current, end=current + slot_duration))
current += slot_duration + (break_after or timedelta(0))
# 4. Remove blocked intervals
blocks = get_blocks(specialist_id, date)
bookings = get_confirmed_bookings(specialist_id, date)
busy = blocks + bookings
return [
slot for slot in slots
if not any(slot.overlaps(b) for b in busy)
and slot.start >= datetime.utcnow() + timedelta(minutes=30) # can't book "right now"
]
Next Available Slot
Clients often don't know when to book — they want "as soon as possible":
def find_next_available(specialist_id: int, from_date: date = None, max_days: int = 30):
start = from_date or date.today()
for delta in range(max_days):
check_date = start + timedelta(days=delta)
slots = get_available_slots(specialist_id, check_date)
if slots:
return slots[0], check_date
return None, None
Specialist Substitution
If the client wants to see a specific specialist but the next slot is in 2 weeks, the system can offer an analogous specialist:
def find_same_service_specialists(specialist_id: int) -> list[int]:
# Specialists with the same services
return db.query("""
SELECT DISTINCT s2.id
FROM specialist_services ss1
JOIN specialist_services ss2 ON ss1.service_id = ss2.service_id
JOIN specialists s2 ON ss2.specialist_id = s2.id
WHERE ss1.specialist_id = %s AND s2.id != %s AND s2.is_active = true
""", [specialist_id, specialist_id])
Booking Interface
Client flow:
- Service selection → filter specialists by service
- Specialist selection (or "any available")
- Date selection — shown only if at least one slot exists
- Time selection — slots for specific date
- Contact form → hold → payment / confirmation
On the date selection step, it's convenient to use inline calendar where gray dates have no slots, green have slots:
// react-day-picker + custom modifiers
const disabledDays = dates.filter(d => !d.hasSlots);
const availableDays = dates.filter(d => d.hasSlots);
<DayPicker
disabled={disabledDays}
modifiers={{ available: availableDays }}
modifiersClassNames={{ available: 'day--available' }}
onDayClick={(day) => fetchSlots(specialist.id, day)}
/>
Notifications and Reminders
Booking creation → email to client (confirmation) + email to specialist
24 hours before → SMS + email to client
2 hours before → SMS to client
Client cancellation → email to specialist
Specialist cancellation → email + SMS to client + offer to book another
Reviews After Visit
24 hours after the appointment ends — automatic email requesting a review. Link contains a signed token valid for 7 days:
token = jwt.encode({
'booking_id': booking.id,
'exp': datetime.utcnow() + timedelta(days=7),
}, SECRET_KEY, algorithm='HS256')
review_url = f"https://example.com/review?token={token}"
Implementation Timeline
One specialist, basic schedule, without payment — 7–9 business days. Multiple specialists, flexible schedule with overrides, specialist substitution, SMS notifications, reviews — 13–16 business days.







