В крупном бизнесе нередко случаются ситуации, когда внедряются и используются заведомо ущербные информационные системы. Эти проекты начинаются как крутая собственная разработка компании, под её процессы, с учётом всех особенностей. Но уже после сдачи выясняется, что то тут, то там недоделки, несуразности. Что необходимые отчёты и графики получить невозможно, поскольку их не смогли или забыли учесть в ТЗ. Руководство требует, потом просит что-нибудь сделать, но система закрыта для изменений, а подрядчик находится с нами в процессе арбитражной тяжбы. Однако, безвыходных ситуаций конечно же не бывает.

Эта статья появилась, как продолжение давно начатого разговора о том, как быстро поднять систему управленческого учёта (СУУ) «без бюджета». Всё описанное ниже делали не профессиональные программисты и не подрядчики. Основная идея заключается в том, что опытный менеджер осваивает навыки программирования и делает решение конкретной задачи используя хорошо документированные технологии. Результат такого подхода позволяет сэкономить значительные деньги и время. Как показывает опыт, таким «прокаченным менеджерам» намного проще самим сделать модель того, что понадобится в будущем заказать у поставщика уже для масштабного решения.

Итак, у нас уже была простенькая СУУ на базе MS Access + MS SQL, с необходимым набором полей и всеми требуемыми отчётами. Доработка её велась постоянно, поскольку всё время возникали новые требования по удобству, дополнительным функциям.

Вскоре пришло понимание некоторой ограниченности MS Access в плане front-end. То есть можно было конечно же делать кастомные контролы, писать библиотеки, но как-то не хотелось ввязываться в эту непростую историю. Изначально цель проекта была найти максимально простое и быстрое решение конкретной задачи – учёта. Раздувать Access в полноценный аналог 1С или чего-то подобного было бы ошибкой. Access не предназначен для построения серьёзных систем. Кстати, именно по этой причине мы решили не масштабировать систему на все департаменты агентства, а ограничились только подразделением Digital-рекламы.

Появились, также, проблемы с совместимостью. Access Runtime решал вопрос запуска СУУ у пользователей без установленного полноценного платного MS Access. Однако, версии Windows и Office у многих оказались разные, как и подключённые библиотеки. В результате пришлось потанцевать с бубном для запуска системы у сотрудников с установленным Office 2016.

В довершение ко всему, имелась двойная работа по внесению данных в СУУ на базе Access и «большую систему» документооборота компании, п��скольку часть вносимой информации всё же была общей. Пока наша система испытывалась на небольшом отделе внутри департамента Digital эта проблема была не так актуальна: пользователи были заинтересованные. Раньше они сами вели такую вторую «базу» со всеми необходимыми им данными в формате Excel. После распространения СУУ на все баинговые отделы (контекстная реклама, SMM, продакшн и прочие), народ начал потихоньку нас ненавидеть. Двойной работы стало многовато в масштабе департамента.

Главное затруднение при этом состояло в том, что «большая система», куда обязательно нужно было вносить часть данных, стоившая компании миллионы рублей и годы работы, была для нас абсолютно закрыта. Исходников не было. Доступ к базе был, но поскольку от работоспособности этого решения зависел бизнес компании, и мы не всегда могли понять, как она работает (закрытые исходники), было решено не влезать в базу.

Упрощённо говоря, требовался единый терминал для заполнения информации в двух базах одновременно: «большой» (с меньшим числом полей) и «маленькой» (с большим числом полей). Причём, как уже говорилось выше, без возможности доступа к одной из баз иначе чем через веб-интерфейс пользователя.

Казалось бы, неразрешимая задача. Но тут попалось мне расширение Google Chrome, уже не помню как оно называлось, суть которого была в том, что оно изменяло форму отображения информации на веб-странице, дополняя её отсутствующими ранее полями. При этом для пользователя практически ничего не менялось. Он заходил по тому же самому URL, но видел гораздо более удобную форму. Это расширен��е брало информацию из полей и сохраняло на другом сервере, где потом можно было увидеть эти дополнительные данные. Идея нам понравилась. Фактически требовалось примерно то же и в нашем случае.

Схема хранения данных выглядит следующим образом:



На фронт-энде была выбрана связка: Расширение Google Chrome+Bootstrap+Angular. Почему так? Bootstrap избавил от необходимости долго прорабатывать вёрстку. Мы использовали дефолтную тему и пользователям она вполне понравилась. Angular позволил быстро выстроить логику поведения формы и взаимодействия с бэк-эндом. Удобные контролы настраивались на раз. Практически не было проблем с совместимостью между Bootstrap и Angular. Для контролов использовалась библиотека UI Bootstrap. Для интерактивных таблиц использовали Angular UI Grid совместно с Angular JS dropdown multi-select, которая и добавляла данные в таблицу. Кроме того, были использованы другие библиотеки Angular, среди которых ngDialog для прорисовки всплывающих диалогов и некоторые вспомогательные (зависимости), для указанных выше модулей.

Настройка самого расширения Google Chrome заняла не более пары дней копаний документации. Этот вопрос разберём более подробно. Начать следует с того, что для нормальной дистрибуции новых версий расширения необходимо приобрести (разово) доступ к интернет-магазину Chrome. Ограничений на разработку и выкладывание расширений в настоящий момент нет, хотя выкладывание новых приложений Google Chrome недавно обрубили. Вроде как планов относительно выключения расширений пока не было. Выкладка новой версии представляет собой выгрузку зазипованной папки в которой идёт разработка (файл «manifest.json» в корне).

Для тестирования расширения необходимо зайти: «Главное меню» -> «Дополнительные инструменты» -> «Расширения» -> «Загрузить распакованное расширение». При обновлении кода нужно каждый раз нажимать «Обновить расширения», чтобы изменения вступили в силу и отобразились. Если для разработки использовать Chrome Dev Editor, то обновление расширений происходит автоматически. Впрочем, мы уже перешли с него на Visual Studio Code, он нам показался гораздо более удобным.

Простейшее расширение можно создать при помощи конструктора: Extensionizr. Оно будет включать в себя минимальный набор файлов:

  1. «manifest.json» — включает основные параметры расширения, права доступа к браузеру, указания на «контент скрипты», о которых ещё будет сказано ниже
  2. «page_action.html» — представление расширения. В нашем случае оно открывалось в виде модального окна поверх формы «большой системы». Логику контроллера Angular мы вынесли в отдельный файл «page_action.js», а потом ещё в другие файлы
  3. «inject.js» – тот самый «контент-скрипт», который позволяет совершить некое чудо с страницей поверх которой открывается расширение. Можно вообще полностью её перерисовать
  4. «background.js» — отслеживает что происходит на странице, поверх которой будем открывать форму. Содержит правила показа расширения

В нашем случае расширение активировалось только когда пользователь открывал определённую форму. Чтобы «узнать» эту форму было задано следующее правило в background.js:

var rule1 = {
  conditions: [
    //На какой id присутствующий на странице реагировать активацией значка расширения
    
    new chrome.declarativeContent.PageStateMatcher({
      css: ["textarea[id='id_поля_на_которое_реагируем_активацией_значка_расширения']"]
    })
  ],
  
  actions: [ new chrome.declarativeContent.ShowPageAction() ]
};

chrome.runtime.onInstalled.addListener(function () {

    chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {

	//Добавляем правило в Chrome
        chrome.declarativeContent.onPageChanged.addRules([rule1]);

    });

});

Чтобы по клику на кнопку расширения открывалось окно, в том же background.js был такой код:

chrome.pageAction.onClicked.addListener(function (tab) {
	//Получаем id таба
  tabid = tab.id;

  
    var w = 1400;
    var h = 900;
    var vleft = (screen.width/2)-(w/2);
    var vtop = (screen.height/2)-(h/2);
    chrome.tabs.create({
        url: chrome.extension.getURL('/src/page_action/page_action.html'),
        active: false
    }, function (tab) {
        //Свойства диалога, который открывается по клику на кнопку расширения 
        chrome.windows.create({
            tabId: tab.id,
            width: w,
            height: h,
            type: 'popup',
            focused: true,
            left: vleft,
            top: vtop
        },
          function(window) {
          }
        
        );
    });

})

Сам файл page_action.html это обычная HTML-страничка, но в нашем случае она ещё содержала директивы Angular:

<html lang="en" ng-app="название_приложения_в_инструкции_angular">
<head>
<!-- Подключение различных скриптов и css, title -->
</head>
<body role="document" ng-controller="названия_контроллера">
<!-- Код формы -->
</body>
</html>

Контроллер у нас был один и как уже было написано выше, он жил в файле page_action.js (основная логика) и куче других файлов .js, содержавших конкретную реализацию каждого участка логики.

Для связи записей в базах использовалось поле «Комментарий» в форме «большой системы» (поверх которой открывалось расширение). При добавлении новой записи, расширение вставляло в поле комментарий запись вида: {”ext_id”:”1233”}. Далее, при открытии расширения поверх любой записи в «большой системе», оно сразу подгружало сохранённые ранее данные в сво�� форму.

Отдельно хотелось бы затронуть тему контент-скриптов (файл «inject.js»). Дело в том, что это уникальная возможность Google Chrome, позволяющая полностью перерисовывать оригинальную страничку. Приведу пример. В «большой системе» отсутствовала табличка с некими сводными данными по рекламным размещениям в рамках проекта. Эта табличка сильно упростила бы жизнь сотрудникам, работающим с формой, поскольку конкретное размещение сразу было бы видно в контексте. Для реализации был создан контент-скрипт, который выводил кнопку открывающую модальное окно с табличкой и содержал логику самой таблички, а также код подгрузки данных в неё.

Вначале вставляем кнопку на страничку, поверх которой работает расширение Google Chrome:

//Определяем HTML-элемент в который будет вставлена наша кнопка
var parent_node = document.getElementsByTagName("id_родительской_ноды");

//Определяем новый HTML-элемент, который и будет нашей кнопкой (на самом деле ссылкой) 
var elem = document.createElement('a');
elem.setAttribute("href", "#");
elem.setAttribute("class", "dyn");
elem.setAttribute("ng-click", "openTotalsBySellers()");
var html_code = '<strong style="color:green;">[Итого селлеры]</strong>' + "\n";
elem.innerHTML = html_code;

//Добавляем новый элемент в родительский
parent_node.appendChild(elem);

//Перекомпилируем страничку с помощью Angular, чтобы кнопка появилась
$compile(elem)($scope);

Вы могли заметить функцию «openTotalsBySellers», которая собственно и открывает нужную нам табличку. Приведу код функции контент-скрипта, которая вставляет нужный JavaScript на исходную страничку (на которой мы только что поместили кнопку:

function defineDialogTableTotalsBySellers($scope, $compile, uiGridConstants, parent_node) {
//Определяем таблицу, которая будет в всплывающем окне (используется ui-grid)
$scope.totalsBySellersG = {
        showGridFooter: true,
        showColumnFooter: true,
        enableFiltering: true
  };

//Определяем колонки таблицы
$scope.totalsBySellersG.columnDefs = [
        { name: 'seller', displayName:'Площадка'},
        { name: 'totals_agency', displayName:'Сумма', aggregationType: uiGridConstants.aggregationTypes.sum, type: 'number'},
  ];
//Переменная для данных
$scope.totalsBySellersG.data = [];

//Волшебная функция без которой таблица не появится в диалоге
$scope.totalsBySellersG.onRegisterApi = function(gridApi){
      //set gridApi on scope
      $scope.gridApi = gridApi;
 };
  
//JavaScript код диалога, в котором будет таблица (используется ngDialog)
var dialog = document.createElement('script');
dialog.setAttribute("id", "openTotalsBySellers");
dialog.setAttribute("type", "text/ng-template");
html_code = "";
html_code += '<div class="ngdialog-message">';
html_code += '<h2>Итоги по селлерам (Resolution Money)</h2>';
html_code += '<br /><div ui-grid="totalsBySellersG" class="grid"></div>';
//Переменная $scope.total_sum пересчитывается каждый раз при открытии формы
html_code += '<br /><strong>Полная стоимость (с НДС):</strong> {{total_sum}}<br /><br />';
  html_code += '</div>';
  html_code += '<div class="ngdialog-buttons mt">';
  html_code += '<input type="button" value="Закрыть" ng-click="closeThisDialog()"/>';
  html_code += '</div>';
  dialog.innerHTML = html_code;
  parent_node.appendChild(dialog);
  $compile(dialog)($scope);
  
}

На бэк-энде работала связка Apache + PHP + MS SQL. Подробно расписывать, наверное, смысла нет, но основные идеи были в следующем. Мы решили не извращаться с шифрованием данных: всё-таки расширение работало только внутри компании. Чтобы не пересылать сырой JSON, данные упаковывались в Base64 и так уходили на сервер и обратно. Использовался метод POST, поскольку он не содержит ограничений по объёму данных. Изначально архитектура приложения PHP была выстроена на базе шаблона ApplicationHelper, описанного в замечательной книге Мэтта Зандстра «PHP объекты, шаблоны и методики программирования» (Вильямс, 2015).

Общая архитектура системы выглядит следующим образом:



В результате менее чем через месяц было получено рабочее решение, которое действует до сих пор. За этот год у нас появилось чёткое представление о желаемой функциональности и формах. Фактически менеджерами, которые разбираются в нюансах бизнеса, самостоятельно была создана модель «идеальной» Системы управленческого учёта, полностью адаптированной под наши задачи. Благодаря этому, мы с гораздо меньшей кровью скоро заменим расширение Google Chrome на большую информационную систему, включающую workflow, которая выстроена совсем иначе и масштабируется на всё агентство. Описанное в этой статье расширение позволило на протяжении полутора лет вести управленческий учёт без дублирования работы. Неотъемлемой частью СУУ являются отчёты о которых хотелось бы рассказать подробно, но уже за рамками этой статьи.