Налаштування онлайн-запису до лікаря на 1С-Бітрікс
Сайт медичної клініки з формою «Залиште заявку і ми передзвонимо» втрачає значну частку конверсії — користувач хоче обрати конкретного лікаря, конкретний час і отримати підтвердження негайно. Онлайн-запис із вибором слоту — стандарт для медицини. Реалізація на 1С-Бітрікс передбачає зв'язок з МІС або управління розкладом усередині самого 1С-Бітрікс, якщо МІС немає.
Джерело розкладу
Перше питання при проектуванні: звідки береться розклад?
Варіант A: Розклад у 1С-Бітрікс. Адміністратор клініки управляє розкладом лікарів через інтерфейс у 1С-Бітрікс. Записи зберігаються в 1С-Бітрікс і передаються в МІС (або не передаються — клініка без МІС). Підходить для невеликих клінік без складної МІС.
Варіант B: Розклад з МІС. 1С-Бітрікс синхронізує розклад із МІС кожні N хвилин. Записи створюються через API МІС. Сайт — лише інтерфейс, майстер-дані в МІС.
Описуємо Варіант A — автономний розклад у 1С-Бітрікс.
Таблиці розкладу
-- Шаблон робочого часу лікаря
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 тижнів з інтеграцією МІС.







