Учёт рабочего времени в Битрикс24 обычно воспринимают как инструмент контроля, но у него есть куда более полезное применение — маршрутизация. Звонок должен попадать к тому, кто на месте; задача — назначаться работающему сотруднику, а не тому, кто в отпуске.
Разберём API модуля timeman и оговорки, без которых такая маршрутизация начинает работать против вас.
Введение
Модуль учёта рабочего времени (timeman) в Битрикс24 отслеживает начало и окончание рабочего дня сотрудников. Через API можно:
- Проверить, начал ли сотрудник рабочий день
- Получить список активных сотрудников
- Определить сотрудников отдела, которые сейчас работают
Это полезно для распределения задач, маршрутизации звонков и автоматизации бизнес-процессов.
Статусы рабочего дня
| Статус | Описание |
|---|---|
OPENED | Рабочий день начат |
CLOSED | Рабочий день завершён |
PAUSED | Рабочий день на паузе |
EXPIRED | День истёк (не закрыт вовремя) |
Статус EXPIRED — не «сотрудник работает». Он означает, что рабочий день не был закрыт вовремя: человек ушёл домой и забыл нажать кнопку. Для маршрутизации звонков это принципиально: код, считающий «не CLOSED — значит на месте», начнёт направлять звонки тому, кто уже неделю в отпуске и просто не закрыл последний день.
Явно перечисляйте статусы, которые считаете рабочими (OPENED), а не отсекайте нерабочие.
Базовая проверка статуса
<?php
use Bitrix\Main\Loader;
Loader::includeModule('timeman');
$userId = 123;
$timeMan = new \CTimeManUser($userId);
$settings = $timeMan->GetSettings();
// Проверяем, включён ли учёт времени для пользователя
if ($settings['UF_TIMEMAN']) {
$state = $timeMan->State();
echo match ($state) {
'OPENED' => "Сотрудник на работе",
'CLOSED' => "Рабочий день завершён",
'PAUSED' => "На перерыве",
'EXPIRED' => "День не закрыт",
default => "Статус неизвестен",
};
}Получение всех активных сотрудников
<?php
use Bitrix\Main\Loader;
Loader::includeModule('timeman');
// Получаем всех активных пользователей
$userResult = \CUser::GetList(
'ID', 'ASC',
['ACTIVE' => 'Y'],
['FIELDS' => ['ID', 'NAME', 'LAST_NAME']]
);
$workingUsers = [];
while ($user = $userResult->Fetch()) {
$timeMan = new \CTimeManUser($user['ID']);
$settings = $timeMan->GetSettings();
// Пропускаем пользователей без учёта времени
if (!$settings['UF_TIMEMAN']) {
continue;
}
if ($timeMan->State() === 'OPENED') {
$workingUsers[] = [
'id' => $user['ID'],
'name' => trim($user['NAME'] . ' ' . $user['LAST_NAME']),
];
}
}
echo "Сейчас работают:\n";
foreach ($workingUsers as $user) {
echo " - [{$user['id']}] {$user['name']}\n";
}Класс для работы с учётом времени
<?php
namespace Local\Timeman;
use Bitrix\Main\Loader;
class WorkingUsersService
{
public function __construct()
{
if (!Loader::includeModule('timeman')) {
throw new \RuntimeException('Модуль timeman не установлен');
}
}
/**
* Проверка статуса рабочего дня пользователя
*/
public function getUserState(int $userId): ?string
{
$timeMan = new \CTimeManUser($userId);
$settings = $timeMan->GetSettings();
if (!$settings['UF_TIMEMAN']) {
return null; // Учёт времени отключён
}
return $timeMan->State();
}
/**
* Проверка, работает ли сотрудник
*/
public function isWorking(int $userId): bool
{
return $this->getUserState($userId) === 'OPENED';
}
/**
* Получение всех работающих сотрудников
*/
public function getWorkingUsers(): array
{
$users = $this->getAllActiveUsers();
return array_filter($users, fn($u) => $this->isWorking($u['id']));
}
/**
* Получение работающих сотрудников отдела
*/
public function getWorkingUsersInDepartment(
int $departmentId,
bool $excludeManager = true
): array {
Loader::includeModule('intranet');
$result = [];
$managerId = $excludeManager
? \CIntranetUtils::GetDepartmentManagerID($departmentId)
: null;
$employees = \CIntranetUtils::GetDepartmentEmployees(
$departmentId,
false, // не рекурсивно
false, // не пропускать себя
'Y', // только активные
['ID', 'NAME', 'LAST_NAME', 'WORK_POSITION']
);
while ($employee = $employees->Fetch()) {
// Пропускаем руководителя
if ($managerId && $employee['ID'] == $managerId) {
continue;
}
if ($this->isWorking((int) $employee['ID'])) {
$result[] = [
'id' => (int) $employee['ID'],
'name' => trim($employee['NAME'] . ' ' . $employee['LAST_NAME']),
'position' => $employee['WORK_POSITION'] ?? '',
];
}
}
return $result;
}
/**
* Статистика по статусам в отделе
*/
public function getDepartmentStats(int $departmentId): array
{
Loader::includeModule('intranet');
$stats = [
'OPENED' => 0,
'CLOSED' => 0,
'PAUSED' => 0,
'EXPIRED' => 0,
'NO_TIMEMAN' => 0,
];
$employees = \CIntranetUtils::GetDepartmentEmployees(
$departmentId,
true, // рекурсивно
false,
'Y',
['ID']
);
while ($employee = $employees->Fetch()) {
$state = $this->getUserState((int) $employee['ID']);
if ($state === null) {
$stats['NO_TIMEMAN']++;
} else {
$stats[$state]++;
}
}
return $stats;
}
/**
* Получение информации о рабочем дне
*/
public function getWorkdayInfo(int $userId): ?array
{
$timeMan = new \CTimeManUser($userId);
$settings = $timeMan->GetSettings();
if (!$settings['UF_TIMEMAN']) {
return null;
}
$info = $timeMan->GetCurrentInfo();
if (empty($info)) {
return null;
}
return [
'state' => $timeMan->State(),
'date_start' => $info['DATE_START'] ?? null,
'date_finish' => $info['DATE_FINISH'] ?? null,
'time_start' => $info['TIME_START'] ?? null,
'time_finish' => $info['TIME_FINISH'] ?? null,
'duration' => $info['DURATION'] ?? 0,
'pauses' => $info['PAUSES'] ?? [],
];
}
/**
* Получение всех активных пользователей
*/
private function getAllActiveUsers(): array
{
$users = [];
$result = \CUser::GetList(
'ID', 'ASC',
['ACTIVE' => 'Y'],
['FIELDS' => ['ID', 'NAME', 'LAST_NAME']]
);
while ($user = $result->Fetch()) {
$users[] = [
'id' => (int) $user['ID'],
'name' => trim($user['NAME'] . ' ' . $user['LAST_NAME']),
];
}
return $users;
}
}Примеры использования
Маршрутизация звонка на работающего менеджера
<?php
use Local\Timeman\WorkingUsersService;
$service = new WorkingUsersService();
// Получаем работающих менеджеров отдела продаж
$salesDepartmentId = 10;
$workingManagers = $service->getWorkingUsersInDepartment($salesDepartmentId);
if (empty($workingManagers)) {
// Все менеджеры офлайн — перевод на автоответчик
$targetPhone = 'voicemail';
} else {
// Выбираем случайного работающего менеджера
$manager = $workingManagers[array_rand($workingManagers)];
$targetPhone = getUserPhone($manager['id']);
}Автоназначение задачи работающему сотруднику
<?php
use Local\Timeman\WorkingUsersService;
use Bitrix\Tasks\TaskTable;
$service = new WorkingUsersService();
// Находим работающего сотрудника для новой задачи
$departmentId = 5;
$working = $service->getWorkingUsersInDepartment($departmentId, true);
if (!empty($working)) {
// Назначаем первому работающему
$assignee = $working[0]['id'];
TaskTable::add([
'TITLE' => 'Новая заявка с сайта',
'RESPONSIBLE_ID' => $assignee,
'CREATED_BY' => 1,
'DESCRIPTION' => 'Обработать заявку...',
]);
}Отчёт по отделу
<?php
use Local\Timeman\WorkingUsersService;
$service = new WorkingUsersService();
$stats = $service->getDepartmentStats(10);
echo "=== Статистика отдела продаж ===\n";
echo "На работе: {$stats['OPENED']}\n";
echo "Завершили день: {$stats['CLOSED']}\n";
echo "На перерыве: {$stats['PAUSED']}\n";
echo "Не закрыли вчера: {$stats['EXPIRED']}\n";
echo "Без учёта времени: {$stats['NO_TIMEMAN']}\n";Что учесть при маршрутизации
Часовые пояса. У сотрудника из Владивостока рабочий день начинается тогда, когда в Москве ещё ночь. Сравнение времени в поясе сервера даст неверную картину в распределённой команде — а именно в распределённой команде маршрутизация и нужна.
Не у всех включён учёт времени. Модуль может быть выключен для отдела или для конкретных сотрудников. Статус NO_TIMEMAN в примерах статьи — не ошибка, а нормальная ситуация, и логика маршрутизации должна решать явно: считать таких доступными или пропускать. Молчаливое исключение их из выборки означает, что половина отдела перестанет получать звонки.
Перерыв — не то же самое, что отсутствие. PAUSED длится 15 минут за обедом, и переводить на другого менеджера клиента, который звонит своему, — обычно неверное решение. Для звонков разумнее очередь с ожиданием, для задач — назначение с более поздним сроком.
Про кэширование списка работающих. Совет кэшировать на 5–10 минут разумен для отчёта или дашборда и опасен для маршрутизации звонков: за эти минуты человек успевает уйти на обед, и звонок уйдёт в пустоту. Для операций, где ошибка видна клиенту, проверяйте статус в момент действия.
Права доступа. Данные о рабочем времени других пользователей доступны при правах администратора или соответствующих настройках модуля. В фоновом скрипте без авторизации выборка вернёт пустой результат, а не ошибку, — и маршрутизация «никого не найдёт», выглядя при этом полностью исправной.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.