Календарь в Битрикс24 — модуль, у которого мощное внутреннее API и почти нет документации по нему. При этом задачи вокруг него типовые: показать расписание сотрудника, проверить занятость, собрать свободные слоты для онлайн-записи.
Разберём CCalendarEvent::GetList() и три вещи, на которых такие интеграции обычно спотыкаются: часовые пояса, повторяющиеся события и права доступа.
Задача
Получить список календарных событий (встреч, задач, напоминаний) для конкретного сотрудника за определённый период.
Базовый пример
<?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_LIMIT | date | Начальная дата периода |
TO_LIMIT | date | Конечная дата периода |
DELETED | Y/N | Фильтр удалённых событий |
OWNER_ID | int | ID владельца/участника |
CAL_TYPE | string | Тип календаря (user, group, company) |
SECTION | int | ID раздела календаря |
ACTIVE | Y/N | Только активные события |
Расширенный класс для работы с календарём
<?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
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
$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
$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.