Страница врача с текстом «Записаться по телефону» — это потерянный онлайн-трафик. Пользователь хочет видеть конкретные доступные дни и часы, а не звонить в регистратуру. Отображение расписания — отдельная задача от онлайн-записи: расписание должно быть наглядным, быстрым и актуальным, даже если кнопка «Записаться» ведет к звонку. Мы проектируем архитектуру так, чтобы данные подгружались из МИС или 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+ проектов для клиник с гарантией производительности. Свяжитесь — обсудим ваш проект и покажем demo с вашими данными. Получите консультацию бесплатно.







