Как добавить вкладку в CRM Bitrix24

Функционал модуля CRM Bitrix24 предоставляет широкие возможности для управления вашим бизнесом. Но в ряде случаев, в силу специфики и разнообразия рабочих процессов организаций, этого функционала недостаточно и приходится разрабатывать дополнительные компоненты. В этой статье мы рассмотрим один из способов отображения вашего функционала в интерфейсе CRM сущностей.

Как вы уже могли понять, речь пойдёт о добавлении собственных табов в элементах crm (лидах, сделках, контактах, компаниях и т.д). Разрабатываемый компонент будет предоставлять механизм управления этими вкладками, т.е. позволит определять логику отображения функционала в зависимости от условий (например прав доступа). Сперва определимся с решаемой задачей.

Шаг 1. Постановка задачи

Требуется отобразить компонент списка сотрудников (сам компонент возьмём из статьи про создание списков b24) в crm сделках. Отображать компонент нужно только пользователям с правами на редактирование выбранной сделки. При создании сделки вкладка должна быть заблокирована.

Шаг 2. Концепция и архитектура

Событие

Понятное дело, что для добавления своих вкладок в интерфейс выносить шаблон компонента или, тем более, компонент — избыточно. В модуле CRM есть штатное событие onEntityDetailsTabsInitialized. Это событие происходит в компоненте bitrix:crm.entity.details (именно этот компонент отвечает за отображение элементов crm) при получении массива отображаемых вкладок.

/**
 * file path: bitrix/components/bitrix/crm.entity.details/class.php
 */

public function executeComponent()
{
	...
    $this->arResult['TABS'] = $this->updateTabsByEvent($this->arResult['TABS']);
    ...    
}

protected function updateTabsByEvent(array $tabs): array
{
  $event = new Event('crm', 'onEntityDetailsTabsInitialized', [
    'entityID' => $this->entityID,
    'entityTypeID' => $this->entityTypeID,
    'guid' => $this->guid,
    'tabs' => $tabs,
  ]);
  
  EventManager::getInstance()->send($event);
  
  foreach ($event->getResults() as $result) {
    if ($result->getType() === EventResult::SUCCESS) {
      $parameters = $result->getParameters();
      if (is_array($parameters) && is_array($parameters['tabs'])) {
        $tabs = $parameters['tabs'];
      }
    }
  }

  return $tabs;
}

Менеджер вкладок

Подписавшись на событие можно добавить в существующий массив вкладок свою (можно удалить/изменить другие, но сейчас не об этом 🙂 ).

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

Шаг 3. Реализация

Компонент списка пользователей

Создаём компонент (для примера возьмём готовый компонент из статьи «Отображение данных в виде списков в Bitrix24»). На текущем этапе оставим всё как есть, далее немного доработаем его для корректной работы во вкладке.

Менеджер вкладок

Создадим класс CrmCustomTabManager а пространстве имён Aclips\CustomCrm. (Не забудьте правильно описать автозагрузку классов).

<?php

/**
 * file path: local/php_interface/classes/Aclips/CustomCrm/CrmCustomTabManager.php
 */
  
namespace Aclips\CustomCrm;

use Bitrix\Main\Loader;

Loader::includeModule('crm');

/**
 * Менеджер для работы со вкладками сущностей CRM
 */
class CrmCustomTabManager
{
    /**
     * CRM права текущего пользователя
     * @var \CCrmPerms
     */
    protected \CCrmPerms $userPermissions;

    public function __construct()
    {
        $this->userPermissions = \CCrmPerms::GetCurrentUserPermissions();
    }

    /**
     * Получение актуальных вкладок
     * @param int $elementId
     * @param int $entityTypeID
     * @param array $tabs
     * @return array
     */
    public function getActualEntityTab(int $elementId, int $entityTypeID, array $tabs = []): array
    {
        switch ($entityTypeID) {
            case \CCrmOwnerType::Deal:
                $tabs = $this->getActualDealTabs($tabs, $elementId);
                break;
            case \CCrmOwnerType::Company:
                // @TODO Реализовать получение вкладок для компаний
                break;
            case \CCrmOwnerType::Contact:
                // @TODO Реализовать получение вкладок для контактов
                break;
        }

        return $tabs;
    }

    /**
     * Получение актуальных вкладок элемента сущности "Сделка"
     * @param array $tabs
     * @param int $elementId
     * @return array
     */
    private function getActualDealTabs(array $tabs, int $elementId): array
    {
        $canUpdateDeal = \CCrmDeal::CheckUpdatePermission($elementId, $this->userPermissions);

        if ($canUpdateDeal) {
            $tabs[] = [
                'id' => 'component_users',
                'name' => 'Пользователи',
                'enabled' => !empty($elementId),
                'loader' => [
                    'serviceUrl' => '/local/components/aclips/base.grid/lazyload.ajax.php?&site=' . \SITE_ID . '&' . \bitrix_sessid_get(),
                    'componentData' => [
                        'template' => '',
                        'params' => [
                            // Параметры вызываемого компонента ($arParams)
                        ]
                    ]
                ]
            ];
        }

        return $tabs;
    }
}
  • В конструкторе класса получаем crm права текущего пользователя;
  • метод getActualEntityTab — определит, актуальные вкладки какой сущности нужно вернуть;
  • метод getActualDealTabs — вернёт вкладки по сделке.

Элемент вкладок описывается следующими параметрами:

  • id — уникальный идентификатор таба на странице;
  • name — отображаемое название вкладки;
  • enabled — признак активности вкладки;
  • loader — параметры подключаемого компонента;
    • serviceUrl — адрес обработчика компонента;
    • componentData — параметры компонента;
      • template — шаблон компонента (по умолчанию .default);
      • params — параметры вызываемого компонента ($arParams);

Помимо loader можно указать:

  • html — будет загружен указанный html контент;
  • url — произойдёт переход по указанному url.

Обработчик компонента serviceUrl

Для загрузки функционала выбранным нами способом, создадим файл lazyload.ajax.php в нашем компоненте.

<?php

/**
 * file path: local/components/aclips/base.grid/lazyload.ajax.php
 */
  
use Bitrix\Main\Application;

define('NO_KEEP_STATISTIC', 'Y');
define('NO_AGENT_STATISTIC', 'Y');
define('NO_AGENT_CHECK', true);
define('PUBLIC_AJAX_MODE', true);
define('DisableEventsCheck', true);

$siteID = isset($_REQUEST['site']) ? mb_substr(preg_replace('/[^a-z0-9_]/i', '', $_REQUEST['site']), 0, 2) : '';

if ($siteID !== '') {
    define('SITE_ID', $siteID);
}

require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');

if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true) {
    die();
}

/**
 * Проверка сессии
 */
if (!check_bitrix_sessid()) {
    die();
}

Header('Content-Type: text/html; charset=' . LANG_CHARSET);

global $APPLICATION;
$APPLICATION->ShowAjaxHead();

$request = Application::getInstance()->getContext()->getRequest();

$componentData = $request->get('PARAMS');

if(is_array($componentData)){
    $componentParams = isset($componentData['params']) && is_array($componentData['params']) ? $componentData['params'] : array();
}

$server = $request->getServer();

$ajaxLoaderParams = array(
    'url' => $server->get('REQUEST_URI'),
    'method' => 'POST',
    'dataType' => 'ajax',
    'data' => array('PARAMS' => $componentData)
);

$componentParams['AJAX_LOADER'] = $ajaxLoaderParams;

$APPLICATION->IncludeComponent(
    'bitrix:ui.sidepanel.wrapper',
    '',
    [
        'PLAIN_VIEW' => false,
        'USE_PADDING' => true,
        'POPUP_COMPONENT_NAME' => 'aclips:base.grid',
        'POPUP_COMPONENT_TEMPLATE_NAME' => $componentData['template'] ?? '',
        'POPUP_COMPONENT_PARAMS' => $componentParams
    ]
);

\CMain::FinalActions();

Этот скрипт будет обрабатывать наши запросы, проверять возможность загрузки и, в случае успеха, возвращать содержимое компонента.

Доработка компонента

Наш компонент содержит список, который работает через ajax запросы. Поскольку в явном виде параметры компонента ($arParams) не передаются (загрузка происходит через lazyload.ajax.php), нам потребуется передать их напрямую. Сделать это можно при наступлении JS события Grid::beforeRequest.

/**
 * file path: local/components/aclips/base.grid/templates/.default/template.php
 */

...

<?php if (!empty($arParams['AJAX_LOADER'])) { ?>
    <script>
        BX.addCustomEvent('Grid::beforeRequest', function (gridData, argse) {
            if (argse.gridId != '<?=$arResult['GRID_ID'];?>') {
                return;
            }

            argse.method = 'POST'
            argse.data = <?= \Bitrix\Main\Web\Json::encode($arParams['AJAX_LOADER']['data']) ?>
        });
    </script>
<?php } ?>

Таким образом начальные параметры будут фигурировать во всех действиях списка.

Подключение обработчика (событие)

Как было сказано ранее, подписываться будем на событие модуля crm onEntityDetailsTabsInitialized

<?php
  
/**
 * file path:local/php_interface/events.php
 */

$eventManager = \Bitrix\Main\EventManager::getInstance();

...

$eventManager->addEventHandler('crm', 'onEntityDetailsTabsInitialized', [
        'Aclips\\CustomCrm\\Handler',
        'setCustomTabs'
    ]
);

Событие вызывает метод, который принимает в качестве параметра объект класса Bitrix\Main\EventResult. Сам метод обращается к менеджеру табов и возвращает объект класса EventResult, который содержит данные по актуальным вкладкам.

<?php
  
/**
 * file path: local/php_interface/classes/Aclips/CustomCrm/Handler.php
 */

namespace Aclips\CustomCrm;

use Bitrix\Main\EventResult;
use Bitrix\Main\Event;

/**
 * События, выполняемые в рамках модуля Заявки счёта
 */
class Handler
{
    /**
     * Получение актуальных вкладок элемента CRM
     * @param Event $event
     * @return EventResult
     */
    static function setCustomTabs(Event $event): EventResult
    {
        $entityId = $event->getParameter('entityID');
        $entityTypeID = $event->getParameter('entityTypeID');
        $tabs = $event->getParameter('tabs');

        $crmCustomTabManager = new CrmCustomTabManager();

        $tabs = $crmCustomTabManager->getActualEntityTab($entityId, $entityTypeID, $tabs);

        return new EventResult(EventResult::SUCCESS, [
            'tabs' => $tabs,
        ]);
    }
}

Шаг 4. Проверка

Будучи пользователем с наличием прав на редактирования сделки, перейдём на страницу детального просмотра. Среди стандартных вкладок должна появится новая. При переходе по ней откроется компонент списка пользователей.

Просмотр вкладки собственной вкладки «Пользователи».

Если права на редактирования отсутствуют, то вкладка не должна отобразиться.

Отображаемые вкладки для сотрудника без права редактирования сделки.

Обратите внимание! Если присутствует несколько подписок на событие onEntityDetailsTabsInitialized, то результирующими будут значения, возвращаемые последним обработчиком.

Андрей Николаев

Неприятность эту мы переживём!

Для сохранения вкладок, добавленных ранее откроем доступ к свойству параметров класса через Reflection API и сохраним предыдущие значения.

<?php
  
/**
 * file path: local/php_interface/classes/Aclips/CustomCrm/Handler.php
 */

namespace Aclips\CustomCrm;

use Bitrix\Main\EventResult;
use Bitrix\Main\Event;

/**
 * События, выполняемые в рамках модуля Заявки счёта
 */
class Handler
{
    /**
     * Получение актуальных вкладок элемента CRM
     * @param Event $event
     * @return EventResult
     */
    static function setCustomTabs(Event $event): EventResult
    {
        $entityId = $event->getParameter('entityID');
        $entityTypeID = $event->getParameter('entityTypeID');
        $tabs = $event->getParameter('tabs');

        $reflection = new \ReflectionClass($event);
        $property = $reflection->getProperty('parameters');
        $property->setAccessible(true);
      
        $eventParameters = $property->getValue($event);
      
        $crmCustomTabManager = new CrmCustomTabManager();

        $tabs = $crmCustomTabManager->getActualEntityTab($entityId, $entityTypeID, $tabs);

        // Добавление вкладки с помощью Reflection API
        $eventParameters['tabs'] = $tabs;
        $property->setValue($event, $eventParameters);
      
        return new EventResult(EventResult::SUCCESS, [
            'tabs' => $tabs,
        ]);
    }
}

Добавление вкладок таким способом сохранит значения, добавленные в рамках других обработчиков события onEntityDetailsTabsInitialized.

Итог

Таким образом можно отобразить важные для пользователя данные и собственный функционал на расстоянии одного клика, ограничивая их дополнительной логикой и правами.

Добавить комментарий