Доработки в Битрикс24 без модулей — как стройка без чертежей: что-то получится, но надолго ли?
Всем привет, я Сергей — ведущий программист в e-commerce агентстве KISLOROD.
В первой части я рассказал, как разработать базовую структуру модуля и начать разработку под Битрикс24. Теперь — следующий шаг.
Битрикс24 можно доработать «под себя», но если задачи сложные — без модулей вы далеко не уедете. Модули позволяют объединить доработки в один понятный, управляемый блок. Это удобно как для команды разработки, так и для поддержки в будущем.
Что нужно знать о модулях
Модуль — это структура, которая объединяет весь функционал: бизнес-логику, интерфейсы и вспомогательные файлы в одном месте. У каждого файла в модуле есть своя роль, подробную структуру можно посмотреть в официальной документации — особенно полезно, если вы впервые работаете с модулями.
Структура модулей в Битрикс24 и в классической коробочной версии (БУС) практически идентична. Поэтому если вы уже работали с БУС, адаптация под коробочный Битрикс24 будет несложной.
Один модуль — множество решений
Рассматривать дальнейшие кейсы будем на примере разработанного модуля kislorod.d7, который был описан в прошлой статье. Все кейсы основаны на реальных задачах, которые мы решали для клиентов, но данные в них вымышленные. Некоторые кейсы можно реализовать и без участия модуля, стандартными средствами, но целью является ознакомление с API, событиями и модульной архитектурой. К тому же, возможно, стандартных средств для реализации задуманных задач может не хватить.
Кейс. Пропущенные звонки менеджеров в Битрикс24.
Проблема клиента:
Менеджеры пропускают звонки от клиентов, теряя потенциальные сделки.
Задача:
Автоматически напоминать менеджерам о пропущенных звонках. Уведомление должно содержать:
-
ссылку на карточку клиента;
-
ссылки на все его сделки;
-
текущие статусы этих сделок.
Как реализовать уведомление о пропущенном звонке
Для отслеживания окончания звонка используем событие onCallEnd модуля voximplant.
Подписываемся на него в инсталлере модуля:
\Bitrix\Main\EventManager::getInstance()->registerEventHandler('voximplant', 'onCallEnd', $this->MODULE_ID, '\o2k\d7\Events\Voximplant', 'endCall');
Далее при срабатывании события:
-
Проверяем статус звонка. У пропущенного звонка статус 304.
-
Если статус совпал, получаем CALL_ID.
-
По CALL_ID запрашиваем номер телефона клиента из таблицы StatisticTable модуля voximplant.
-
Используем номер телефона, чтобы найти контакт в CRM. Пока работаем только с контактами, компании не обрабатываем.
-
Находим контакт — формируем уведомление с нужными ссылками и отправляем ответственному менеджеру.
(int)$contactId = Voximplant::getOwnerIdByPhone($arCall['PHONE_NUMBER']);
Если нашелся контакт с привязанным номером телефона, получим его данные для подстановки в будущее сообщение:
$ownerContact = static::getExtranetClientById($contactId);
А также получим все сделки контакта, для наглядности:
$ownerDeals = Entities\DealField::getHTMLValues([], $contactId);
Далее формируем сообщение:
$message = Loc::getMessage(Settings::$langPrefix.'_MESSAGE', ['#CLIENT#' => $ownerContact, '#DEALS#' => $ownerDeals]);
И отправляем его пользователю с ID 1(по умолчанию админу):
Скрытый текст
$result = \CIMNotify::Add([ 'TO_USER_ID' => 1, //отправлять будем допустим админу 'FROM_USER_ID' => 0, 'NOTIFY_TYPE' => IM_NOTIFY_SYSTEM, 'NOTIFY_MODULE' => Settings::$mid, 'NOTIFY_TAG' => 'missed-call'.intval($contactId).'-time-'.time(), 'NOTIFY_MESSAGE' => $message ]);
Событие onCallEnd — это гибкий инструмент. Он позволяет не только отправлять уведомления, но и запускать любой нужный бизнес-процесс. Вот примеры:
-
Автоматический запуск бизнес-процессов — например, постановка задачи по итогам пропущенного звонка.
-
Аналитика эффективности менеджеров — можно считать количество пропущенных звонков и строить отчеты по каждому сотруднику.
-
Автоматическое создание лидов или сделок — звонок пропущен? Создаем лид с привязкой к клиенту.
-
Расширение сценариев обработки звонков — например, сразу переводить клиента на другого менеджера, если у текущего много пропущенных.
-
И прочие кейсы коих бесконечное множество.
Сам класс с методами у нас будет выглядеть следующим образом:
Скрытый текст
<?php namespace o2k\d7\Events; use o2k\d7\Conf\Settings, o2k\d7\Entities, o2k\d7\Events\Voximplant, Bitrix\Main\Loader, Bitrix\Main\ORM, Bitrix\Iblock\ORM as IblockORM, Bitrix\Crm, Bitrix\Voximplant\StatisticTable, Bitrix\Main\Localization\Loc, Bitrix\Main\Config\Option; Loc::loadMessages(__FILE__); class Voximplant { private $fail = 304; public static function endCall($arFields) { if( Loader::includeModule(Settings::$voximplantMid) && $arFields['CALL_FAILED_CODE'] == self::$fail && intval($arFields['CALL_TYPE']) === \CVoxImplantMain::CALL_INCOMING ) { $callID = (!empty($arFields['CALL_ID'])) ? trim($arFields['CALL_ID']) : false; if($callID) { if(!Loader::includeModule(Settings::$voximplantMid)) { return false; } $arCall = StatisticTable::getList([ 'select' => ['PHONE_NUMBER'], 'filter' => ['=CALL_ID' => $callID], 'limit' => 1 ])->fetch(); if(!empty($arCall['PHONE_NUMBER'])) { $message = ''; (int)$contactId = Voximplant::getOwnerIdByPhone($arCall['PHONE_NUMBER']); if($contactId > 0) { $ownerContact = static::getExtranetClientById($contactId); $ownerDeals = Entities\DealField::getHTMLValues([], $contactId); $message = Loc::getMessage(Settings::$langPrefix.'_MESSAGE', ['#CLIENT#' => $ownerContact, '#DEALS#' => $ownerDeals]); } if(Loader::IncludeModule("im") && !empty($message)) { $result = \CIMNotify::Add([ 'TO_USER_ID' => 1, //отправлять будем допустим админу 'FROM_USER_ID' => 0, 'NOTIFY_TYPE' => IM_NOTIFY_SYSTEM, 'NOTIFY_MODULE' => Settings::$mid, 'NOTIFY_TAG' => 'missed-call'.intval($contactId).'-time-'.time(), 'NOTIFY_MESSAGE' => $message ]); } } } } return true; } public static function getOwnerIdByPhone(string $phone):int { $result = 0; $query = new IblockORM\Query(Crm\FieldMultiTable::getEntity()); $query->setSelect(['*']); $query->setFilter([ 'TYPE_ID' => 'PHONE', '%VALUE' => trim($phone), 'ENTITY_ID' => \CCrmOwnerType::ContactName ]); $query->setLimit(1); $getOwner = ORM\Query\QueryHelper::decompose($query); if(is_object($getOwner) && count($getOwner) > 0) { foreach($getOwner as $owner) { $owner = $owner->collectValues(ORM\Objectify\Values::ALL, ORM\Fields\FieldTypeMask::ALL, true); $result = $owner['ELEMENT_ID']; } } return $result; } public static function getExtranetClientById(int $id):string { $result = 0; $query = new IblockORM\Query(Crm\ContactTable::getEntity()); $query->setSelect([ 'FULL_NAME' ]); $query->setFilter([ '=ID' => $id ]); $query->setLimit(1); $getOwner = ORM\Query\QueryHelper::decompose($query); if(is_object($getOwner) && count($getOwner) > 0) { $link = Option::get(Settings::$intranetMid, 'path_user', 'path_user', 's1'); foreach($getOwner as $owner) { $owner = $owner->collectValues(ORM\Objectify\Values::ALL, ORM\Fields\FieldTypeMask::ALL, true); if(!empty($link)) { $result = \CCrmViewHelper::PrepareUserBaloonHtml([ 'PREFIX' => (int)$owner['ID'].'_PREF', 'USER_ID' => (int)$owner['ID'], 'USER_NAME' => $owner['FULL_NAME'], 'USER_PROFILE_URL' => str_replace('#USER_ID#', (int)$owner['ID'], $link) ]); } } } return $result; } }
Вот такое сообщение получаем в уведомлениях:
На визуал опять не обращаем внимание, главное сейчас — понять, как это работает.
Кейс. Вывод кастомной информации по клиенту, сделке или компании.
Проблема клиента:
Менеджеры тратят много времени на поиск данных в сторонней системе. Это замедляет работу и снижает качество общения с клиентом во время звонков.
Задача:
Добавить закладку в карточке сделки, чтобы ускорить поиск информации и выводить кастомные данные по клиенту.
Часто возникает необходимость выводить индивидуальные данные по клиенту, сделке или компании. Для этого можно встроить вкладку в интерфейс нужной карточки. В стандартном компоненте bitrix:crm.entity.details для этого используется событие onEntityDetailsTabsInitialized, с помощью которого можно модифицировать массив меню и добавлять свои вкладки.
Скрытый текст
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; }
Рассмотрим на примере, как вывести данные из ORM-таблицы модуля с фильтрацией по ответственному сотруднику. Задача — получить все сделки, где ответственный совпадает с текущим в карточке сделки. Что нам потребуется:
-
подписаться на событие;
-
получить ID текущей сущности;
-
определить по этому ID ответственного;
-
передать его в обработчик компонента (ajax.php).
Начнем с подписки на событие. В файле install.php модуля, в функции InstallEvents(), подпишемся на событие.
\Bitrix\Main\EventManager::getInstance()->registerEventHandler('crm', 'onEntityDetailsTabsInitialized', $this->MODULE_ID, 'o2k\\d7\\Events\CrmMenu','getTab');
Создадим класс CrmMenu и метод getTab, который будет модифицировать меню, и запрашивать необходимую нам информацию из обработчика.
Еще нам понадобится тот самый обработчик ajax.php компонента. Он будет выглядеть следующим образом:
Скрытый текст
<?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'] : []; } $server = $request->getServer(); $ajaxLoaderParams = [ 'url' => $server->get('REQUEST_URI'), 'method' => 'POST', 'dataType' => 'ajax', 'data' => ['PARAMS' => $componentData] ]; $componentParams['AJAX_LOADER'] = $ajaxLoaderParams; $APPLICATION->IncludeComponent( 'bitrix:ui.sidepanel.wrapper', '', [ 'PLAIN_VIEW' => false, 'USE_PADDING' => true, 'POPUP_COMPONENT_NAME' => 'o2k:o2k.test', 'POPUP_COMPONENT_TEMPLATE_NAME' => $componentData['template'] ?? '', 'POPUP_COMPONENT_PARAMS' => $componentParams ] ); \CMain::FinalActions();
Думаю, здесь в целом понятно, что происходит. Но один момент всё же стоит пояснить — это компонент bitrix:ui.sidepanel.wrapper.
Мы выше говорили о выводе данных из ORM-таблицы, а здесь вдруг используется другой компонент. Однако это не случайно: bitrix:ui.sidepanel.wrapper — это обертка, предназначенная для отображения компонентов внутри слайдера.
Если в iframe слайдера открыть обычную страницу, то она загрузится со всем шаблоном сайта — шапкой, меню, подвалом. Такой режим требует адаптации вывода.
bitrix:ui.sidepanel.wrapper как раз и решает эту задачу: он позволяет отобразить нужный компонент без лишних элементов оформления, корректно встроив его в слайдер.
Подробное описание — в официальной документации.
В коде видно, что мы передаем в параметры обертки наш компонент:’POPUP_COMPONENT_NAME’ => ‘o2k:o2k.test’, который выводится в слайдере уже без шапки и футера. Также в ajax прокидываем параметры компонента o2k:o2k.test и фильтра.
Что касается класса CrmMenu, то он будет выглядеть следующим образом:
Скрытый текст
<?php namespace o2k\d7\Events; use Bitrix\Main\Loader, o2k\d7\Conf\Settings, Bitrix\Main\EventResult, Bitrix\Crm\DealTable, Bitrix\Main\ORM, Bitrix\Iblock\ORM as IblockORM, Bitrix\Main\Localization\Loc; Loc::loadMessages( __FILE__ ); Loader::includeModule(Settings::$crmMid); class CrmMenu { public static function getTab(\Bitrix\Main\Event $event): EventResult { $entityId = $event->getParameter('entityID'); $entityTypeID = $event->getParameter('entityTypeID'); $tabs = $event->getParameter('tabs'); switch ($entityTypeID) { case \CCrmOwnerType::Deal: $tabs = self::getResponsibleDeals($tabs, $entityId); break; } return new EventResult(EventResult::SUCCESS, [ 'tabs' => $tabs, ]); } private static function getResponsibleDeals(array $tabs, int $id): array { $responsible = 0; $query = new IblockORM\Query(DealTable::getEntity()); $query->setSelect([ 'ASSIGNED_BY_ID' ]); $query->setFilter([ '=ID' => $id ]); $query->setLimit(1); $arDeals = ORM\Query\QueryHelper::decompose($query); if(is_object($arDeals) && count($arDeals) > 0) { foreach($arDeals as $deal) { $deal = $deal->collectValues(ORM\Objectify\Values::ALL, ORM\Fields\FieldTypeMask::ALL, true); $responsible = $deal['ASSIGNED_BY_ID']; } } $tabs[] = [ 'id' => 'deals_contact', 'name' => Loc::getMessage(Settings::$langPrefix.'_DEALS_RESPONSIBLE'), 'enabled' => !empty($id), 'loader' => [ 'serviceUrl' => '/local/components/o2k/o2k.test/ajax.php?&site=' . \SITE_ID . '&' . \bitrix_sessid_get(), 'componentData' => [ 'template' => '', 'params' => [ 'GRID_ID' => 'test_grid_table', 'FILTER_ID' => 'test_grid_filter', 'FILTER' => ['RESPONSIBLE' => $responsible] ] ] ] ]; return $tabs; } }
Здесь мы видим, что при наступлении события onEntityDetailsTabsInitialized вызывается наш метод getTab.
Внутри метода мы проверяем, что текущая страница — это именно карточка сделки. Если это так, вызывается вспомогательный метод, который определяет ответственного по ID сущности (то есть сделки).
switch ($entityTypeID) { case \CCrmOwnerType::Deal: $tabs = self::getResponsibleDeals($tabs, $entityId); break; }
Найдя ответственного, он передается в массив параметров serviceUrl, который в свою очередь передает эти данные в виде фильтра на на обработчик компонента (файл ajax.php)
Скрытый текст
$tabs[] = [ 'id' => 'deals_contact', 'name' => Loc::getMessage(Settings::$langPrefix.'_DEALS_RESPONSIBLE'), 'enabled' => !empty($id), 'loader' => [ 'serviceUrl' => '/local/components/o2k/o2k.test/ajax.php?&site=' . \SITE_ID . '&' . \bitrix_sessid_get(), 'componentData' => [ 'template' => '', 'params' => [ 'GRID_ID' => 'test_grid_table', 'FILTER_ID' => 'test_grid_filter', 'FILTER' => ['RESPONSIBLE' => $responsible] ] ] ] ];
Параметры ‘componentData’ -> ‘params’ — это массив с настройками, которые будут переданы в компонент o2k.test.
В результате мы получаем новую вкладку с таблицей из нашей ORM-модели и фильтром для удобного поиска.

Дальше все зависит от вашей фантазии — вы можете создавать любые таблицы с разными данными.
Кейс. Встраивание в выпадающее меню сделки
Проблема клиента:
Формирование уникального отчета по сделке требует множества действий. Каждый отчет должен пройти несколько стадий перед финальным утверждением. Это занимает много времени.
Еще сотрудники часто забывают нажать нужную кнопку в задаче или не вносят важные данные. Из-за этого отчет невозможно сформировать оперативно.
Задачи:
-
Добавить в контекстное меню сделки (в списке сделок) два отчета. У каждого — свой алгоритм: запуск бизнес-процесса, создание задачи, отправка на согласование, генерация PDF и другие шаги.
-
Максимально автоматизировать эти действия и использовать индивидуальные шаблоны для каждого отчета.
Теперь давайте рассмотрим, как реализовать встраивание в выпадающее меню сделки — это довольно распространенный сценарий.
\Bitrix\Main\EventManager::getInstance()->registerEventHandler('crm', 'onCrmDealListItemBuildMenu', $this->MODULE_ID, 'o2k\\d7\\Events\DealContextItemMenu','injectContextMenu');
Если открыть код стандартного компонента crm.deal.list, а точнее — файл template.php, можно увидеть интересную деталь: при построении контекстного меню строки сделки используется специальное событие.
Это событие — onCrmDealListItemBuildMenu. С его помощью можно дополнить или изменить пункты контекстного меню, встроив туда собственные действия.
Зная это, мы можем подписаться на это событие, модифицировать или добавлять меню. В файле install.php в функции
function InstallEvents()
Для модуля добавим подписку на событие:
\Bitrix\Main\EventManager::getInstance()->registerEventHandler('crm', 'onCrmDealListItemBuildMenu', $this->MODULE_ID, 'o2k\\d7\\Events\DealContextItemMenu','injectContextMenu');
Создадим класс, в котором опишем логику:
Скрытый текст
<?php namespace o2k\d7\Events; use Bitrix\Main\Loader, o2k\d7\Conf\Settings, Bitrix\Main\EventResult; class DealContextItemMenu { public static function injectContextMenu($restPlacement, $contactId, array &$menu) { if($restPlacement == 'CRM_DEAL_LIST_MENU') { $menu[]['SEPARATOR'] = true; $menu[] = [ 'TITLE' => 'Test', 'TEXT' => 'Test 1', 'MENU' => [ [ 'TITLE' => 'Sub Test 1', 'TEXT' => 'Sub Test 1', 'ONCLICK' => 'console.log("test 1");' ], [ 'TITLE' => 'Sub Test 2', 'TEXT' => 'Sub Test 2', 'ONCLICK' => 'console.log("test 2");' ] ] ]; $menu[]['SEPARATOR'] = true; } return $menu; } }
Принцип простой: мы проверяем, что обрабатывается именно контекстное меню сделки, затем добавляем разделитель, чтобы визуально отделить наши пункты.
Далее создаём свой раздел — например, «Основное меню», — и в нем размещаем два подпункта.
Важно: параметр ONCLICK — это JavaScript-код, который будет выполнен при нажатии на пункт меню. Это может быть что угодно:
– открытие слайдера,
– отправка AJAX-запроса,
– генерация счета по сделке,
– или любое другое действие, которое часто требуется клиенту «в два клика».
Такие и другие события подробно описаны в документации События и обработка меню в Bitrix24.
В итоге получилось вот так:
Выводы
Как видно из примеров, работать с Битрикс24 не так уж сложно — особенно сейчас, когда появилась документация и для облачной версии (ранее она была менее информативна). Возможно, позже мы с вами рассмотрим и облачные приложения. Но это будет уже совсем другая история 🙂
Сегодня мы познакомились с:
-
фильтрацией по собственному гриду с кастомными свойствами;
-
встройкой пунктов «меню» в карточку сделки с фильтрацией;
-
встройкой собственных пунктов в контекстное меню списка сделкок (или лида, или компании).
-
методом serviceUrl;
-
использованием компонента bitrix:ui.sidepanel.wrapper;
Главная цель данной статьи — показать практические и работающие решения, которые реально применяются в проектах для наших клиентов.
С таким «джентльменским набором» уже можно решать довольно широкий круг задач в Битрикс24.
ссылка на оригинал статьи https://habr.com/ru/articles/935914/
Добавить комментарий