Продолжаем учиться правильно кастомизировать Битрикс24
В этой статье - создаем свою сущность, но в типовом интерфейсе. Это позволит разработчику не изобретать велосипед, а пользователю - использовть уже существующий опыт работы с системой. Слово коллегам из
В изучении этой темы вам поможет
[spoiler]
Введение
В данной статье разберем создание сущности в Б24 «с нуля». Такая задача обычно возникает в крупных проектах с большими и сложными структурами данных. Создание сущности (в целом) необходимо, если компания собирается вести учет чего-либо в Б24. Как правило, с этими данными также связана некоторая логика, которая исходит из процессов компании.
Данная статья больше ориентирована на повторение UX Б24 при работе с данными. Сотрудники компании уже привыкли к интерфейсам CRM, задач и т. д., знают их возможности и умеют ими пользоваться. Полагаясь на этот пользовательский опыт вы можете снизить затраты на обучение сотрудников.
Способы создания сущностей в Б24
Нам видятся два основных подхода:
- универсальные списки;
- создание «с нуля»: таблица в БД и разработка интерфейсов.
Списки не подойдут, если:
- ваши доработки неудобно реализовывать в списках;
- реализация ваших доработок в списках трудоемка/не выгодна, а полученный в результате код будет трудно сопровождать в будущем;
- требуется тонкая настройка прав доступа (на уровне отдельных полей, действий и т. д.);
- для вашей сущности нужны кастомные интерфейсы, сильно отличающиеся от списков.
В данной статье подробно разберем второй подход.
Даже если вы не собираетесь разрабатывать сущности вторым способом, мы все же рекомендуем изучить раздел о разработке представления «список». В корпоративных системах вывод списков — очень частая задача.
В статье будут приводиться лишь фрагменты кода. Рекомендуем установить готовый модуль и изучать его код параллельно с чтением статьи. Модуль нужно разместить в каталоге /local/modules.
Пример задачи — торговые точки CRM
В качестве примера возьмем небольшую задачу: создадим сущность «Торговая точка», разработаем к ней необходимые интерфейсы и реализуем связь сделок с торговыми точками.
Торговая точку будем описывать небольшим количеством полей (этого достаточно для примера):
- название;
- адрес;
- ответственный.
В данной статье мы не будем рассматривать (и реализовывать):
- проверку прав доступа (не помещается ;
- кеширование (здесь нет ничего специфического для Б24, чтобы узнать, как делать кеширование в компонентах, посмотрите курсы Академии 1С-Битрикс).
Структура решения
Для хранения данных сущности создадим таблицу в БД. Для этого сначала нужно создать модуль.
Для создания интерфейса (список, просмотр, редактирование) будем использовать компоненты Б24. Нам понадобится создать комплексный компонент (роутер), с помощью которого мы свяжем представления с URL. Для каждого представления потребуется разработать отдельный компонент. Таким образом, чтобы реализовать все операции CRUD, потребуется разработать 4 компонента.
Структура компонентов на рисунке ниже.
Связь сделок с торговыми точками мы реализуем путем создания своего типа пользовательского поля «Привязка к торговой точке». Чтобы вывести список привязанных сделок в карточке торговой точки, разработаем еще один компонент.
Создание сущности
Модуль назовем academy.crmstores. Код модуля расположим в каталоге /local/modules/academy.crmstores.
Опишем класс сущности. Для этого будем использовать D7 ORM.
namespace Academy\CrmStores\Entity; class StoreTable extends DataManager { public static function getTableName() { return 'academy_crmstores_store'; } public static function getMap() { return array( new IntegerField('ID', array('primary' => true, 'autocomplete' => true)), new StringField('NAME'), new StringField('ADDRESS'), new IntegerField('ASSIGNED_BY_ID'), new ReferenceField( 'ASSIGNED_BY', UserTable::getEntity(), array('=this.ASSIGNED_BY_ID' => 'ref.ID') ) ); } } |
Осталось добавить в установщик модуля создание этой таблицы. В методе InstallDB напишем код:
$db = Application::getConnection(); $storeEntity = StoreTable::getEntity(); if (! $db->isTableExists($storeEntity->getDBTableName())) { $storeEntity->createDbTable(); } |
Теперь можно установить модуль в административной панели и убедиться, что таблица создалась. Мы также заполнили ее тестовыми данными.
Создание раздела и разработка роутера
Разработка комплексного компонента (роутера)
В разделе торговых точек в публичной части (который мы создадим позднее) управлять отображением данных будет комплексный компонент academy.crmstores:stores. Разместим его в каталоге /local/components/academy.crmstores/stores.
В рамках учебной задачи мы не будем описывать параметры компонента, за исключением настроек ЧПУ. Кроме того, сделаем так, чтобы компонент мог работать только в режиме ЧПУ.
Настройки опишем в файле .parameters.php:
$arComponentParameters = array( 'PARAMETERS' => array( 'SEF_MODE' => array( 'details' => array( 'NAME' => Loc::getMessage('CRMSTORES_DETAILS_URL_TEMPLATE'), 'DEFAULT' => '#STORE_ID#/', 'VARIABLES' => array('STORE_ID') ), 'edit' => array( 'NAME' => Loc::getMessage('CRMSTORES_EDIT_URL_TEMPLATE'), 'DEFAULT' => '#STORE_ID#/edit/', 'VARIABLES' => array('STORE_ID') ) ) ) ); |
В классе компонента в методе executeComponent определим текущую страницу и подключим шаблон для нее.
public function executeComponent() { //… $page = CComponentEngine::parseComponentPath( $this->arParams['SEF_FOLDER'], $sefTemplates, $arVariables ); if (empty($page)) { $page = 'list'; } // … $this->includeComponentTemplate($page); } |
Создадим шаблон встроенный шаблон .default с тремя страницами — list.php, details.php и edit.php. На каждой странице для начала просто выведем ее название.
Теперь создадим страницу /crm/stores/index.php и вызовем на ней написанный компонент. При запросах по URL /crm/stores/, /crm/stores/0/ и /crm/stores/0/edit/ должны открываться разные страницы.
Показ меню раздела CRM и добавление пункта меню «Торговые точки»
Меню раздела в CRM сделано не с помощью компонента bitrix:menu, как это обычно бывает, а с помощью компонента bitrix:crm.control_panel. Чтобы вывести меню CRM на каждой странице шаблона комплексного компоненты сделаем следующий вызов.
$APPLICATION->IncludeComponent( 'bitrix:crm.control_panel', '', array( 'ID' => 'STORES', 'ACTIVE_ITEM_ID' => 'STORES', ), $component ); |
$eventManager = EventManager::getInstance(); $eventManager->registerEventHandlerCompatible( 'crm', 'OnAfterCrmControlPanelBuild', self::MODULE_ID, '\Academy\CrmStores\Handler\CrmMenu', 'addStores' ); |
public static function addStores(&$items) { $items[] = array( 'ID' => 'STORES', 'MENU_ID' => 'menu_crm_stores', 'NAME' => Loc::getMessage('CRMSTORES_MENU_ITEM_STORES'), 'TITLE' => Loc::getMessage('CRMSTORES_MENU_ITEM_STORES'), 'URL' => '/crm/stores/' ); } |
После переустановки модуля должен появиться пункт меню «Торговые точки».
Разработка компонента списка
Код компонента списка разместим в каталоге /local/components/academy.crmstores/stores.list.
Выборка данных
В классе компонента заведем метод getStores, который возвращает массив торговых точек, а также получает данные привязанных ответственных сотрудников. На вход метод принимает параметры выборки торговых точек.
private function getStores($params = array()) { $dbStores = StoreTable::getList($params); $stores = $dbStores->fetchAll(); $userIds = array_column($stores, 'ASSIGNED_BY_ID'); $userIds = array_unique($userIds); $userIds = array_filter( $userIds, function ($userId) { return intval($userId) > 0; } ); $dbUsers = UserTable::getList(array( 'filter' => array('=ID' => $userIds) )); $users = array(); foreach ($dbUsers as $user) { $users[$user['ID']] = $user; } foreach ($stores as &$store) { if (intval($store['ASSIGNED_BY_ID']) > 0) { $store['ASSIGNED_BY'] = $users[$store['ASSIGNED_BY_ID']]; } } return $stores; } |
Также создадим встроенный шаблон .default, в котором будем выводить список.
Выбор компонента для вывода таблицы и его вызов
Выводить список торговых точек будем с помощью компонентов Б24. Для вывода списков используют компонент «Грид».
В Б24 существует несколько различных гридов. Условно можно разделить их на базовые и производные (специфические для некоторых модулей).
К базовым относятся:
- bitrix:main.interface.grid (устарел);
- bitrix:main.ui.grid.
bitrix:main.interface.grid | bitrix:main.ui.grid |
- bitrix:crm.interface.grid;
- bitrix:disk.interface.grid;
- bitrix:bizproc.interface.grid.
Вызовем этот компонент в шаблоне компонента списка (academy.stores:stores.list).
$APPLICATION->IncludeComponent( 'bitrix:crm.interface.grid', 'titleflex', array( 'GRID_ID' => $arResult['GRID_ID'], 'AJAX_ID' => '', 'AJAX_OPTION_JUMP' => 'N', 'AJAX_OPTION_HISTORY' => 'N', 'AJAX_LOADER' => null, ), $this->getComponent(), array('HIDE_ICONS' => 'Y',) ); |
Шаблон titleflex сам встроит фильтр в заголовок страницы. В параметрах компонента мы передаем GRID_ID и некоторые настройки, влияющие на его поведение на стороне клиента (в частности, грид не будет менять URL текущей страницы и не будет записывать в историю браузера действия пользователя при смене сортировки, фильтров и т. п.).
Как вы знаете, каждый сотрудник может сконфигурировать грид под свои задачи. Сотрудник может управлять отображением столбцов, их порядком, управлять видимостью полей в фильтре, создавать именованные фильтры… Б24 сохраняет настройки каждого грида. Для этого необходимо задавать GRID_ID — настройки будут связаны с ним.
Теперь вызовем компонент списка на странице комплексного компонента. Вы должны увидеть пустой грид с надписью «Нет данных».
Передача данных в грид: столбцы и строки
В компоненте списка опишем столбцы грида следующей структурой и передадим их в шаблон через $arResult.
$headers = array( array( 'id' => 'ID', 'name' => Loc::getMessage('CRMSTORES_HEADER_ID'), 'sort' => 'ID', 'first_order' => 'desc', 'type' => 'int', ), array( 'id' => 'NAME', 'name' => Loc::getMessage('CRMSTORES_HEADER_NAME'), 'sort' => 'NAME', 'default' => true, ), array( 'id' => 'ASSIGNED_BY', 'name' => Loc::getMessage('CRMSTORES_HEADER_ASSIGNED_BY'), 'sort' => 'ASSIGNED_BY_ID', 'default' => true, ), array( 'id' => 'ADDRESS', 'name' => Loc::getMessage('CRMSTORES_HEADER_ADDRESS'), 'sort' => 'ADDRESS', 'default' => true, ), ); |
Смысл настроек столбцов следующий:
Параметр | Описание |
id | Идентификатор столбца, по которому будет установлена связь с данными строки. |
name | Название столбца, которое увидит пользователь. |
sort | Идентификатор столбца при смене сортировки. При смене сортировки грид отправит AJAX-запрос, где будут указаны название столбца по которому нужно отсортировать (из этого параметра) и направление сортировки. |
first_order | Направление сортировки, которое должно быть установлено при переключении сортировки на данный столбец с другого. |
type | Тип данных столбца. Может незначительно влиять на формат вывода. |
default | Когда пользователь открывает грид первый раз, будут показаны только столбцы, у которых этот параметр установлен в значение true. Также он используется при сбросе настроек грида. |
Полученные из БД торговые точки мы уже передали в шаблон. Сформируем массив строк для грида в шаблоне.
$rows = array(); foreach ($arResult['STORES'] as $store) { $viewUrl = CComponentEngine::makePathFromTemplate( $arParams['URL_TEMPLATES']['DETAIL'], array('STORE_ID' => $store['ID']) ); $rows[] = array( 'id' => $store['ID'], 'data' => $store, 'columns' => array( 'ID' => $store['ID'], 'NAME' => '<a href="' . $viewUrl . '" target="_self">' . $store['NAME'] . '</a>', 'ASSIGNED_BY' => empty($store['ASSIGNED_BY']) ? '' : CCrmViewHelper::PrepareUserBaloonHtml(array( 'PREFIX' => "STORE_{$store['ID']}_RESPONSIBLE", 'USER_ID' => $store['ASSIGNED_BY_ID'], 'USER_NAME'=> CUser::FormatName(CSite::GetNameFormat(), $store['ASSIGNED_BY']), 'USER_PROFILE_URL' => Option::get('intranet', 'path_user', '', SITE_ID) . '/' )), 'ADDRESS' => $store['ADDRESS'], ) ); } |
Передадим строки и столбцы в грид.
$APPLICATION->IncludeComponent( 'bitrix:crm.interface.grid', 'titleflex', array( // ... 'HEADERS' => $arResult['HEADERS'], 'ROWS' => $rows, // ... ), $this->getComponent(), array('HIDE_ICONS' => 'Y',) ); |
Теперь вы должны увидеть все данные в таблице.
Реализация сортировки
При смене сортировки грид отправляет AJAX-запрос на текущую страницу, при этом вызывается наш компонент списка. Для получения данных о состоянии грида (какая сортировка сейчас установлена, какие столбцы видит пользователь и т. д.) воспользуемся классом \Bitrix\Main\Grid\Options. Следующий код размещается в методе executeComponent.$grid = new Grid\Options(self::GRID_ID);
$gridSort = $grid->getSorting(); $sort = array_filter( $gridSort['sort'], function ($field) { return in_array($field, self::SORTABLE_FIELDS); }, ARRAY_FILTER_USE_KEY ); if (empty($sort)) { $sort = array('NAME' => 'asc'); } |
Мы отфильтровываем возвращаемые им значения, таким образом, чтобы остались только поля, поддерживаемые getList’ом.
const SORTABLE_FIELDS = array('ID', 'NAME', 'ASSIGNED_BY_ID', 'ADDRESS'); |
Теперь $sort можно использовать в getList.
$stores = $this->getStores(array( 'order' => $sort )); |
Поскольку фактическая сортировка может отличаться от той, что пришла в запросе из грида, гриду нужно сообщить фактическую сортировку, чтобы она была правильно отображена пользователю. Передайте $sort как значение параметра SORT компонента грида.
Реализация фильтра
Чтобы фильтр появился, его необходимо настроить. А именно — указать по каким полям можно фильтровать и задать предустановленные именованные фильтры.
Поля для фильтра опишем в компоненте списка следующим образом.
$filterFields = array( array( 'id' => 'ID', 'name' => Loc::getMessage('CRMSTORES_FILTER_FIELD_ID') ), array( 'id' => 'NAME', 'name' => Loc::getMessage('CRMSTORES_FILTER_FIELD_NAME'), 'default' => true, ), array( 'id' => 'ASSIGNED_BY_ID', 'name' => Loc::getMessage('CRMSTORES_FILTER_FIELD_ASSIGNED_BY'), 'type' => 'custom_entity', 'params' => array( 'multiple' => 'Y' ), 'selector' => array( 'TYPE' => 'user', 'DATA' => array( 'ID' => 'ASSIGNED_BY', 'FIELD_ID' => 'ASSIGNED_BY_ID' ) ), 'default' => true, ), array( 'id' => 'ADDRESS', 'name' => Loc::getMessage('CRMSTORES_FILTER_FIELD_ADDRESS'), 'default' => true, ), ); |
Настройки похожи на описание столбцов:
Параметр | Описание |
id | Идентификатор поля, который будет сообщен компоненту при установке фильтра пользователем. |
name | Название поля фильтра, которое увидит пользователь. |
default | Будет ли отображено поле в форме фильтра по-умолчанию. |
type | Элемент управления в форме фильтра. |
Создадим один именованный фильтр «Мои торговые точки» (те, в которых текущий поьзователь является ответственным). Для этого фильтра указываем его название и значения полей из массива выше.
$filterPresets = array( 'my_stores' => array( 'name' => Loc::getMessage('CRMSTORES_FILTER_PRESET_MY_STORES'), 'fields' => array( 'ASSIGNED_BY_ID' => $USER->GetID(), 'ASSIGNED_BY_ID_name' => $USER->GetFullName(), ) ) ); |
$arResult.$APPLICATION->IncludeComponent( 'bitrix:crm.interface.grid', 'titleflex', array( // ... 'FILTER' => $arResult['FILTER'], 'FILTER_PRESETS' => $arResult['FILTER_PRESETS'], 'IS_EXTERNAL_FILTER' => false, 'ENABLE_LIVE_SEARCH' => $arResult['ENABLE_LIVE_SEARCH'], 'DISABLE_SEARCH' => $arResult['DISABLE_SEARCH'], // ... ), $this->getComponent(), array('HIDE_ICONS' => 'Y',) ); |
Теперь в заголовке над списком появился привычный фильтр
Вывод количества выбранных элементов
Компонент main.ui.grid позволяет передать ему количество выбранных элементов как параметр. Компонент crm.interface.grid, который мы используем, — нет. Вместо этого мы должны разработать небольшой служебный AJAX-обработчик, который будет возвращать необходимые данные гриду.
Поскольку этот обработчик будет поддерживать лишь одну операцию, сделаем его частью компонента списка. Создадим метод processServiceActions и вызовем его из метода executeComponent. Ниже фрагмент метода processServiceActions.
$APPLICATION->RestartBuffer(); header('Content-Type: application/json'); switch ($action) { case 'GET_ROW_COUNT': $count = StoreTable::getCount($currentFilter); echo Json::encode(array( 'DATA' => array( 'TEXT' => Loc::getMessage('CRMSTORES_GRID_ROW_COUNT', array('#COUNT#' => $count)) ) )); break; default: break; } die; |
Теперь настроим грид. В первую очередь, подключим JS-класс, предоставляющий гриду некоторые методы.
$asset = Asset::getInstance(); $asset->addJs('/bitrix/js/crm/interface_grid.js'); Подключим «JS-расширение» для грида. $gridManagerId = $arResult['GRID_ID'] . '_MANAGER'; $APPLICATION->IncludeComponent( 'bitrix:crm.interface.grid', 'titleflex', array( // ... 'ENABLE_ROW_COUNT_LOADER' => true, 'EXTENSION' => array( 'ID' => $gridManagerId, 'CONFIG' => array( 'ownerTypeName' => 'STORE', 'gridId' => $arResult['GRID_ID'], 'serviceUrl' => $arResult['SERVICE_URL'], ), 'MESSAGES' => array( 'deletionDialogTitle' => Loc::getMessage('CRMSTORES_DELETE_DIALOG_TITLE'), 'deletionDialogMessage' => Loc::getMessage('CRMSTORES_DELETE_DIALOG_MESSAGE'), 'deletionDialogButtonTitle' => Loc::getMessage('CRMSTORES_DELETE_DIALOG_BUTTON'), ) ), // ... ), $this->getComponent(), array('HIDE_ICONS' => 'Y',) ); |
Теперь при клике по ссылке «Показать количество» (внизу, под списком) будет появляться количество элементов.
До клика | После клика |
До текущего момента мы выводили в списке все выбранные элементы на одной странице. Если элементов будет достаточно много, интерфейс начнет «тормозить». Чтобы решить эту проблему, реализуем постраничную навигацию.
Чтобы узнать, какой размер страницы установил пользователь (внизу справа от списка), воспользуемся все тем же объектом класса \Bitrix\Main\Grid\Options.
$gridNav = $grid->GetNavParams(); |
Саму логику постраничной навигации мы тоже не будем реализовывать самостоятельно, а воспользуемся классом \Bitrix\Main\UI\PageNavigation.
$pager = new PageNavigation(''); $pager->setPageSize($gridNav['nPageSize']); $pager->setRecordCount(StoreTable::getCount($gridFilterValues)); if ($request->offsetExists('page')) { $currentPage = $request->get('page'); $pager->setCurrentPage($currentPage > 0 ? $currentPage : $pager->getPageCount()); } else { $pager->setCurrentPage(1); } |
При создании экземпляра указываем идентификатор постраничной навигации. Если на одной странице вам нужно создать несколько независимых «постраничек», у них должны быть разные идентификаторы.
Наша цель — получить параметры для getList. Чтобы получить корректные значения, сначала объект постраничной навигации нужно настроить. Мы задаем размер страницы и количество элементов, соответствующих текущему фильтру.
Номер текущей страницы мы получаем из параметров запроса: $request->get('page'). Надо заметить, что в гриде есть ссылка для перехода к последней странице. При клике по ней параметр page примет значение -1. Этот случай тоже нужно учитывать.
Теперь остается лишь указать limit и offset в параметрах getList. С помощью объекта постраничной навигации это сделать очень просто.
$stores = $this->getStores(array( 'filter' => $gridFilterValues, 'limit' => $pager->getLimit(), 'offset' => $pager->getOffset(), 'order' => $sort )); |
$this->arResult = array( // ... 'PAGINATION' => array( 'PAGE_NUM' => $pager->getCurrentPage(), 'ENABLE_NEXT_PAGE' => $pager->getCurrentPage() < $pager->getPageCount(), 'URL' => $request->getRequestedPage(), ), // ... ); Указываем номер текущей страницы, показывать ли ссылку на следующую страницу (true/false) и URL, на который грид будет отправлять запрос при переключении страниц. Передаем эти настройки в грид. $APPLICATION->IncludeComponent( 'bitrix:crm.interface.grid', 'titleflex', array( // ... 'PAGINATION' => $arResult['PAGINATION'], // ... ), $this->getComponent(), array('HIDE_ICONS' => 'Y',) ); |
Реализация действий с несколькими строками
Строки в гриде можно отмечать флажками. Реализуем возможность выполнять действия над отмеченными строками.
Сначала добавим кнопки на панель инструментов (она расположена в нижней части грида и до этого момента оставалась пустой). В шаблоне компонента списка создадим экземпляр класса \Bitrix\Main\Grid\Panel\Snippet, с помощью которого мы подготовим код кнопок и передадим его в грид.
$snippet = new Snippet(); $this->arResult = array( // ... 'ACTION_PANEL' => array( 'GROUPS' => array( array( 'ITEMS' => array( $snippet->getRemoveButton(), $snippet->getForAllCheckbox(), ) ) ) ), // ... ); |
На панели должны появиться кнопка «Удалить» и флажок для выбора всех записей.
При нажатии на кнопку «Удалить» грид отправит запрос на текущий URL с тремя параметрами:
- действие — удаление;
- отмечен ли флажок «для всех»;
- идентификаторы строк, отмеченных флажками.
$allRows = $request->get('action_all_rows_' . self::GRID_ID) == 'Y'; if ($allRows) { $dbStores = StoreTable::getList(array( 'filter' => $currentFilter, 'select' => array('ID'), )); $storeIds = array(); foreach ($dbStores as $store) { $storeIds[] = $store['ID']; } } else { $storeIds = $request->get('ID'); if (!is_array($storeIds)) { $storeIds = array(); } } if (empty($storeIds)) { return; } switch ($action) { case 'delete': foreach ($storeIds as $storeId) { StoreTable::delete($storeId); } break; default: break; } |
Реализация действий над одной строкой
Добавим для каждой строки контекстное меню с тремя действиями: «Просмотреть», «Редактировать», «Удалить».
Для этого добавим массив actions к описанию строки для грида в шаблоне компонента списка.
$rows[] = array( 'id' => $store['ID'], 'actions' => array( array( 'TITLE' => Loc::getMessage('CRMSTORES_ACTION_VIEW_TITLE'), 'TEXT' => Loc::getMessage('CRMSTORES_ACTION_VIEW_TEXT'), 'ONCLICK' => 'BX.Crm.Page.open(' . Json::encode($viewUrl) . ')', 'DEFAULT' => true ), array( 'TITLE' => Loc::getMessage('CRMSTORES_ACTION_EDIT_TITLE'), 'TEXT' => Loc::getMessage('CRMSTORES_ACTION_EDIT_TEXT'), 'ONCLICK' => 'BX.Crm.Page.open(' . Json::encode($editUrl) . ')', ), array( 'TITLE' => Loc::getMessage('CRMSTORES_ACTION_DELETE_TITLE'), 'TEXT' => Loc::getMessage('CRMSTORES_ACTION_DELETE_TEXT'), 'ONCLICK' => 'BX.CrmUIGridExtension.processMenuCommand(' . Json::encode($gridManagerId) . ', BX.CrmUIGridMenuCommand.remove, { pathToRemove: ' . Json::encode($deleteUrl) . ' })', ) ), 'data' => $store, 'columns' => array(...) ); |
Для перехода к карточке торговой точки мы используем функцию BX.Crm.Page.open. Что касается удаления — написанный код заставит грид отправить запрос на выполнение действия удаления с одним идентификатором строки. Запрос будет обработан в методе, который мы написали для действий с несколькими строками.
Теперь у каждой строки появилось выпадающее меню.
Вывод кнопки создания новой торговой точки
На этом функциональность списка торговых точек можно считать законченной. Завершим разработку представления «список» добавлением панели инструментов с кнопкой создания торговой точки.
На странице списка шаблона комплексного компонента вызовем компонент bitrix:crm.interface.toolbar, в настройках передадим, какую кнопку мы хотим вывести. Вызов этого компонента нужно сделать перед вызовом компонента списка.
$APPLICATION->IncludeComponent( 'bitrix:crm.interface.toolbar', 'title', array( 'TOOLBAR_ID' => 'CRMSTORES_TOOLBAR', 'BUTTONS' => array( array( 'TEXT' => Loc::getMessage('CRMSTORES_ADD'), 'TITLE' => Loc::getMessage('CRMSTORES_ADD'), 'LINK' => CComponentEngine::makePathFromTemplate($urlTemplates['EDIT'], array('STORE_ID' => 0)), 'ICON' => 'btn-add', ), ) ), $this->getComponent(), array('HIDE_ICONS' => 'Y') ); |
Разработка компонента детального просмотра
Для вывода карточки торговой точки в режиме просмотра и редактирования будем использовать компонент bitrix:crm.interface.form. Это «старая» карточка CRM, которая использовалась до появления слайдера. На момент написания статьи, мы не можем повторно использовать новую, это связано с ее внутренними ограничениями. В будущем, надеемся, это станет возможным.
Для реализации карточки в режиме просмотра разработаем компонент academy.crmstores:store.show. Идентификатор торговой точки для отображения компонент принимает как параметр.
Выбор данных торговой точки
Код компонента довольно прост. Выполняется получение данных с помощью метода StoreTable::getById, а также задается заголовок страницы вида «Торговая точка №<ID> — <Название>».
$dbStore = StoreTable::getById($this->arParams['STORE_ID']); $store = $dbStore->fetch(); if (empty($store)) { ShowError(Loc::getMessage('CRMSTORES_STORE_NOT_FOUND')); return; } $APPLICATION->SetTitle(Loc::getMessage( 'CRMSTORES_SHOW_TITLE', array( '#ID#' => $store['ID'], '#NAME#' => $store['NAME'] ) )); $this->arResult =array( 'FORM_ID' => self::FORM_ID, 'TACTILE_FORM_ID' => CAcademyCrmStoresStoreEditComponent::FORM_ID, 'GRID_ID' => CAcademyCrmStoresStoresListComponent::GRID_ID, 'STORE' => $store ); $this->includeComponentTemplate(); |
Также в классе определена константа — идентификатор формы для просмотра (по аналогии с идентификатором грида). TACTILE_FORM_ID — это идентификатор формы для редактирования. В приведенном коде он берется из класса компонента редактирования (которого у вас еще нет, но ID нужен уже сейчас .
В шаблоне компонента просто вызываем crm.interface.form и передаем в него данные.
$APPLICATION->IncludeComponent( 'bitrix:crm.interface.form', 'show', array( 'GRID_ID' => $arResult['GRID_ID'], 'FORM_ID' => $arResult['FORM_ID'], 'TACTILE_FORM_ID' => $arResult['TACTILE_FORM_ID'], 'ENABLE_TACTILE_INTERFACE' => 'Y', 'SHOW_SETTINGS' => 'Y', 'DATA' => $arResult['STORE'], 'TABS' => array( array( 'id' => 'tab_1', 'name' => Loc::getMessage('CRMSTORES_TAB_STORE_NAME'), 'title' => Loc::getMessage('CRMSTORES_TAB_STORE_TITLE'), 'display' => false, 'fields' => array( array( 'id' => 'section_store', 'name' => Loc::getMessage('CRMSTORES_FIELD_SECTION_STORE'), 'type' => 'section', 'isTactile' => true, ), array( 'id' => 'ID', 'name' => Loc::getMessage('CRMSTORES_FIELD_ID'), 'type' => 'label', 'value' => $arResult['STORE']['ID'], 'isTactile' => true, ), array( 'id' => 'NAME', 'name' => Loc::getMessage('CRMSTORES_FIELD_NAME'), 'type' => 'label', 'value' => $arResult['STORE']['NAME'], 'isTactile' => true, ), array( 'id' => 'ADDRESS', 'name' => Loc::getMessage('CRMSTORES_FIELD_ADDRESS'), 'type' => 'label', 'value' => $arResult['STORE']['ADDRESS'], 'isTactile' => true, ), array( 'id' => 'ASSIGNED_BY', 'name' => Loc::getMessage('CRMSTORES_FIELD_ASSIGNED_BY'), 'type' => 'custom', 'value' => CCrmViewHelper::PrepareFormResponsible( $arResult['STORE']['ASSIGNED_BY_ID'], CSite::GetNameFormat(), Option::get('intranet', 'path_user', '', SITE_ID) . '/' ), 'isTactile' => true, ) ), ) ), ), $this->getComponent(), array('HIDE_ICONS' => 'Y') ); |
Структурно, форма состоит из вкладок, вкладка состоит из разделов и полей. Визуально это хорошо видно на форме настройки CRM:
Карточка CRM — такая же форма, но с той лишь разницей, что поля первой вкладки выводятся выше самих вкладок и есть возможность их перемещать, скрывать/показывать и т. д., минуя окно настроек. Это называется «тактильным» интерфесом (шаблон show компонента crm.interface.form).
В настройках компонента мы описали одну вкладку. Ее параметр display равен false для того, чтобы она не выводилась в списке вкладок — это логично, т. к. ее поля и так выводятся вне зависимости от выбранной вкладки.
Для описания полей мы используем три типа поля:
- section — раздел (технически, это поле);
- label — простой текст в значении поля;
- custom — HTML-код в значении поля.
Ответственного за торговую точку выводим с помощью метода CCrmViewHelper::PrepareFormResponsible (не забудьте подключить модуль crm).
Вывод карточки торговой точки
На странице детального просмотра комплексного компонента вызовем три компонента:
- bitrix:crm.control_panel — меню раздела CRM,
- bitrix:crm.interface.toolbar — панель инструментов с кнопкой «Редактировать»,
- academy.crmstores:store.show — наш компонент карточки в режиме просмотра.
ID торговой точки берем из переменных, полученных при обработке ЧПУ.
На странице просмотра появилась форма.
Разработка формы создания и редактирования
Подготовка данных для формы
Компонент редактирования торговой точки, в целом не сложный (кто из программистов не делал обработку форм? , но есть места, где нужно быть внимательным. Начнем с подготовки данных для формы.
Традиционно, в Битриксе для создания и редактирования данных используется одна и та же форма. Считается, что если идентификатор редактируемого элемента равен нулю, то форма работает в режиме создания. Вот небольшой пример:
- /company/personal/user/1/tasks/task/edit/0/ — создание новой задачи
- /company/personal/user/1/tasks/task/edit/1/ — редактирование задачи №1.
- создаем массив с данными «пустой торговой точки»;
- если передали идентификатор торговой точки, заменяем его данными этой точки;
- если при отправке формы произошли ошибки, заменяем данные на те, что отправил пользователь.
Фрагмент метода executeComponent компонента academy.crmstores:store.edit.
$store = array( 'NAME' => '', 'ADDRESS' => '', 'ASSIGNED_BY_ID' => 0 ); if (intval($this->arParams['STORE_ID']) > 0) { $dbStore = StoreTable::getById($this->arParams['STORE_ID']); $store = $dbStore->fetch(); if (empty($store)) { ShowError(Loc::getMessage('CRMSTORES_STORE_NOT_FOUND')); return; } } // Здесь будет обработка формы. $this->arResult =array( 'FORM_ID' => self::FORM_ID, 'GRID_ID' => CAcademyCrmStoresStoresListComponent::GRID_ID, 'IS_NEW' => empty($store['ID']), 'TITLE' => $title, 'STORE' => $store, 'BACK_URL' => $this->getRedirectUrl(), 'ERRORS' => $this->errors, ); $this->includeComponentTemplate(); |
Вывод формы редактирования торговой точки
Для вывода формы будем использовать все тот же компонент bitrix:crm.interface.form, но с шаблоном edit и несколько другими параметрами.
$errors = $arResult['ERRORS']; foreach ($errors as $error) { ShowError($error->getMessage()); } $APPLICATION->IncludeComponent( 'bitrix:crm.interface.form', 'edit', array( 'GRID_ID' => $arResult['GRID_ID'], 'FORM_ID' => $arResult['FORM_ID'], 'ENABLE_TACTILE_INTERFACE' => 'Y', 'SHOW_SETTINGS' => 'Y', 'TITLE' => $arResult['TITLE'], 'IS_NEW' => $arResult['IS_NEW'], 'DATA' => $arResult['STORE'], 'TABS' => array( array( 'id' => 'tab_1', 'name' => Loc::getMessage('CRMSTORES_TAB_STORE_NAME'), 'title' => Loc::getMessage('CRMSTORES_TAB_STORE_TITLE'), 'display' => false, 'fields' => array( array( 'id' => 'section_store', 'name' => Loc::getMessage('CRMSTORES_FIELD_SECTION_STORE'), 'type' => 'section', 'isTactile' => true, ), array( 'id' => 'NAME', 'name' => Loc::getMessage('CRMSTORES_FIELD_NAME'), 'type' => 'text', 'value' => $arResult['STORE']['NAME'], 'isTactile' => true, ), array( 'id' => 'ADDRESS', 'name' => Loc::getMessage('CRMSTORES_FIELD_ADDRESS'), 'type' => 'text', 'value' => $arResult['STORE']['ADDRESS'], 'isTactile' => true, ), array( 'id' => 'ASSIGNED_BY', 'name' => Loc::getMessage('CRMSTORES_FIELD_ASSIGNED_BY'), 'type' => 'intranet_user_search', 'value' => $arResult['STORE']['ASSIGNED_BY_ID'], 'componentParams' => array( 'NAME' => 'crmstores_edit_responsible', 'INPUT_NAME' => 'ASSIGNED_BY_ID', 'SEARCH_INPUT_NAME' => 'ASSIGNED_BY_NAME', 'NAME_TEMPLATE' => CSite::GetNameFormat() ), 'isTactile' => true, ) ) ), ), 'BUTTONS' => array( 'back_url' => $arResult['BACK_URL'], 'standard_buttons' => true, ), ), $this->getComponent(), array('HIDE_ICONS' => 'Y') ); |
Вот, что у нас получилось:
При создании новой торговой точки (когда IS_NEW равно false) компонент выведет другие кнопки:
Обработка формы
При нажатии на любую из кнопок, кроме «Отменить» будет отправлен запрос на текущий URL. Реализуем обработку формы в компоненте редактирования торговой точки.
Заведем несколько методов. isFormSubmitted будет определять, отправлена ли форма. Мы не ограничили отправку только методом POST.
private static function isFormSubmitted() { $context = Context::getCurrent(); $request = $context->getRequest(); $saveAndView = $request->get('saveAndView'); $saveAndAdd = $request->get('saveAndAdd'); $apply = $request->get('apply'); return !empty($saveAndView) || !empty($saveAndAdd) || !empty($apply); } |
Заведем метод getSubmittedStore, который извлекает из параметров запроса торговую точку.
private function getSubmittedStore() { $context = Context::getCurrent(); $request = $context->getRequest(); $submittedStore = array( 'NAME' => $request->get('NAME'), 'ADDRESS' => $request->get('ADDRESS'), 'ASSIGNED_BY_ID' => $request->get('ASSIGNED_BY_ID'), ); return $submittedStore; } |
private static function validate($store) { $errors = new ErrorCollection(); if (empty($store['NAME'])) { $errors->setError(new Error(Loc::getMessage('CRMSTORES_ERROR_EMPTY_NAME'))); } if (empty($store['ASSIGNED_BY_ID'])) { $errors->setError(new Error(Loc::getMessage('CRMSTORES_ERROR_EMPTY_ASSIGNED_BY_ID'))); } else { $dbUser = UserTable::getById($store['ASSIGNED_BY_ID']); if ($dbUser->getSelectedRowsCount() <= 0) { $errors->setError(new Error(Loc::getMessage('CRMSTORES_ERROR_UNKNOWN_ASSIGNED_BY_ID'))); } } return $errors; } |
private function processSave($initialStore) { $submittedStore = $this->getSubmittedStore(); $store = array_merge($initialStore, $submittedStore); $this->errors = self::validate($store); if (!$this->errors->isEmpty()) { return false; } if (!empty($store['ID'])) { $result = StoreTable::update($store['ID'], $store); } else { $result = StoreTable::add($store); } if (!$result->isSuccess()) { $this->errors->add($result->getErrors()); } return $result->isSuccess() ? $result->getId() : false; } |
И осталась еще одна небольшая задача. Как вы знаете, после успешного сохранения данных необходимо сделать перенаправление пользователя на какую-либо страницу. Какую?
В зависимости от того, откуда пользователь перешел к форме редактирования и какую кнопку нажал, он будет ожидать определенного поведения. Опишем его для всех кнопок. Также мы будем поддерживать параметр запроса backurl, позволяющий другим интерфейсам указать URL, на которую нужно вернуть пользователя, когда он закончит редактирование.
Нажатая | backurl | Назначение |
Сохранить и добавить еще | Не важно | Форма редактирования в режиме создания. |
Форма редактирования в режиме создания. | Не важно | Форма редактирования этой же торговой точки. |
Сохранить | Задан | Значение backurl. |
Сохранить | Не задан | Карточка торговой точки в режиме просмотра. |
В остальных случаях | Список торговых точек. |
private function getRedirectUrl($savedStoreId = null) { $context = Context::getCurrent(); $request = $context->getRequest(); if (!empty($savedStoreId) && $request->offsetExists('saveAndAdd')) { return CComponentEngine::makePathFromTemplate( $this->arParams['URL_TEMPLATES']['EDIT'], array('STORE_ID' => 0) ); } elseif (!empty($savedStoreId) && $request->offsetExists('apply')) { return CComponentEngine::makePathFromTemplate( $this->arParams['URL_TEMPLATES']['EDIT'], array('STORE_ID' => $savedStoreId) ); } $backUrl = $request->get('backurl'); if (!empty($backUrl)) { return $backUrl; } if (!empty($savedStoreId) && $request->offsetExists('saveAndView')) { return CComponentEngine::makePathFromTemplate( $this->arParams['URL_TEMPLATES']['DETAIL'], array('STORE_ID' => $savedStoreId) ); } else { return $this->arParams['SEF_FOLDER']; } } Наконец, мы можем реализовать сохранение торговой точки в методе executeComponent. if (self::isFormSubmitted()) { $savedStoreId = $this->processSave($store); if ($savedStoreId > 0) { LocalRedirect($this->getRedirectUrl($savedStoreId)); } $submittedStore = $this->getSubmittedStore(); $store = array_merge($store, $submittedStore); } |
На этом реализация обработки формы закончена. На текущий момент мы имеем свою сущность с полноценным интерфейсом для основных операций (CRUD) с ее данными.
Реализация связи сделок с торговыми точками
Разработка типа пользовательского поля «Привязка к торговой точке»
Чтобы дать возможность привязывать другие сущности к торговым точкам, разработаем специальный тип пользовательского поля «Привязка к торговой точке». Реализация, которую мы предлагаем используется лишь для демонстрации работоспособности подхода. Код типа поля следует значительно улучшить перед реальным использованием.
Полный код класса смотрите в приложенных исходниках. О создании типов пользовательских полей мы уже рассказывали в
По сути: в классе есть два метода, один из которых используется для создания выпадающего списка в режиме редактирования, а другой — для вывода ссылки на карточку торговой точки, используемый при выводе значения поля.
private static function getStoreSelector($fieldName, $fieldValue = null) { if (!Loader::includeModule('academy.crmstores')) { return ''; } $dbStores = StoreTable::getList(array('select' => array('ID', 'NAME'))); $stores = $dbStores->fetchAll(); $isNoValue = $fieldValue === null; ob_start(); ?> <select name="<?= $fieldName ?>"> <option value="" <?= $isNoValue ? 'selected' : '' ?>> <?= Loc::getMessage('CRMSTORES_NO_BINDING') ?> </option> <? foreach ($stores as $store): ?> <? $selected = $store['ID'] == $fieldValue ? 'selected' : ''; ?> <option value="<?= $store['ID'] ?>" <?= $selected ?>> <?= htmlspecialcharsbx($store['NAME']) ?> </option> <? endforeach; ?> </select> <? $selectorHtml = ob_get_clean(); return $selectorHtml; } private static function getStoreLink($storeId) { if (!Loader::includeModule('academy.crmstores')) { return ''; } $dbStore = StoreTable::getById($storeId); $store = $dbStore->fetch(); if (empty($store)) { return ''; } $storeDetailTemplate = Option::get('academy.crmstores', 'STORE_DETAIL_TEMPLATE'); $storeUrl = \CComponentEngine::makePathFromTemplate( $storeDetailTemplate, array('STORE_ID' => $store['ID']) ); return '<a href="' . htmlspecialcharsbx($storeUrl) . '">' . htmlspecialcharsbx($store['NAME']) . '</a>'; } |
После создания поля этого типа для сделок, в карточке сделки появилась возможность выбирать торговую точку.
Редактирование сделки | Просмотр сделки |
Создание компонента для вывода связанных сделок
Выведем в карточке торговой точки список сделок, связанных с ней.
Поскольку список сделок, прежде всего, список, снова делаем грид .
На этот раз мы реализуем минимум возможностей. Создадим компонент academy.crmstores:store.bounddeals, который принимает на вход идентификатор торговой точки, а на выход выдает список сделок, которые с ней связаны. Код метода executeComponent довольно прост, ниже приведен его фрагмент.
$dbStore = StoreTable::getById($this->arParams['STORE_ID']); $store = $dbStore->fetch(); if (empty($store)) { ShowError(Loc::getMessage('CRMSTORES_STORE_NOT_FOUND')); return; } $dealUfName = Option::get('academy.crmstores', 'DEAL_UF_NAME'); $dbDeals = DealTable::getList(array( 'filter' => array( $dealUfName => $store['ID'] ) )); $deals = $dbDeals->fetchAll(); $this->arResult = array( 'STORE' => $store, 'DEALS' => $deals, ); $this->includeComponentTemplate(); |
В списке выведем только ID, название и сумму сделки. Для этого используем компонент bitrix:main.ui.grid.
Шаблона компонента:
$rows = array(); foreach ($arResult['DEALS'] as $deal) { $dealUrl = CComponentEngine::makePathFromTemplate( Option::get('academy.crmstores', 'DEAL_DETAIL_TEMPLATE'), array('DEAL_ID' => $deal['ID']) ); $rows[] = array( 'id' => $deal['ID'], 'columns' => array( 'ID' => $deal['ID'], 'TITLE' => '<a href="' . htmlspecialcharsbx($dealUrl) . '">' . $deal['TITLE'] . '</a>', 'OPPORTUNITY' => CurrencyFormat($deal['OPPORTUNITY'], $deal['CURRENCY_ID']) ) ); } $APPLICATION->IncludeComponent( 'bitrix:main.ui.grid', '', array( 'GRID_ID' => 'CRMSTORES_BOUND_DEALS', 'HEADERS' => array( array( 'id' => 'ID', 'name' => Loc::getMessage('CRMSTORES_DEAL_ID'), 'type' => 'int', 'default' => false, ), array( 'id' => 'TITLE', 'name' => Loc::getMessage('CRMSTORES_DEAL_TITLE'), 'default' => true ), array( 'id' => 'OPPORTUNITY', 'name' => Loc::getMessage('CRMSTORES_DEAL_OPPORTUNITY'), 'default' => true, ) ), 'ROWS' => $rows ), $this->getComponent(), array('HIDE_ICONS' => 'Y') ); |
Вывод связанных сделок в карточке торговой точки
Осталось вывести этот список в карточке торговой точки.
Вызовем компонент привязанных сделок в шаблоне компонента карточки торговой точки (academy.crmstores:store.show) и захватим его вывод с помощью функций буферизации.
ob_start(); $APPLICATION->IncludeComponent( 'academy.crmstores:store.bounddeals', '', array( 'STORE_ID' => $arResult['STORE']['ID'] ), $this->getComponent(), array('HIDE_ICONS' => 'Y') ); $boundDealsHtml = ob_get_clean(); |
array( 'id' => 'deals', 'name' => Loc::getMessage('CRMSTORES_TAB_DEALS_NAME'), 'title' => Loc::getMessage('CRMSTORES_TAB_DEALS_TITLE'), 'fields' => array( array( 'id' => 'DEALS', 'colspan' => true, 'type' => 'custom', 'value' => $boundDealsHtml ) ) ) |
Создание установщика модуля
Если вы хотите сделать тиражное решение, следует доработать установщик модуля.
Код установщика вы можете посмотреть, скачав исходные коды, которые приложены к статье. Здесь опишем лишь основные задачи методов класса academy_crmstores.
Метод | Деятельность |
DoInstall | Регистрирует модуль в системе и запускает методы в следующем порядке:
|
DoUninstall | Запускает методы в следующем порядке, после этого помечает модуль как «не установленный»:
|
InstallEvents | Регистрирует обработчики двух событий:
|
InstallDB |
|
InstallFiles |
|
UnInstallEvents | Удаляет события, зарегистрированные в InstallEvents |
UnInstallFiles | Удаляет код компонентов из /local/components и правило из urlrewrite.php. |
UnInstallDB | Ничего не делает в данном примере. Будет правильно, если сделать удаление в два шага и спрашивать пользователя, хочет ли он удалить таблицы модуля. |
Заключение
Подведем итог. В Б24 есть два способа создания своей сущности:
- универсальные списки;
- «с нуля»: модуль, таблица, интерфейсы в публичной части.
Такой подход бывает нужен при крупных доработках Б24.
На практике некоторые функции списка вам могут быть не нужны. Например, если вы используете грид для вывода отчета, вам не нужно делать удаление, редактирование и т. п.
Стоит еще раз отметить, что полагаясь на опыт работы пользователей с интерфейсами Б24, вы можете снижать затраты на обучение. В этом уроке мы как раз повторили интерфейсы Б24 для созданной сущности.
-----
Спасибо за внимание
Материалы:
Автор статьи:
Так зато все рассмотрено пошагово, меньше самому разбираться
Не хочу докапываться, но бОльшая часть статьи читается так:
Заголовок: Строим Титаник своими руками!
Первые строчки: построение двигателя, а так же палуб выходит за рамки данной статьи, поэтому мы рассмотрим построение корабля на примере плота, где имея воображение Вы сможете достроить все что угодно.
Начинается с того, что под совершенно, на мой взгляд, нелепыми аргументами отметаются Универсальные списки (ровно как и процессы).
И для УС и для Процессов можно разрабатывать свои кастомные вьюхи и пользоваться большими преимуществами системы: Бизнес-процессы, права доступа, административный интерфейс.
Хорошо.. вот отметаются универсальные списки. Зачем? Чтобы взять и сделать аналог табличных данных! Поздравляю - Вы переизобрели УС только более бедный по возможностям.
Все то, что вкладывается в понятие "своя сущность в Битрикс24" как раз и не было рассмотрено (нет, были определенные моменты, которые стоило рассмотреть - например меню или вкладки в CRM).
Из чего складывается своя сущность вообще?
- Подсистема хранения (не важно это своя таблица, highloadblock, инфоблок или вообще внешняя система)
- Визуальная часть (все формочки редактирования, просмотра и т.п.)
- Управление доступом
- Бизнес-процессы и бизнес-смыслы (т.е. то, что помимо всего остального можно дополнительно с ней делать)
- Механизма управления для администратора
Нет, не всегда и не все должно включаться - все зависит от задачи, но вот так вот сразу пренебрегать доступными из коробки средствами?
Ну окей, допустим Вы рассматриваете только эти две части, но в чем именно ценность вебинара?
Рассказать про вышеупомянутые статьи, которые можно использовать только в разрезе Битрикс24? Тогда хинт: заменяем bitrix:crm.interface.* на bitrix:main.interface.*, убираем спецэффичные части CRM и вот универсальный механизм как делать новые сущности не только для Битрикс24, но и для БУСов.
Я ничуть не хочу занизить ценность данного вебинара для 1С-Битрикс, но ценность именно в разрезе Битрикс24 сомнительна.
Хотелось бы увидеть нечто большее чем "сделали собственный велосипед, натянули устаревшие интерфейсы и сделали CRUD".
Показали как вывести что-то свое в типовых гридах и сценариях И то это на 2 часа по верхам.
Но я тебя услышал, хорошие и конкретные тезисы что хочется еще.
Есть у нас мысли дальше показать как БП сделать по свой сущности.
Особенно, если "мяса" будет больше
Для свойств типа "Привязка к элементам инфоблока" отображение в списке работает корректно.
Проблема решается только изменением ядра, буквально несколькими строками
Файл: /bitrix/modules/crm/classes/general/crm_usertype.php
Строка 1227 добавить:
Строка 1354 добавить:
строка 1476 добавить:
примерное содержание:
Надеюсь Иван передаст это своим коллегам и они внесут необходимые изменения в ядро
Будем надеяться что внесут
Также внес в файл storebinding.php (маршрут /bitrix/modules/academy.crmstores/lib/usertype/storebinding.php) приведенную Вами функцию GetListValues (только исправив в ней AccountTable::getlist на $query = StoreTable::getlist, т.к. класс в примере зовется StoreTable).
В результате о вкладке CRM "Сделки" в списке сделок в поле-свойстве "Торговые точки" теперь вместо неправильного варианта отображения (id сделки) стало отображаться текстовое название торговой точки. Спасибо Вам, удалось починить визуализацию польз. типа!
Ожидаю внесения Вашего замечательного патча в код фирменного crm_usertype.php разработчиками Битрикса.
есть компонент фильтра и грида
а компонент для реадактирования сущности есть доступный в главном модуле (те. компонент карточки в таком же стиле)?
такие компоненты актуальны как раз для редакци где нет модуля Универсальных списков
в иных случаях вообще не понятно зачем городить огород когда настройкой списков все решается и реализацией пользовательских полей для них
Торговая точку будем описывать небольшим количеством полей (этого достаточно для примера):
название;
адрес;
ответственный.
веть это какаято дичь - для сущности из трех полей писать кучу кода
когда можно взять Универсальный список или Список БП
Только я не понял, это для CRM или же как раз для кастомизации УС (унив. списков) ? Борюсь вот с недугом стандартных унив. списков по имени "Нет зоны выбора разделов" (как в Товарах в CRM).Почти сделал, но есть проблема с визуализацией в моем "переплавленном списке" поля типа "S:DiskFile". Просто мне надо некий библиотечный каталог разработать, залить в него электр. версии книг (pdf-файлы), ну и чтобы юзер мог тыкнуть мышой по книжке прям в списке, ну и открылся встроенный битриксовый просмотрщик текстов (если поле делать просто типа Файл, то тогда по клику на ссылке вместо просмотра будет лишь скачивание файла, увы). Вот думаю повторить маневры с разработкой модуля "Библиотека" в режиме создания полностью своей сущности. Сейчас ведь я по суди криво-косо "склонировал" спец-инфоблок под торговый каталог и попытался обычный инфоблок научить отображаться как некий каталог с разделами
т.е. доступы с самой начальной редакции
это просто реализация внешнего вида нового списка и фильтра
а уж какой набор полей вы добавите и как будете хранить - такие ограничения и будут
но по вашему кейсу достаточно просто реализовать пользовательское поле (или это будет свойство инфоблока) на основе стандартного
т.е. достаточно будет реализовать как оно будет представлено в списке
когда можно взять Универсальный список или Список БП
> Для вывода карточки торговой точки в режиме просмотра и редактирования будем использовать компонент bitrix:crm.interface.form. Это «старая» карточка CRM, которая использовалась до появления слайдера.
плохо это
Материалы готовы, но не уверен что на этой неделе успею сверстать в блог. След неделя - уже точно.
Исходя из вашего руководства, допустим у торговой точки есть еще "Тип торговой точки" (для типа торговой точки создана сущность). Дадим возможность выбора "типа" для торговой точки. Логично было бы создать пользовательское поле "Привязка к торговой точке", а объектом указать STORETYPE_ENTITY. (т.е так же как мы объектом указывали CRM_DEAL). НО для стандартных сущностей новое поле отображается, а для пользовательской (STORETYPE_ENTITY). Как быть:?
если чтото свое передали в пост запросе - он это не подставляет в запросы
$gridFilter = new Filter\Options(self::GRID_ID, self::$filterPresets);
$gridFilterValues = $gridFilter->getFilter(self::$filterFields);
$stores = $this->getStores(array(
'filter' => $gridFilterValues,
'limit' => $pager->getLimit(),
'offset' => $pager->getOffset(),
'order' => $sort
));
я в примере не вижу никакой обработки фильтра
у меня получается
array (size=10)
'ID_numsel' => string 'more' (length=4)
'ID_from' => string '35' (length=2)
'ID_to' => string '' (length=0)
'CREATED_BY_numsel' => string 'exact' (length=5)
'CREATED_BY_from' => string '1' (length=1)
'CREATED_BY_to' => string '1' (length=1)
'PRESET_ID' => string 'tmp_filter' (length=10)
'FILTER_ID' => string 'tmp_filter' (length=10)
'FILTER_APPLIED' => boolean true
'FIND' => string '' (length=0)
это же не подходит для ORM
Для реализации в фильтре выбора ответственного сотрудника
такая контсрукция уже не работает:
нужно данный массив формировать так (подглядел в сделках):
Но на мой взгляд, там оч. муторно.
Если требуется отредактировать какие-то определенные поля, то лучше вывести грид с контекстным меню, и в нем посадить обработчик, который будет редактирвать нужные нам поля
'id' => 'ASSIGNED_BY_ID',
'name' => Loc::getMessage('CRMSTORES_FILTER_FIELD_ASSIGNED_BY'),
'type' => 'dest_selector',
'params' => array(
'context' => 'CRMSTORES_FILTER_ASSIGNED_BY_ID',
'multiple' => 'Y',
'contextCode' => 'U',
'enableAll' => 'N',
'enableSonetgroups' => 'N',
'allowEmailInvitation' => 'N',
'allowSearchEmailUsers' => 'N',
'departmentSelectDisable' => 'Y',
'isNumeric' => 'Y',
'prefix' => 'U',
),
'default' => true,
),
Возникла задача перебирать не только сотрудников, но всех активных пользователей (редакция "Интернет-магазин + CRM"), но как не пробовал менять параметры, ищет только сотрудников.
Как сделать обычный user.selector в фильтре?
'id' => 'ASSIGNED_BY_ID',
'name' => Loc::getMessage('CRMSTORES_FILTER_FIELD_ASSIGNED_BY'),
'type' => 'dest_selector',
'params' => array(
'context' => 'CRMSTORES_FILTER_ASSIGNED_BY_ID',
'multiple' => 'Y',
'contextCode' => 'U',
'enableAll' => 'Y',
'enableSonetgroups' => 'Y',
'allowEmailInvitation' => 'N',
'allowSearchEmailUsers' => 'N',
'departmentSelectDisable' => 'Y',
'isNumeric' => 'Y',
'prefix' => 'U',
),
'default' => true,
),
.
Пробовал разные вариации.
Скорее нужно через custom, но это не так тривиально.
не подойдет, т.к. у него нет в настройках нет нужных параметров:
Все остальное самостоятельно.
Чтобы фильтр появился, его необходимо настроить. А именно — указать по каким полям можно фильтровать и задать предустановленные именованные фильтры.
Поля для фильтра опишем в компоненте списка следующим образом.
то поля фильтра нужно описывать так:
{ReferenceName}.{FieldName}
где {ReferenceName} = имя Reference поля связанной сущности
{FieldName} - имя поля в связанной сущности.
см. в примере ниже последний элемент массива.
Кроме этого, при добавлении в константу: FILTERABLE_FIELDS это поле, необходимо указывать со сслыкой на связанную сущность:
И как добавить toolbar (кнопки сверху возле фильтра)?
upd. сорян, тут про компоненты crm.... а как-то возможно наиграть подобный результат из main.ui.grid и main.ui.filter?
Есть необходимость выводить в main.ui.filter типовым способом пользователей.
В идеале конечно бы получить поле поиска сотрудника как в сделках/компаниях и т.д