Реалізація бронювання спеціалістів (лікар, майстер, тренер) на веб-сайту
Бронювання спеціаліста складніше, ніж бронювання зали. У спеціаліста може бути нерегулярний розклад, перерви між прийомами, відпустки, замінники. Клієнт хоче бачити найближчий вільний слот, а не перебирати дати вручну.
Модель розкладу спеціаліста
Розклад — це кілька шарів з різним пріоритетом:
-- Базовий тижневий шаблон (повторюється щотижня)
CREATE TABLE specialist_schedules (
id SERIAL PRIMARY KEY,
specialist_id INTEGER NOT NULL,
weekday SMALLINT NOT NULL, -- 0=пн ... 6=нд
start_time TIME NOT NULL,
end_time TIME NOT NULL,
slot_duration INTERVAL DEFAULT '60 minutes',
break_after INTERVAL DEFAULT '0', -- перерва після кожного слота
valid_from DATE,
valid_until DATE
);
-- Переопределення на конкретні дати (відпустки, зміни, відпуски)
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)
);
-- Заблоковані інтервали (обід, внутрішнє засідання)
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)
);
Алгоритм генерації доступних слотів
def get_available_slots(specialist_id: int, date: date) -> list[TimeSlot]:
# 1. Перевірити переопределення на цю дату
override = get_override(specialist_id, date)
if override and override.type == 'day_off':
return []
if override and override.type == 'vacation':
return []
# 2. Отримати базовий розклад
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. Згенерувати теоретичні слоти
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. Видалити заблоковані інтервали
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) # не можна бронювати «прямо зараз»
]
Найближчий доступний слот
Клієнти часто не знають, коли записатися — вони хочуть «як можна скоріше»:
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
Заміна спеціаліста
Якщо клієнт хоче попасти до конкретного спеціаліста, але найближчий слот через 2 тижні, система може запропонувати аналогічного спеціаліста:
def find_same_service_specialists(specialist_id: int) -> list[int]:
# Спеціалісти з тими ж послугами
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])
Інтерфейс запису
Потік клієнта:
- Вибір послуги → фільтрування спеціалістів за послугою
- Вибір спеціаліста (або «будь-хто доступний»)
- Вибір дати — показується лише якщо є хоча б один слот
- Вибір часу — слоти конкретної дати
- Форма контактів → hold → оплата / підтвердження
На кроці вибору дати зручно використовувати inline calendar, де сірі дати — немає слотів, зелені — есть:
// react-day-picker + користувацькі 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)}
/>
Сповіщення та нагадування
Створення брони → email клієнту (підтвердження) + email спеціалісту
За 24 години → SMS + email клієнту
За 2 години → SMS клієнту
Скасування клієнтом → email спеціалісту
Скасування спеціалістом → email + SMS клієнту + пропозиція запису до іншого
Відгуки після візиту
Через 24 години після закінчення прийому — автоматичний лист із проханням залишити відгук. Посилання містить підписаний токен, дійсний 7 днів:
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}"
Строки реалізації
Один спеціаліст, базовий розклад, без оплати — 7–9 робочих днів. Декілька спеціалістів, гнучкий розклад із переопределеннями, заміна спеціаліста, SMS-сповіщення, відгуки — 13–16 робочих днів.







