Сторінка лікаря з текстом «Записатися за телефоном» — це втрачений онлайн-трафік. Користувач хоче бачити конкретні доступні дні та години, а не телефонувати до реєстратури. Відображення розкладу — окреме завдання від онлайн-запису: розклад має бути наочним, швидким і актуальним, навіть якщо кнопка «Записатися» веде до дзвінка. Ми проєктуємо архітектуру так, щоб дані підвантажувалися з МІС або 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+ проєктів для клінік з гарантією продуктивності. Зв'яжіться — обговоримо ваш проєкт і покажемо демо з вашими даними. Отримайте консультацію безплатно.







