В крупном бизнесе нередко случаются ситуации, когда внедряются и используются заведомо ущербные информационные системы. Эти проекты начинаются как крутая собственная разработка компании, под её процессы, с учётом всех особенностей. Но уже после сдачи выясняется, что то тут, то там недоделки, несуразности. Что необходимые отчёты и графики получить невозможно, поскольку их не смогли или забыли учесть в ТЗ. Руководство требует, потом просит что-нибудь сделать, но система закрыта для изменений, а подрядчик находится с нами в процессе арбитражной тяжбы. Однако, безвыходных ситуаций конечно же не бывает.
Эта статья появилась, как продолжение давно начатого разговора о том, как быстро поднять систему управленческого учёта (СУУ) «без бюджета». Всё описанное ниже делали не профессиональные программисты и не подрядчики. Основная идея заключается в том, что опытный менеджер осваивает навыки программирования и делает решение конкретной задачи используя хорошо документированные технологии. Результат такого подхода позволяет сэкономить значительные деньги и время. Как показывает опыт, таким «прокаченным менеджерам» намного проще самим сделать модель того, что понадобится в будущем заказать у поставщика уже для масштабного решения.
Итак, у нас уже была простенькая СУУ на базе 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, которая выстроена совсем иначе и масштабируется на всё агентство. Описанное в этой статье расширение позволило на протяжении полутора лет вести управленческий учёт без дублирования работы. Неотъемлемой частью СУУ являются отчёты о которых хотелось бы рассказать подробно, но уже за рамками этой статьи.
Комментарии (11)
SergeyUstinov
08.03.2017 17:46+1Хорошая статья.
Но в первую очередь как иллюстрация цитаты
Русские сами создают себе трудности, а потом их героически преодолевают.
Уинстон Черчиль
Сперва за много миллионов внедрили закрытую систему, а потом с этим боролись. :)))
Кстати, а были сделаны какие-то шаги, чтобы еще раз не наступить на те же грабли, но уже с новой системой?nsuvorov
09.03.2017 10:24Конечно) Полностью сменили команду. Новая была подчинена лично высшему руководству компании. Каждую неделю/две — статус. Руководителем проекта стал IT-директор, вместо отдельно нанятого под это задачу товарища. Ошибки были проанализированы. Пришли в выводу, что самописная система несёт слишком большие риски в плане непродуманной архитектуры и безопасности. Выбрали коммерческую систему известного вендора. Позже расскажу подробнее.
SergeyUstinov
09.03.2017 16:42Было бы интересно почитать. Это более «универсальный» опыт.
Ведь то, что у вас получилось разработать таким образом вашу систему — это во многом случайность. Структура данных вашего решения хорошо «стыковалась» со структурой данных «большой» закрытой системы. А это бывает далеко не всегда.nsuvorov
09.03.2017 18:08Идея как раз в том, что мы «маленькое решение» смогли подстроить под «большое». Из полей острая необходимость была только в поле «Комментарий», куда засовывается id по которому мэтчим данные.
Ananiev_Genrih
10.03.2017 13:28"Появились, также, проблемы с совместимостью. Access Runtime решал вопрос запуска СУУ у пользователей без установленного полноценного платного MS Access."
у нас ms sql server + Access 2013 (большая часть бизнес логики реализована в формах через VBA) в связке через odbc native client
(Я знаю что это некрасивое решение но когда данных много а пользователей мало — вполне себе жизнеспособно)
Т.е. у каждого пользователя (а их к счастью мало) требуется проф.версия офис с Access на борту + установка тех.поддержкой соотв.драйвера odbc.
Если потребуется расширить список сотрудников (пока не говорим о теме данной статьи с web ) в рамках той же связки, эту связку потянет Access Runtime?
(версии Windows и Office у всех одинаковые)nsuvorov
11.03.2017 18:54У нас именно такую задачу runtime и решает. Попробуйте. Если пользователи не должны сами делать таблицы/запросы, а только работают с формами, то всё будет ок.
ErshoFF
16.03.2017 19:18Вроде как прикрывается магазин в хроме
http://www.androidpolice.com/2016/08/19/google-phasing-chrome-apps-will-no-longer-work-2018/
Shadow_ru
…
Чтобы не пересылать сырой JSON, данные упаковывались в Base64 и так уходили на сервер и обратно. Использовался метод POST, поскольку он не содержит ограничений по объёму данных.
…
Оба заявления, простите, глупость. JSON чудно пакуется gzip, POST таки ограничен, правда по дефолту 8МБ, по моему на Апаче.
nsuvorov
GZip можно да. Речь о том, что GET ограничен и от него отказались в пользу POST.