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

Битрикс24: получение сотрудников с открытым рабочим днём

Работа с модулем учёта рабочего времени через API. Проверка статуса рабочего дня, выборка активных сотрудников отдела.

ДМ
Дмитрий Мещеряков
📅 21 мая 2022 г.📖 6 мин чтения

Учёт рабочего времени в Битрикс24 обычно воспринимают как инструмент контроля, но у него есть куда более полезное применение — маршрутизация. Звонок должен попадать к тому, кто на месте; задача — назначаться работающему сотруднику, а не тому, кто в отпуске.

Разберём API модуля timeman и оговорки, без которых такая маршрутизация начинает работать против вас.

Введение

Модуль учёта рабочего времени (timeman) в Битрикс24 отслеживает начало и окончание рабочего дня сотрудников. Через API можно:

  • Проверить, начал ли сотрудник рабочий день
  • Получить список активных сотрудников
  • Определить сотрудников отдела, которые сейчас работают

Это полезно для распределения задач, маршрутизации звонков и автоматизации бизнес-процессов.

Статусы рабочего дня

СтатусОписание
OPENEDРабочий день начат
CLOSEDРабочий день завершён
PAUSEDРабочий день на паузе
EXPIREDДень истёк (не закрыт вовремя)
💡 Совет

Статус EXPIRED — не «сотрудник работает». Он означает, что рабочий день не был закрыт вовремя: человек ушёл домой и забыл нажать кнопку. Для маршрутизации звонков это принципиально: код, считающий «не CLOSED — значит на месте», начнёт направлять звонки тому, кто уже неделю в отпуске и просто не закрыл последний день.

Явно перечисляйте статусы, которые считаете рабочими (OPENED), а не отсекайте нерабочие.

Базовая проверка статуса

php
<?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
<?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
<?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
<?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
<?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
<?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.