Bitrix Doctor Schedule Component: Caching and MIS Integration
A doctor page with a "Book by phone" text is lost online traffic. Users want to see specific available days and times, not call the reception desk. Displaying a schedule is a separate task from online booking: the schedule must be clear, fast, and up-to-date, even if the "Book" button leads to a phone call. We design the architecture so that data loads from the MIS or highload blocks, caches with auto-invalidation, and renders in an adaptive UI. This article covers our approach with real code and cases for clinics with 50+ doctors.
Why Ready-Made Solutions Do Not Work
Most ready-made Marketplace modules either show an empty table or require manual slot entry. They do not integrate with your internal MIS, do not handle complex appointment rules (recurring patterns, days off). The result is either outdated data or page load times up to 10 seconds.
What Problems We Solve
Data Consistency
The schedule must synchronize with the MIS (1C:Medicine, MedMix, iMed) via REST or SQL. If a schedule change occurs in the MIS, it appears on the site within the TTL cache period (2–5 minutes), not after a day.
Performance
A page listing 50+ doctors should not take 10 seconds to load. In a recent project we reduced load time from 3.2s to 0.4s. We use a single SQL query for the nearest date, cache the result with tags. For AJAX navigation, we update in the background using \Bitrix\Main\Data\Cache.
Mobile Adaptation
A date slider on mobile is not just horizontal scrolling—it is a component with touch events. For desktop, a weekly grid with green cells.
How We Implement the Schedule Component
Stack: PHP 8.1, Bitrix ORM, highload blocks (local_doctor_slots), tagged cache. Component local:doctor.schedule with parameters DOCTOR_ID, WEEKS_AHEAD, VIEW_TYPE. Slot loading algorithm: each slot is a highload block record with fields DOCTOR_ID, SLOT_DATE, SLOT_TIME, STATUS, PATIENT_ID (if booked), SOURCE (manual/api). Highload blocks filter 5–10 times faster than information blocks on large datasets (50k+ records).
For template selection we consider doctor type. For dense schedules (therapists), a weekly grid showing all slots works best. For narrow specialists (surgeon, neurologist), a compact list of upcoming dates is more suitable—it hides empty cells and focuses on available windows. On mobile devices we use a date slider with touch events; it is intuitive but imposes a heavier JS load.
Component Code
/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();
}
}
Caching
Schedule data changes with each new booking. We cache with auto-invalidation:
$this->arParams['CACHE_TYPE'] = 'A';
$this->arParams['CACHE_TIME'] = 120;
// On slot creation, clear the component cache for the doctor
\CBitrixComponent::clearComponentCache('local:doctor.schedule', '', ['DOCTOR_ID' => $doctorId]);
For AJAX requests when switching weeks, we use a separate cache via \Bitrix\Main\Data\Cache.
Detailed Caching Mechanism
The component uses tag-based caching with autoclaring on highload block events. A cache tag is assigned per doctor. When a slot is created or updated via the admin panel or API, the cache tag is invalidated, ensuring the next request fetches fresh data. The TTL of 120 seconds provides a balance between freshness and load.Template: Weekly Grid
templates/.default/template.php:
$today = new \DateTime();
$daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
?>
<div class="doctor-schedule" data-doctor-id="<?= $arResult['DOCTOR_ID'] ?>">
<div class="schedule-nav">
<button class="schedule-prev" data-offset="-7">← Previous week</button>
<button class="schedule-next" data-offset="7">Next week →</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'] ?> slots</div>
<?php else: ?>
<div class="no-slots-label">No appointments</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php if ($arResult['NEXT_FREE_SLOT']): ?>
<div class="next-available">
Next available appointment:
<strong><?= date('d.m.Y', strtotime($arResult['NEXT_FREE_SLOT']['SLOT_DATE'])) ?></strong>
at <strong><?= substr($arResult['NEXT_FREE_SLOT']['SLOT_TIME'], 0, 5) ?></strong>
</div>
<?php endif; ?>
</div>
AJAX Loading for Week Switching
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;
});
});
Displaying Schedule on the Doctor List Page
On the doctor catalog page, full schedule is unnecessary—just an indicator "Next appointment: tomorrow". This is one SQL query across all doctors:
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
How to Integrate the Schedule with the MIS?
MIS integration is a key step. If the MIS provides a REST API, we configure an agent with a period of 1–5 minutes. With direct SQL access, we create a materialized view or triggers. In any case, after synchronization we invalidate the tagged cache for the corresponding doctor.
Implementation Process
- Requirements analysis and slot data model design.
- Development of highload block schema with indexing.
- Component coding (weekly grid + list templates).
- AJAX and mobile date slider implementation.
- MIS integration (REST or SQL) with sync agent.
- Cache tuning and load testing (over 100 doctors tested).
- Deployment and documentation.
What's Included in the Deliverables
- Highload block schema design and caching system.
-
local:doctor.schedulecomponent with two templates (weekly grid and list). - AJAX loading and mobile date slider.
- MIS integration (REST/SQL) and synchronization agent.
- Testing with real data (50+ doctors) and load testing.
- Operation and administration documentation.
- Admin access and training session.
- One month post-launch support.
Template Comparison
| Parameter | Weekly Grid | Date List |
|---|---|---|
| Best for | Dense schedules (therapists) | Sparse slots (surgeons) |
| Informativeness | Shows all booked and free slots | Focuses on available dates |
| Mobile adaptation | Date slider with touch events | Vertical list |
| Load speed | Requires more data (all slots) | Less data (only dates) |
Performance: Highload Blocks vs. Information Blocks
| Parameter | Highload Blocks | Information Blocks |
|---|---|---|
| Query time for 50k records | ~150 ms | ~800 ms |
| Index flexibility | Indexes on any fields | Only standard indexes |
| Integration complexity | Simple ORM | Requires meta fields |
| Suitable for | Tabular data (slots) | Content data (news) |
According to Bitrix documentation (https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&CHAPTER_ID=04225), highload blocks are optimized for tabular data and perform 5–10 times faster than information blocks.
Typical Implementation Mistakes
- Storing schedule in an information block (slow filtering). We use highload blocks—ORM works faster, indexes are easier to set.
- Not invalidating cache when booking via admin panel or API. We use
OnAfterAdd/Update/Deletehighload block events. - Ignoring time zones. Doctors may work in different branches—store times in UTC, convert on the client side.
How to Get Started?
We have implemented 20+ projects for clinics with a performance guarantee. Development cost starts from $1,200 per component. Contact us to discuss your project and we can show a demo with your data. Get a free consultation.







