Сторінка лікаря з текстом «Записатися за телефоном» — це втрачений онлайн-трафік. Користувач хоче бачити конкретні доступні дні та години, а не телефонувати до реєстратури. Відображення розкладу — окреме завдання від онлайн-запису: розклад має бути наочним, швидким і актуальним, навіть якщо кнопка «Записатися» веде до дзвінка. Ми проєктуємо архітектуру так, щоб дані підвантажувалися з МІС або HL-блоків, кешувалися з автоінвалідацією та віддавалися в адаптивному UI. У цій статті — наш підхід з реальним кодом та кейсами для клінік із 50+ лікарями.
Чому готові рішення не підходять?
Більшість готових модулів Маркетплейсу або показують порожню таблицю, або вимагають ручного введення слотів. Вони не інтегруються з внутрішньою МІС, не враховують складні правила прийому (повторювані шаблони, відгули). У результаті — або застарілі дані, або довге завантаження сторінки до 10 секунд.
Які проблеми вирішуємо?
- Консистентність даних. Розклад має синхронізуватися з МІС (1С:Медицина, МедМікс, iMed) через REST або SQL. Якщо в МІС змінили графік, на сайті зміни з'являються в межах TTL кешу (2–5 хвилин), а не через добу.
-
Продуктивність. Сторінка списку лікарів (50+ спеціалістів) не повинна завантажуватися 10 секунд. Використовуємо один SQL-запит для найближчої дати, кешуємо результат теговано. При AJAX-навігації — фонове оновлення через
\Bitrix\Main\Data\Cache. - Мобільна адаптація. Слайдер дат на мобільних — не просто горизонтальний скрол, а компонент з touch-подіями. Для десктопа — тижнева сітка із зеленими комірками.
Як ми реалізуємо компонент розкладу
Стек: PHP 8.1, Bitrix ORM, HL-блоки (local_doctor_slots), тегований кеш. Компонент local:doctor.schedule з параметрами DOCTOR_ID, WEEKS_AHEAD, VIEW_TYPE. Алгоритм завантаження слотів: кожен слот — запис HL-блока з полями DOCTOR_ID, SLOT_DATE, SLOT_TIME, STATUS, PATIENT_ID (якщо зайнятий), SOURCE (manual/api). HL-блоки проходять фільтрацію в 5–10 разів швидше за інфоблоки на великих вибірках (50k+ записів).
Для вибору шаблону відображення орієнтуємося на тип лікаря. При щільному розкладі (терапевти) зручна тижнева сітка з відображенням усіх слотів. Для вузьких спеціалістів (хірург, невролог) компактніший список найближчих дат — він не показує пусті комірки та фокусується на доступних вікнах. На мобільних пристроях застосовуємо слайдер дат із touch-подіями — це інтуїтивно, але дає більше JS-навантаження.
Компонент розкладу
/local/components/local/doctor.schedule/class.php:
class DoctorScheduleComponent extends CBitrixComponent
{
public function executeComponent(): void
{
$doctorId = (int)($this->arParams['DOCTOR_ID'] ?? 0);
$weeksAhead = (int)($this->arParams['WEEKS_AHEAD'] ?? 2);
if (!$doctorId) {
$this->arResult = ['ERROR' => 'Doctor not specified'];
$this->includeComponentTemplate();
return;
}
$dateFrom = new \DateTime();
$dateTo = (clone $dateFrom)->modify("+{$weeksAhead} weeks");
$slots = $this->loadSlots($doctorId, $dateFrom, $dateTo);
$scheduleByDate = [];
foreach ($slots as $slot) {
$date = $slot['SLOT_DATE'];
if (!isset($scheduleByDate[$date])) {
$scheduleByDate[$date] = [
'date' => $date,
'day_name' => $this->getDayName(new \DateTime($date)),
'free_count' => 0,
'slots' => [],
];
}
$scheduleByDate[$date]['slots'][] = $slot;
if ($slot['STATUS'] === 'free') {
$scheduleByDate[$date]['free_count']++;
}
}
$nextFreeSlot = $this->getNextFreeSlot($slots);
$this->arResult = [
'DOCTOR_ID' => $doctorId,
'SCHEDULE' => $scheduleByDate,
'NEXT_FREE_SLOT' => $nextFreeSlot,
'DATE_FROM' => $dateFrom->format('Y-m-d'),
'DATE_TO' => $dateTo->format('Y-m-d'),
];
$this->setResultCacheKeys(['SCHEDULE', 'NEXT_FREE_SLOT']);
$this->includeComponentTemplate();
}
private function loadSlots(int $doctorId, \DateTime $from, \DateTime $to): array
{
return LocalDoctorSlotsTable::getList([
'filter' => [
'DOCTOR_ID' => $doctorId,
'>=SLOT_DATE' => $from->format('Y-m-d'),
'<=SLOT_DATE' => $to->format('Y-m-d'),
],
'order' => ['SLOT_DATE' => 'ASC', 'SLOT_TIME' => 'ASC'],
'select' => ['ID', 'SLOT_DATE', 'SLOT_TIME', 'STATUS'],
])->fetchAll();
}
}
Кешування
Розклад — дані, які змінюються при новому записі. Кешуємо з автоінвалідацією:
$this->arParams['CACHE_TYPE'] = 'A';
$this->arParams['CACHE_TIME'] = 120;
// При створенні запису скидаємо кеш компонента для лікаря
\CBitrixComponent::clearComponentCache('local:doctor.schedule', '', ['DOCTOR_ID' => $doctorId]);
Для AJAX-запитів при перемиканні тижня використовуємо окремий кеш через \Bitrix\Main\Data\Cache.
Шаблон: тижнева сітка
templates/.default/template.php:
$today = new \DateTime();
$daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Нд'];
?>
<div class="doctor-schedule" data-doctor-id="<?= $arResult['DOCTOR_ID'] ?>">
<div class="schedule-nav">
<button class="schedule-prev" data-offset="-7">← Попередній тиждень</button>
<button class="schedule-next" data-offset="7">Наступний тиждень →</button>
</div>
<div class="schedule-grid">
<?php foreach ($arResult['SCHEDULE'] as $dateStr => $dayData): ?>
<?php
$dateObj = new \DateTime($dateStr);
$isPast = $dateObj < $today;
$dayOfWeek = (int)$dateObj->format('N') - 1;
?>
<div class="schedule-day <?= $isPast ? 'past' : '' ?> <?= $dayData['free_count'] > 0 ? 'has-slots' : 'no-slots' ?>">
<div class="day-header">
<span class="day-name"><?= $daysOfWeek[$dayOfWeek] ?></span>
<span class="day-date"><?= $dateObj->format('d.m') ?></span>
</div>
<?php if ($dayData['free_count'] > 0): ?>
<div class="slots-container">
<?php foreach ($dayData['slots'] as $slot): ?>
<?php if ($slot['STATUS'] === 'free'): ?>
<button class="slot-btn free"
data-slot-id="<?= $slot['ID'] ?>"
data-time="<?= substr($slot['SLOT_TIME'], 0, 5) ?>">
<?= substr($slot['SLOT_TIME'], 0, 5) ?>
</button>
<?php endif; ?>
<?php endforeach; ?>
</div>
<div class="day-free-count"><?= $dayData['free_count'] ?> місця</div>
<?php else: ?>
<div class="no-slots-label">Немає запису</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php if ($arResult['NEXT_FREE_SLOT']): ?>
<div class="next-available">
Найближчий вільний запис:
<strong><?= date('d.m.Y', strtotime($arResult['NEXT_FREE_SLOT']['SLOT_DATE'])) ?></strong>
о <strong><?= substr($arResult['NEXT_FREE_SLOT']['SLOT_TIME'], 0, 5) ?></strong>
</div>
<?php endif; ?>
</div>
AJAX-підвантаження при перемиканні тижня
document.querySelectorAll('.schedule-prev, .schedule-next').forEach(btn => {
btn.addEventListener('click', async function() {
const doctorId = document.querySelector('.doctor-schedule').dataset.doctorId;
const offset = parseInt(this.dataset.offset);
const dateFrom = new Date(currentDateFrom);
dateFrom.setDate(dateFrom.getDate() + offset);
const res = await fetch('/local/ajax/doctor-schedule.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
doctor_id: doctorId,
date_from: dateFrom.toISOString().split('T')[0],
sessid: BX.bitrix_sessid()
})
});
const data = await res.json();
renderScheduleGrid(data.schedule);
currentDateFrom = dateFrom;
});
});
Відображення розкладу на сторінці списку лікарів
На сторінці каталогу лікарів повний розклад не потрібен — достатньо індикатора «Найближчий запис: завтра». Це один SQL-запит по всіх лікарях:
SELECT DOCTOR_ID, MIN(CONCAT(SLOT_DATE, ' ', SLOT_TIME)) as NEXT_FREE_SLOT
FROM local_doctor_slots
WHERE STATUS = 'free' AND SLOT_DATE >= CURDATE()
GROUP BY DOCTOR_ID
Як інтегрувати розклад із МІС?
Інтеграція з МІС — ключовий етап. Якщо МІС надає REST API, налаштовуємо агент із періодичністю 1–5 хвилин. При прямому SQL-доступі — створюємо матеріалізоване представлення або тригери. У будь-якому разі, після синхронізації скидаємо тегований кеш відповідного лікаря.
Що входить у роботу?
- Проєктування схеми HL-блоків та системи кешування.
- Розробка компонента
local:doctor.scheduleз двома шаблонами (тижнева сітка та список). - Налаштування AJAX-підвантаження та мобільного слайдера.
- Інтеграція з МІС (REST/SQL) та агент синхронізації.
- Тестування на реальних даних (від 50 лікарів) та навантажувальне тестування.
- Документація з експлуатації та адміністрування.
Порівняння шаблонів відображення
| Параметр | Тижнева сітка | Список дат |
|---|---|---|
| Підходить для | Щільний розклад (терапевти) | Рідкі слоти (хірурги) |
| Інформативність | Показує всі зайняті та вільні слоти | Фокусується на доступних датах |
| Мобільна адаптація | Слайдер дат із touch-подіями | Вертикальний список |
| Швидкість завантаження | Вимагає більше даних (усі слоти) | Менше даних (тільки дати) |
Продуктивність: HL-блоки vs Інфоблоки
| Параметр | HL-блоки | Інфоблоки |
|---|---|---|
| Час запиту 50k записів | ~150 мс | ~800 мс |
| Гнучкість індексів | Індекси на будь-які поля | Тільки стандартні |
| Складність інтеграції | Проста ORM | Вимагає метаполів |
| Підходить для | Табличних даних (слоти) | Контентних даних (новини) |
Згідно з документацією Бітрікс, HL-блоки оптимізовані для табличних даних і виконують вибірку в 5–10 разів швидше за інфоблоки.
Типові помилки при реалізації
- Зберігати розклад в інфоблоці (повільна фільтрація). Використовуємо HL-блоки — ORM працює швидше, індекси ставити простіше.
- Не інвалідувати кеш при записі через адмінку або API. Використовуємо події
OnAfterAdd/Update/DeleteHL-блока. - Ігнорувати часові пояси. Лікарі можуть працювати в різних філіалах — зберігаємо час в UTC, перетворюємо на клієнті.
Як почати?
Ми реалізували 20+ проєктів для клінік з гарантією продуктивності. Зв'яжіться — обговоримо ваш проєкт і покажемо демо з вашими даними. Отримайте консультацію безплатно.







