Главная/Статьи/Битрикс24: получение календарных событий через API

Битрикс24: получение календарных событий через API

Работа с CCalendarEvent для получения списка встреч и событий сотрудника. Фильтрация по датам, владельцу и статусу.

ДМ
Дмитрий Мещеряков
📅 11 октября 2024 г.📖 6 мин чтения

Календарь в Битрикс24 — модуль, у которого мощное внутреннее API и почти нет документации по нему. При этом задачи вокруг него типовые: показать расписание сотрудника, проверить занятость, собрать свободные слоты для онлайн-записи.

Разберём CCalendarEvent::GetList() и три вещи, на которых такие интеграции обычно спотыкаются: часовые пояса, повторяющиеся события и права доступа.

Задача

Получить список календарных событий (встреч, задач, напоминаний) для конкретного сотрудника за определённый период.

Базовый пример

php
<?php
\Bitrix\Main\Loader::includeModule('calendar');

$userId = 286;
$daysBack = 10;

$events = CCalendarEvent::GetList([
    'arFilter' => [
        'FROM_LIMIT' => CCalendar::Date(time() - $daysBack * 24 * 3600, false),
        'TO_LIMIT' => CCalendar::Date(time(), false),
        'DELETED' => 'N',
        'OWNER_ID' => $userId,
    ]
]);

foreach ($events as $event) {
    echo "Событие: {$event['NAME']}\n";
    echo "Начало: {$event['DATE_FROM']}\n";
    echo "Окончание: {$event['DATE_TO']}\n";
    echo "---\n";
}
⚠️ Важно

Часовые пояса — источник ошибки номер один в работе с календарём. У каждого пользователя Битрикс24 свой часовой пояс, события хранятся с привязкой к нему, а PHP работает в поясе сервера. Совпадать эти три вещи не обязаны.

Симптом характерный: расписание сдвинуто ровно на несколько часов, и обнаруживается это, когда сотрудник из другого города жалуется, что видит не свои встречи. Границы периода (FROM_LIMIT, TO_LIMIT) должны собираться в поясе того пользователя, чей календарь вы запрашиваете, а не в серверном.

⚠️ Важно

Повторяющиеся события хранятся одной записью, а не набором. Еженедельная планёрка — это одно событие с правилом повторения. Наивная выборка за период вернёт исходную запись, а не отдельные вхождения на каждую неделю, и расчёт занятости по такой выборке покажет свободное время там, где его нет.

Для задач «занят ли человек» и «какие свободные слоты» это критично: пропущенная еженедельная встреча означает, что клиент запишется на время, когда менеджер на планёрке. Проверьте, разворачивает ли ваш вариант вызова повторения в отдельные вхождения, — прежде чем строить на нём онлайн-запись.

Параметры фильтрации

ПараметрТипОписание
FROM_LIMITdateНачальная дата периода
TO_LIMITdateКонечная дата периода
DELETEDY/NФильтр удалённых событий
OWNER_IDintID владельца/участника
CAL_TYPEstringТип календаря (user, group, company)
SECTIONintID раздела календаря
ACTIVEY/NТолько активные события

Расширенный класс для работы с календарём

php
<?php

namespace Local\Calendar;

use Bitrix\Main\Loader;
use Bitrix\Main\Type\DateTime;

class CalendarService
{
    public function __construct()
    {
        if (!Loader::includeModule('calendar')) {
            throw new \RuntimeException('Модуль calendar не установлен');
        }
    }

    /**
     * Получить события пользователя за период
     */
    public function getUserEvents(
        int $userId,
        \DateTime $dateFrom,
        \DateTime $dateTo,
        array $options = []
    ): array {
        $filter = [
            'FROM_LIMIT' => \CCalendar::Date($dateFrom->getTimestamp(), false),
            'TO_LIMIT' => \CCalendar::Date($dateTo->getTimestamp(), false),
            'DELETED' => 'N',
            'OWNER_ID' => $userId,
            'CAL_TYPE' => 'user',
        ];

        // Фильтр по типу события
        if (!empty($options['event_type'])) {
            $filter['EVENT_TYPE'] = $options['event_type'];
        }

        $events = \CCalendarEvent::GetList([
            'arFilter' => $filter,
            'arOrder' => ['DATE_FROM' => 'ASC'],
        ]);

        return array_map([$this, 'formatEvent'], $events);
    }

    /**
     * Получить события на сегодня
     */
    public function getTodayEvents(int $userId): array
    {
        $today = new \DateTime();
        $today->setTime(0, 0, 0);
        
        $tomorrow = clone $today;
        $tomorrow->modify('+1 day');

        return $this->getUserEvents($userId, $today, $tomorrow);
    }

    /**
     * Получить события на неделю
     */
    public function getWeekEvents(int $userId): array
    {
        $today = new \DateTime();
        $today->setTime(0, 0, 0);
        
        $weekEnd = clone $today;
        $weekEnd->modify('+7 days');

        return $this->getUserEvents($userId, $today, $weekEnd);
    }

    /**
     * Получить предстоящие встречи
     */
    public function getUpcomingMeetings(int $userId, int $limit = 10): array
    {
        $now = new \DateTime();
        $future = clone $now;
        $future->modify('+30 days');

        $events = $this->getUserEvents($userId, $now, $future, [
            'event_type' => 'meeting',
        ]);

        return array_slice($events, 0, $limit);
    }

    /**
     * Проверить занятость пользователя
     */
    public function isUserBusy(int $userId, \DateTime $dateFrom, \DateTime $dateTo): bool
    {
        $events = $this->getUserEvents($userId, $dateFrom, $dateTo);
        
        return !empty($events);
    }

    /**
     * Получить свободные слоты
     */
    public function getFreeSlots(
        int $userId,
        \DateTime $date,
        int $slotDuration = 60, // минуты
        string $workStart = '09:00',
        string $workEnd = '18:00'
    ): array {
        $dayStart = clone $date;
        $dayStart->setTime(0, 0, 0);
        
        $dayEnd = clone $date;
        $dayEnd->setTime(23, 59, 59);

        $events = $this->getUserEvents($userId, $dayStart, $dayEnd);

        // Строим массив занятых интервалов
        $busySlots = [];
        foreach ($events as $event) {
            $busySlots[] = [
                'from' => new \DateTime($event['date_from']),
                'to' => new \DateTime($event['date_to']),
            ];
        }

        // Генерируем свободные слоты
        $freeSlots = [];
        $workStartTime = \DateTime::createFromFormat('H:i', $workStart);
        $workEndTime = \DateTime::createFromFormat('H:i', $workEnd);

        $currentSlot = clone $date;
        $currentSlot->setTime(
            (int)$workStartTime->format('H'),
            (int)$workStartTime->format('i')
        );

        $endOfDay = clone $date;
        $endOfDay->setTime(
            (int)$workEndTime->format('H'),
            (int)$workEndTime->format('i')
        );

        while ($currentSlot < $endOfDay) {
            $slotEnd = clone $currentSlot;
            $slotEnd->modify("+{$slotDuration} minutes");

            $isFree = true;
            foreach ($busySlots as $busy) {
                if ($currentSlot < $busy['to'] && $slotEnd > $busy['from']) {
                    $isFree = false;
                    break;
                }
            }

            if ($isFree) {
                $freeSlots[] = [
                    'from' => $currentSlot->format('H:i'),
                    'to' => $slotEnd->format('H:i'),
                ];
            }

            $currentSlot = $slotEnd;
        }

        return $freeSlots;
    }

    /**
     * Форматирование события
     */
    private function formatEvent(array $event): array
    {
        return [
            'id' => $event['ID'],
            'name' => $event['NAME'],
            'description' => $event['DESCRIPTION'] ?? '',
            'date_from' => $event['DATE_FROM'],
            'date_to' => $event['DATE_TO'],
            'is_meeting' => $event['IS_MEETING'] === 'Y',
            'location' => $event['LOCATION'] ?? '',
            'importance' => $event['IMPORTANCE'] ?? 'normal',
            'attendees' => $event['ATTENDEES_CODES'] ?? [],
        ];
    }
}

Примеры использования

Виджет "События на сегодня"

php
<?php
use Local\Calendar\CalendarService;

$calendar = new CalendarService();
$userId = $USER->GetID();

$todayEvents = $calendar->getTodayEvents($userId);

if (empty($todayEvents)) {
    echo '<p>На сегодня событий нет</p>';
} else {
    echo '<ul class="today-events">';
    foreach ($todayEvents as $event) {
        $time = date('H:i', strtotime($event['date_from']));
        echo "<li><span class='time'>{$time}</span> {$event['name']}</li>";
    }
    echo '</ul>';
}

Проверка доступности для записи

php
<?php
$calendar = new CalendarService();
$managerId = 15;

$requestedTime = new \DateTime('2026-04-10 14:00');
$requestedEnd = new \DateTime('2026-04-10 15:00');

if ($calendar->isUserBusy($managerId, $requestedTime, $requestedEnd)) {
    echo 'Менеджер занят в это время';
} else {
    echo 'Время свободно, можно записать';
}

Получение свободных слотов для записи

php
<?php
$calendar = new CalendarService();
$managerId = 15;
$date = new \DateTime('2026-04-10');

$freeSlots = $calendar->getFreeSlots($managerId, $date, 30); // 30-минутные слоты

echo '<select name="time_slot">';
foreach ($freeSlots as $slot) {
    echo "<option value='{$slot['from']}'>{$slot['from']} - {$slot['to']}</option>";
}
echo '</select>';

Итоги

Календарное API решает задачу, но требует аккуратности в трёх местах, каждое из которых даёт «почти правильный» результат — самый неприятный вид неправильного.

Часовые пояса. Границы периода собирайте в поясе владельца календаря. Расхождение проявится не сразу и не у всех.

Повторяющиеся события. Убедитесь, что они разворачиваются в отдельные вхождения, прежде чем считать по ним занятость.

Права доступа. Выборка ограничена правами текущего пользователя, и в фоновом скрипте, где никто не авторизован, вы получите пустой результат вместо ошибки. Это опаснее исключения: код «работает», данных просто нет.

Кэширование — с осторожностью. Совет кэшировать расписание на 5–10 минут разумен для виджета «события на сегодня» и вреден для формы онлайн-записи: за эти минуты слот успеет занять другой клиент, и вы продадите одно и то же время дважды. Для записи проверяйте занятость в момент подтверждения, а не по кэшу.

💡 Совет

Для внешних интеграций рассмотрите REST-методы calendar.* вместо прямых вызовов классов. Внутреннее API мощнее, но оно не документировано и меняется между версиями портала — а обновления Битрикс24 в облаке происходят без вашего участия. Прямые вызовы уместны в коробочной версии, где вы контролируете момент обновления.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.