
В крупном бизнесе нередко случаются ситуации, когда внедряются и используются заведомо ущербные информационные системы. Эти проекты начинаются как крутая собственная разработка компании, под её процессы, с учётом всех особенностей. Но уже после сдачи выясняется, что то тут, то там недоделки, несуразности. Что необходимые отчёты и графики получить невозможно, поскольку их не смогли или забыли учесть в ТЗ. Руководство требует, потом просит что-нибудь сделать, но система закрыта для изменений, а подрядчик находится с нами в процессе арбитражной тяжбы. Однако, безвыходных ситуаций конечно же не бывает.
Эта статья появилась, как продолжение давно начатого разговора о том, как быстро поднять систему управленческого учёта (СУУ) «без бюджета». Всё описанное ниже делали не профессиональные программисты и не подрядчики. Основная идея заключается в том, что опытный менеджер осваивает навыки программирования и делает решение конкретной задачи используя хорошо документированные технологии. Результат такого подхода позволяет сэкономить значительные деньги и время. Как показывает опыт, таким «прокаченным менеджерам» намного проще самим сделать модель того, что понадобится в будущем заказать у поставщика уже для масштабного решения.
Итак, у нас уже была простенькая СУУ на базе 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. Оно будет включать в себя минимальный набор файлов:
- «manifest.json» — включает основные параметры расширения, права доступа к браузеру, указания на «контент скрипты», о которых ещё будет сказано ниже
- «page_action.html» — представление расширения. В нашем случае оно открывалось в виде модального окна поверх формы «большой системы». Логику контроллера Angular мы вынесли в отдельный файл «page_action.js», а потом ещё в другие файлы
- «inject.js» – тот самый «контент-скрипт», который позволяет совершить некое чудо с страницей поверх которой открывается расширение. Можно вообще полностью её перерисовать
- «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, которая выстроена совсем иначе и масштабируется на всё агентство. Описанное в этой статье расширение позволило на протяжении полутора лет вести управленческий учёт без дублирования работы. Неотъемлемой частью СУУ являются отчёты о которых хотелось бы рассказать подробно, но уже за рамками этой статьи.