Configuring the display of doctors' schedules in 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Setting Up Physician Schedule Display on 1C-Bitrix

A physician's page that says "Book by phone" is lost online traffic. Users want to see specific available days and times, not call the reception desk. Displaying the 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.

Schedule Display Models

Model 1: Weekly grid. A grid of weekdays × time slots. Green cells — available, grey — occupied or non-working hours. Clickable — when online booking is enabled.

Model 2: List of nearest available dates. "Next available slot: tomorrow, 14:30." Compact, but less informative.

Model 3: Horizontal date slider. 7–14 days ahead. Popular in mobile UIs.

The choice depends on the clinic's specialisation: for narrow specialists with few available slots — a list of nearest dates; for a general practitioner with a dense schedule — a weekly grid.

Schedule Component

/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");

        // Load slots from table (or from MIS via cache)
        $slots = $this->loadSlots($doctorId, $dateFrom, $dateTo);

        // Group by date
        $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']++;
            }
        }

        // Nearest available slot
        $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

The schedule is data that changes when a new appointment is made. Cache it with auto-invalidation:

// In class.php — enable component cache
$this->arParams['CACHE_TYPE'] = 'A'; // auto
$this->arParams['CACHE_TIME'] = 120; // 2 minutes

// When an appointment is created — clear the component cache for the physician
// In AppointmentService after bookSlot():
\CBitrixComponent::clearComponentCache('local:doctor.schedule', '', [
    'DOCTOR_ID' => $doctorId
]);

For AJAX requests (dynamic updates when switching weeks) — a separate cache via \Bitrix\Main\Data\Cache.

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'] ?>">
    <!-- Week navigation -->
    <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 availability</div>
                <?php endif; ?>
            </div>
        <?php endforeach; ?>
    </div>

    <?php if ($arResult['NEXT_FREE_SLOT']): ?>
        <div class="next-available">
            Next available slot:
            <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 When Switching Weeks

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;
    });
});

Schedule Display on the Physician List Page

The physician catalogue does not require a full schedule — a "Next available: tomorrow" indicator is enough. This is a single SQL query across all physicians:

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

Scope of Work

  • doctor.schedule component with caching
  • Template: weekly grid or date list (selectable)
  • AJAX week navigation
  • "Next available" indicator for the physician list
  • Cache invalidation on slot changes

Timeline: 1–2 weeks for a basic component with a single display mode. 3–4 weeks with multiple display modes, AJAX navigation, and MIS integration.