Настройка онлайн-записи к врачу на 1С-Битрикс
Сайт медицинской клиники с формой «Оставьте заявку и мы перезвоним» теряет значительную долю конверсии — пользователь хочет выбрать конкретного врача, конкретное время и получить подтверждение немедленно. Онлайн-запись с выбором слота — стандарт для медицины. Реализация на 1С-Битрикс предполагает связь с МИС или управление расписанием внутри самого Битрикс, если МИС нет.
Источник расписания
Первый вопрос при проектировании: откуда берётся расписание?
Вариант A: Расписание в Битрикс. Администратор клиники управляет расписанием врачей через интерфейс в Битрикс. Записи хранятся в Битрикс и передаются в МИС (или не передаются — клиника без МИС). Подходит для небольших клиник без сложной МИС.
Вариант B: Расписание из МИС. Битрикс синхронизирует расписание из МИС каждые N минут. Записи создаются через API МИС. Сайт — только интерфейс, мастер-данные в МИС.
Описываем Вариант A — автономное расписание в Битрикс.
Таблицы расписания
-- Шаблон рабочего времени врача
CREATE TABLE local_doctor_schedule_template (
ID INT AUTO_INCREMENT PRIMARY KEY,
DOCTOR_ID INT NOT NULL, -- ID элемента инфоблока «Врачи»
DAY_OF_WEEK TINYINT NOT NULL, -- 1=Пн, 7=Вс
TIME_FROM TIME NOT NULL,
TIME_TO TIME NOT NULL,
SLOT_DURATION INT DEFAULT 30, -- минут на приём
ACTIVE CHAR(1) DEFAULT 'Y'
);
-- Конкретные слоты (генерируются из шаблона)
CREATE TABLE local_doctor_slots (
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
DOCTOR_ID INT NOT NULL,
SLOT_DATE DATE NOT NULL,
SLOT_TIME TIME NOT NULL,
STATUS ENUM('free','reserved','booked','blocked') DEFAULT 'free',
APPOINTMENT_ID BIGINT,
INDEX idx_doctor_date (DOCTOR_ID, SLOT_DATE, STATUS)
);
-- Записи пациентов
CREATE TABLE local_appointments (
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
DOCTOR_ID INT NOT NULL,
SLOT_ID BIGINT NOT NULL,
USER_ID INT, -- NULL для незарегистрированных
PATIENT_NAME VARCHAR(200),
PATIENT_PHONE VARCHAR(20),
PATIENT_EMAIL VARCHAR(200),
SERVICE_ID INT, -- Услуга (инфоблок услуг)
COMMENT TEXT,
STATUS ENUM('pending','confirmed','cancelled','completed') DEFAULT 'pending',
CREATED_AT DATETIME,
CONFIRMED_AT DATETIME,
CANCELLED_AT DATETIME
);
Генерация слотов из шаблона
Агент, запускаемый ежедневно, генерирует слоты на следующие 30 дней:
function GenerateDoctorSlots(): string
{
$targetDate = (new \DateTime())->modify('+30 days');
$today = new \DateTime();
$templates = LocalDoctorScheduleTemplateTable::getList([
'filter' => ['ACTIVE' => 'Y'],
'select' => ['DOCTOR_ID', 'DAY_OF_WEEK', 'TIME_FROM', 'TIME_TO', 'SLOT_DURATION'],
]);
while ($tpl = $templates->fetch()) {
$date = clone $today;
while ($date <= $targetDate) {
if ((int)$date->format('N') === (int)$tpl['DAY_OF_WEEK']) {
generateSlotsForDay($tpl, $date);
}
$date->modify('+1 day');
}
}
return __FUNCTION__ . '();';
}
function generateSlotsForDay(array $tpl, \DateTime $date): void
{
$from = new \DateTime($date->format('Y-m-d') . ' ' . $tpl['TIME_FROM']);
$to = new \DateTime($date->format('Y-m-d') . ' ' . $tpl['TIME_TO']);
$interval = new \DateInterval('PT' . $tpl['SLOT_DURATION'] . 'M');
$current = clone $from;
while ($current < $to) {
// Не создаём дубли
$exists = LocalDoctorSlotsTable::getCount([
'DOCTOR_ID' => $tpl['DOCTOR_ID'],
'SLOT_DATE' => $date->format('Y-m-d'),
'SLOT_TIME' => $current->format('H:i:s'),
]);
if (!$exists) {
LocalDoctorSlotsTable::add([
'DOCTOR_ID' => $tpl['DOCTOR_ID'],
'SLOT_DATE' => $date->format('Y-m-d'),
'SLOT_TIME' => $current->format('H:i:s'),
'STATUS' => 'free',
]);
}
$current->add($interval);
}
}
Компонент выбора слота
Компонент /local/components/local/appointment.booking/ с шагами:
Шаг 1 — Выбор врача/специализации. Фильтр по специализации из инфоблока врачей. AJAX-обновление списка врачей.
Шаг 2 — Выбор даты и времени. Календарь с подсвеченными доступными датами. При выборе даты — AJAX-запрос доступных слотов:
// AJAX-обработчик /local/ajax/get-slots.php
$doctorId = (int)$_POST['doctor_id'];
$date = $_POST['date']; // Y-m-d
$slots = LocalDoctorSlotsTable::getList([
'filter' => [
'DOCTOR_ID' => $doctorId,
'SLOT_DATE' => $date,
'STATUS' => 'free',
],
'order' => ['SLOT_TIME' => 'ASC'],
'select' => ['ID', 'SLOT_TIME'],
])->fetchAll();
header('Content-Type: application/json');
echo json_encode(['slots' => $slots]);
Шаг 3 — Форма пациента. Имя, телефон, email, комментарий. Для авторизованных — данные подставляются из профиля. Валидация номера телефона.
Шаг 4 — Подтверждение и бронирование.
public function bookSlot(int $slotId, array $patientData, int $serviceId = 0): int
{
$connection = \Bitrix\Main\Application::getConnection();
$connection->startTransaction();
try {
// Атомарное резервирование слота
$connection->queryExecute("
UPDATE local_doctor_slots
SET STATUS = 'reserved'
WHERE ID = ? AND STATUS = 'free'
", [$slotId]);
if ($connection->getAffectedRowsCount() === 0) {
throw new \RuntimeException('Этот слот уже занят');
}
$appointmentId = LocalAppointmentsTable::add([
'DOCTOR_ID' => $this->getSlotDoctorId($slotId),
'SLOT_ID' => $slotId,
'USER_ID' => $patientData['user_id'] ?? null,
'PATIENT_NAME' => $patientData['name'],
'PATIENT_PHONE' => $patientData['phone'],
'PATIENT_EMAIL' => $patientData['email'],
'SERVICE_ID' => $serviceId,
'COMMENT' => $patientData['comment'] ?? '',
'STATUS' => 'confirmed',
])->getId();
// Обновляем слот — статус и привязка к записи
LocalDoctorSlotsTable::update($slotId, [
'STATUS' => 'booked',
'APPOINTMENT_ID' => $appointmentId,
]);
$connection->commitTransaction();
// Уведомления вне транзакции
$this->sendConfirmationSms($patientData['phone'], $appointmentId);
$this->sendConfirmationEmail($patientData['email'], $appointmentId);
return $appointmentId;
} catch (\Exception $e) {
$connection->rollbackTransaction();
throw $e;
}
}
Транзакция с UPDATE ... WHERE STATUS = 'free' и проверкой affectedRows — защита от race condition при одновременной записи двух пользователей на один слот.
Отмена и перенос записи из личного кабинета
Пациент может отменить запись не позднее чем за N часов до приёма:
public function cancelAppointment(int $appointmentId, int $userId): void
{
$appointment = LocalAppointmentsTable::getById($appointmentId)->fetch();
if (!$appointment || (int)$appointment['USER_ID'] !== $userId) {
throw new \RuntimeException('Запись не найдена');
}
$slot = LocalDoctorSlotsTable::getById($appointment['SLOT_ID'])->fetch();
$slotDateTime = new \DateTime($slot['SLOT_DATE'] . ' ' . $slot['SLOT_TIME']);
if ($slotDateTime <= (new \DateTime())->modify('+2 hours')) {
throw new \RuntimeException('Отмена записи возможна не позднее чем за 2 часа');
}
LocalAppointmentsTable::update($appointmentId, ['STATUS' => 'cancelled']);
LocalDoctorSlotsTable::update($appointment['SLOT_ID'], ['STATUS' => 'free', 'APPOINTMENT_ID' => null]);
}
Состав работ
- Таблицы шаблона расписания, слотов, записей
- Агент генерации слотов
- Компонент бронирования: выбор врача → дата → слот → форма → подтверждение
- AJAX-обработчики для слотов
- Защита от race condition при одновременной записи
- SMS/email уведомления, напоминания
- Личный кабинет: история записей, отмена
Сроки: 3–5 недель автономная система без МИС. 6–10 недель с интеграцией МИС.







