Часть 1. Общее описание подхода
Впервые я столкнулся с CRM-системами совершенно неожиданным для себя образом. Когда я пришел в новую компанию, то обнаружил господствующую в диспетчерской систему учета заявок — GLPI. Никогда ранее я не слышал о ней, однако, спустя какое‑то время, прилетел таск на то, чтобы подумать и воплотить в жизнь некие дашборды или наглядные отчеты по следующим критериям:
общее суточное количество заявок
распределение по отделам
остальные подобные метрики и их производные
GLPI предлагает как дефолтные инструменты репортов, так и возможность установить дополнительные плагины из специального маркетплейса (платно). Но меня все это не впечатлило. К тому же, было интересно решить задачу самостоятельно, не прибегая к готовым продуктам. Я стал думать, чтобы такое придумать и «не изобретать велосипед». Хотелось взаимодействовать с GLPI по минимуму. Иметь возможность просто и быстро получать данные и делать с ними все, что угодно. А главное, не разбираться в документациях к API и не копаться в PHP. Я хотел использовать имеющиеся у себя знания и именно на их базе создать некий сервис, который смог бы выполнять поставленные задачи.
Решение было принято вскоре. Я пришел к варианту о том, что достаточно иметь возможность взаимодействия исключительно с базой данных GLPI и ее структурой таблиц и их реляций. Стек, который показался мне наиболее подходящим для данной задачи выглядел так:
Node JS
Express
Handlebars
Знакомство с фреймворком Express я советую начинать с книги Итана Брауна - "Веб-разработка с применением NODE Js и Express". Я сам продолжаю по сей день пользоваться ей как проводником и наставником. Применяю Java Script в Arduino с помощью Johnny-Five. Активно применяю JS для решения кейсов по основному роду деятельности. И, конечно, надеюсь, что в обозримом будущем смогу в полной мере использовать накопленный опыт и знания в этой области для того, чтоб еще больше подружить промышленную автоматизацию с JavaScript.
Самая точная фраза, на мой взгляд, которой довольно точно и емко можно описать фреймворк Express приводится самим Итаном Брауном в книге, упомянутой выше:
... Просто он в меньшей степени становится у вас на пути, позволяя более полно выражать свои идеи. - Itan Braun [1]
Немного описательной части структуры каталогов приложения...
Как правило, каталог src — основной для проекта. «public» — служил на первых парах для хранения файлов css, js, изображений и тестирования. Папка «views» — это про handlebars и систему шаблонов. Также имеется файл «accessMain.log». По его расширению понятна его «цель жизни»:) Так же файл со странным названием «indexV3..2_mine.js» - является основным и самым важным.
Вернемся же к нашему решению, которое будет реализовывать взаимодействие между приложением клиентом и системой GLPI.
За что я люблю Node JS, так это за многообразие NPM пакетов. Так сказать, на все случаи жизни вы можете найти что‑то подходящее для себя среди 1000 NPM — ов. Действительно, практически всегда я нахожу оптимальный пакет для того, чтобы решить очередную задачу. И в этот раз мне не пришлось долго искать нужный, и я взял на вооружение npm "mysql" . Ссылка на пакет.
Довольно простая в использовании библиотека позволяет гибко взаимодействовать с базой данных. В описании пакета подробно приведены основные методы взаимодействия с БД.
В идеале, нам достаточно открыть соединение и получить данные в ответ на цепочку SQL запросов. Так мы и сделаем. Настроим параметры соединения согласно файлу описания пакета:
Пример с офф док:
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost',
user : 'me',
password : 'secret',
database : 'my_db'
});
connection.connect();
connection.query('SELECT 1 + 1 AS solution', function (error, results, fields) {
if (error) throw error;
console.log('The solution is: ', results[0].solution);
});
connection.end();
Для представления данных на стороне клиента мы будем формировать объект и передавать его в некий шаблонизатор для последующего рендеринга HTML в ответ на GET — запрос пользователя.
О POST
Тоже самое мы будем делать и на POST запрос, однако, с той разницей, что в функцию получения данных мы передадим параметры из формы со странички клиента.
В поставленной передо мной задаче было четко обозначено, что хочет видеть конечный пользователь по итогу выполнения запроса. Необходима информация следующего характера:
Заголовок заявки
Статус заявки
Количество заявок по отделам
Общее количество заявок за выбранный период
Вытащить непосредственно имя заявки, содержание, общее количество заявок - не представляется чем‑то трудным. В то же время, для того, чтобы прогнать полученные данные через скрипты подсчета и статистики, прежде необходимо составить сложный SQL запрос. Изучив немного таблицы GLPI я решил, что для получения данных о принадлежности заявки к определенному отделу я буду сопоставлять id инициатора с id группы к которой он принадлежит в системе пользователей GLPI.
connection.query(`SELECT COUNT(*) FROM glpi_tickets WHERE date BETWEEN '${date[0]}' AND '${date[1]}'`, function (err, rows1) {
if (err) throw err;
connection.query(`SELECT DATE_FORMAT(date, '%d/%m/%Y %H:%i'), name, users_id_recipient, status FROM glpi_tickets WHERE date BETWEEN '${date[0]}' AND '${date[1]}'`, function (err, rows2) {
if (err) throw err;
connection.query(`SELECT * FROM glpi_tickets
INNER JOIN glpi_tickets_users ON glpi_tickets.id=glpi_tickets_users.tickets_id
INNER JOIN glpi_groups_users ON glpi_tickets_users.users_id=glpi_groups_users.users_id
INNER JOIN glpi_groups ON glpi_groups_users.groups_id=glpi_groups.id WHERE date BETWEEN '${date[0]}' AND '${date[1]}'`, function (err, rows3)
{ ...
Как видно из фрагмента кода выше, все запросы сведены в колбэк цепочку, в которой предусмотрено отображение в консоли ошибки в случае ее возникновения на этапе транзакции.
Решение проблемы с форматом времени
Для корректного отображения даты в удобочитаемом формате пришлось прибегнуть к встраиванию в SQL запрос функции конвертации временного формата - DATE_FORMAT(date, '%d/%m/%Y %H:%i');
В ответ на наш настойчивый запрос БД ответит нам данными вида Row Data Packet Object.
После того, как данные получены, нам надо их привести в порядок, что называется. Причесать, «отмыть» и распределить по структуре нашего основного объекта, который будет использовать для процесса представления. Это можно сделать простым заполнением объекта, проходясь в цикле по полученным данным.
По итогу получаем основной структурированный объект, который и будем передавать в шаблонизатор.
Представление на стороне клиента
Как я упоминал выше, для представления данных на стороне клиента я буду использовать подход шаблонизации. Подробнее можно почитать тут. А еще тут.
В целом же, философия шаблонизации представления всегда сводится к одним и тем же намерениям:
не писать повторяющийся код несколько раз
тиражирование правок, вносимых в один компонент
Однако, надо помнить об одной детали такого подхода. Существует много шаблонизаторов с разным уровнем абстракции. То есть уровень того, на сколько мы абстрагируемся или, как мне кажется, лучше сказать — "удаляемся" от классического HTML. Handlebars имеет явное преимущество в это плане. Так же, имеется следующий факт, который я нахожу немаловажным:
Handlebars компилирует шаблоны в функции JavaScript. Это делает выполнение шаблона быстрее, чем у большинства других шаблонизаторов.
— https://handlebarsjs.com
Уместно будет представить моему читателю и способ подключения Handlebars в Express, а также структуру его каталогов.
Этот фрагмент кода располагается в основном файле приложения:
...
/*HANDLEBARS*/
const expressHandlebars = require("express-handlebars").engine;
app.engine('handlebars', expressHandlebars({
defaultLayout: 'main',
}));
app.set('view engine', 'handlebars');
...
Ниже приведена структура каталогов папок и файлов Handlebars.
Содержание файла main.handlebars. Это основной каркас пользовательской страницы. К примеру, в нем из ранее сформированного объекта для представления берется значение поля count и подставляется в ячейку таблицы <td> с id=count.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GLPI Analytics</title>
</head>
<style>
...
</style>
<body>
<h1>GLPIReport [beta]</h1>
...
<table id="count">
{{#each this.count}}
<tr>
<td>
Заявок всего
</td>
<h2>
<td>
{{this}}
</td>
</h2>
{{/each}}
</tr>
</table>
</div>
<table cellspacing="0" cellpadding="0" class="layout">
<tr>
<td id="left_col">
{{{body}}}
</td>
</tr>
</table>
</body>
</html>
Для отрисовки таблицы со всеми заявками необходим второй файл- home copy.handlebars.
<h2>{{title}}</h2>
<div style="overflow: auto; width:1300px; height:900px;">
<table id="tickets">
{{#each ticketS}}
<tr id="someRowTickets">
<td>
{{date}}
</td>
<td>
{{name}}
</td>
<td id="status">
{{status}}
</td>
</tr>
{{/each}}
</td>
</table>
</td>
<td>
</div>
<table id="groups">
{{#each groupCount}}
<tr>
<td>
{{maintenance}}
</td>
<td>
{{count}}
</td>
</tr>
{{/each}}
</td>
</table>
</td>
</tr>
</table>
Тут вся магия шаблонизации представления. Данные, переданные в шаблонизатор, как и в первом случае преобразуются в готовый HTML и выдаются при обращении пользователя по определенному маршруту.
По адресу, на котором запущен наш веб-сервер, мы видим следующее:
В целом, такой способ - это не только простая выжимка необходимых данных, но и многочисленные возможности по их представлению и обработке. Основной GLPI продолжает работать на десктопе у хотлайнеров, а наше небольшое и легковесное веб-приложение запущено параллельно, как некий дополнительный сервис.
В дальнейшем, я планирую вторую часть статьи. В ней я продолжу писать о разных способах взаимодействия с GLPI, динамизации данных, визуализации и манипулировании DOM элементами. А так же о других аспектах улучшения представлений и пользовательского опыта веб-приложения.