Реализация бронирования специалистов (врач, мастер, тренер) на сайте
Бронирование специалиста сложнее, чем бронирование зала. У специалиста может быть нерегулярное расписание, перерывы между приёмами, отпуска, замены на другого специалиста. Клиент хочет видеть ближайший доступный слот, а не перебирать даты вручную.
Модель расписания специалиста
Расписание — это несколько слоёв с разным приоритетом:
-- Базовый недельный шаблон (повторяется каждую неделю)
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 + 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)}
/>
Уведомления и напоминания
Создание брони → 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 рабочих дней.







