From 34bbbd0c1a2e80aae8e1f0637ec4bf5dcc589ba3 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 10 Oct 2023 04:14:13 +0300 Subject: [PATCH] unlimited reports rows and pages with filters --- server.js | 376 +++++++++++++++++++++++++++- static/scripts/table-reports.js | 148 +---------- static/styles/main.css | 49 +++- static/templates/reports/index.html | 144 +++++++++-- 4 files changed, 546 insertions(+), 171 deletions(-) diff --git a/server.js b/server.js index 61cb4aa..5ef062b 100644 --- a/server.js +++ b/server.js @@ -885,6 +885,7 @@ async function reports(req, res) { ifDBError: false, Registrars: [], Groups: [], + Count: 0, }; try { const pool = new Pool({ @@ -927,7 +928,7 @@ async function reports(req, res) { FROM geo ) AS g ON a.serial = g.serial AND g.row_num = 1 ORDER BY a.time DESC - LIMIT 100; + LIMIT 14; `; const alarms = await client.query(query, templateData.isAdmin ? [] : [serialValues]); @@ -1104,6 +1105,27 @@ const formattedDate = `${("0" + day).slice(-2)}.${("0" + month).slice(-2)}.${yea numbers: groupedNumbers[groupName], })); + + const countQueryText = ` + SELECT COUNT(*) AS total + FROM ( + SELECT DISTINCT a.evtuuid + FROM alarms AS a + LEFT JOIN registrars AS r ON a.serial = r.serial + LEFT JOIN ( + SELECT *, + ROW_NUMBER() OVER (PARTITION BY serial ORDER BY ABS(EXTRACT(EPOCH FROM (time - NOW())))) AS row_num + FROM geo + ) AS g ON a.serial = g.serial AND g.row_num = 1 + WHERE a.alarmtype = 56 + ${!templateData.isAdmin ? 'AND a.serial = ANY($1)' : ''} + ) AS unique_events + `; + + const countResult = await pool.query(countQueryText, templateData.isAdmin ? [] : [serialValues]); + templateData.Count = countResult.rows[0].total; + + const source = fs.readFileSync( "static/templates/reports/index.html", "utf8" @@ -1127,6 +1149,358 @@ const formattedDate = `${("0" + day).slice(-2)}.${("0" + month).slice(-2)}.${yea } } +app.post('/getreports', async (req, res) => { + if (req.session.userId === undefined) { + return res.redirect("/signin?page=reports"); + } + try { + const pool = new Pool({ + user: DB_User, + host: DB_Host, + database: DB_Name, + password: DB_Password, + port: DB_Port, + }); + + let serialValues = []; + if (req.session.userId != 'admin') { + + const userDevicesQuery = ` + SELECT devices + FROM users + WHERE id = $1 + `; + const userDevicesResult = await pool.query(userDevicesQuery, [req.session.userId]); + + if (userDevicesResult.rows[0].devices.length > 0) { + serialValues = userDevicesResult.rows[0].devices; + } + } + + const { page, timeRangeStart, timeRangeEnd, serials, searchText } = req.body; + + let timeRangeStartCheck = timeRangeStart; + let timeRangeEndCheck = timeRangeEnd; + let serialsCheck = serials; + + console.log(req.body); + + if (!timeRangeStartCheck || !timeRangeEndCheck || serialsCheck.length < 1) { + const minMaxSerialQuery = ` + SELECT + MIN(time) AS min_date, + MAX(time) AS max_date, + ARRAY_AGG(DISTINCT serial) AS unique_serials + FROM + alarms; + `; + + + const minMaxDateResult = await pool.query(minMaxSerialQuery); + + if (!timeRangeStartCheck) { + timeRangeStartCheck = minMaxDateResult.rows[0].min_date; + } + + if (!timeRangeEndCheck) { + timeRangeEndCheck = minMaxDateResult.rows[0].max_date; + } + + if (serialsCheck.length < 1) { + if (req.session.userId != 'admin') { + serialsCheck = serialValues; + } else { + serialsCheck = minMaxDateResult.rows[0].unique_serials; + } + } + + } + + const violationsMapping = { + 0: "усталость", + 1: "водитель пропал", + 2: "разговор по телефону", + 3: "курение за рулём", + 4: "водитель отвлекся", + 5: "выезд с полосы движения", + 6: "!!! лобовое столкновение", + 7: "скорость превышена", + 8: "распознавание номерных знаков", + 9: "!! маленькое расстояние спереди", + 10: "водитель зевает", + 11: "!!! столкновение с пешеходом", + 12: "проходы переполнены", + 13: "!! посадка/высадка вне остановки", + 14: "!! смена полосы с нарушением ПДД", + 15: "! включенный телефон у водителя", + 16: "!!! ремень безопасности", + 17: "проверка не удалась", + 18: "слепые зоны справа", + 19: "!!! заднее столкновение", + 20: "!!! управление без рук", + 21: "!! управление одной рукой", + 22: "очки, блокирующие инфракрасное излучение", + 23: "слепые зоны слева", + 24: "помехи для пассажиров", + 25: "на перекрестке ограничена скорость", + 26: "обнаружен перекресток", + 27: "пешеходы на переходе", + 28: "! неучтивое отношение к пешеходам", + 29: "обнаружен пешеходный переход", + 30: "водитель матерится" + }; + + const idList = Object.entries(violationsMapping) + .filter(([id, violation]) => violation.includes(searchText.toLowerCase())) + .map(([id, violation]) => id); + + console.log(idList); + + const searchConditions = [ + 'a.evtuuid::TEXT ILIKE $4', + 'a.id::TEXT ILIKE $4', + 'r.plate::TEXT ILIKE $4', + 'a.serial::TEXT ILIKE $4', + 'g.latitude::TEXT ILIKE $4', + 'g.longitude::TEXT ILIKE $4', + ]; + + const countQueryText = ` + SELECT COUNT(*) AS total + FROM ( + SELECT DISTINCT a.evtuuid + FROM alarms AS a + LEFT JOIN registrars AS r ON a.serial = r.serial + LEFT JOIN ( + SELECT *, + ROW_NUMBER() OVER (PARTITION BY serial ORDER BY ABS(EXTRACT(EPOCH FROM (time - NOW())))) AS row_num + FROM geo + ) AS g ON a.serial = g.serial AND g.row_num = 1 + WHERE a.alarmtype = 56 + AND a.time >= $1::timestamp + AND a.time <= $2::timestamp + AND a.serial = ANY($3) + ${searchText ? `AND (${idList.length > 0 ? 'st = ANY($5) OR' : ''} (${searchConditions.join(' OR ')}))` : ''} + ) AS unique_events; +`; + + + const countValues = [ + timeRangeStartCheck, + timeRangeEndCheck, + serialsCheck, + ]; + + if (searchText.length > 0) { + countValues.push(`%${searchText}%`); + } + + if (idList.length > 0 && idList.length !== 31) { + countValues.push(idList); + } + + const countResult = await pool.query(countQueryText, countValues); + const totalCount = countResult.rows[0].total; + + const queryConditions = [ + 'a.evtuuid::TEXT ILIKE $6', + 'a.id::TEXT ILIKE $6', + 'r.plate::TEXT ILIKE $6', + 'a.serial::TEXT ILIKE $6', + 'g.latitude::TEXT ILIKE $6', + 'g.longitude::TEXT ILIKE $6', + ]; + + const queryText = ` + SELECT a.evtuuid, a.id, a.cmdno, a.time, a.serial, a.st, r.plate, g.latitude, g.longitude, r.number + FROM ( + SELECT DISTINCT ON (evtuuid) evtuuid, id, cmdno, time, serial, st + FROM alarms + WHERE alarmtype = 56 + AND time >= $1::timestamp + AND time <= $2::timestamp + ORDER BY evtuuid, time DESC + ) AS a + LEFT JOIN registrars AS r ON a.serial = r.serial + LEFT JOIN ( + SELECT *, + ROW_NUMBER() OVER (PARTITION BY serial ORDER BY ABS(EXTRACT(EPOCH FROM (time - NOW())))) AS row_num + FROM geo + ) AS g ON a.serial = g.serial AND g.row_num = 1 + WHERE a.time >= $1::timestamp + AND a.time <= $2::timestamp + AND a.serial = ANY($3) + ${searchText ? `AND (${idList.length > 0 ? 'st = ANY($7) OR' : ''} (${queryConditions.join(' OR ')}))` : ``} + ORDER BY a.time DESC + OFFSET $4 LIMIT $5; + `; + + const values = [ + timeRangeStartCheck, + timeRangeEndCheck, + serialsCheck, + (page - 1) * 14, + 14, + ]; + + if (searchText.length > 0) { + values.push(`%${searchText}%`); + } + + if (idList.length > 0 && idList.length !== 31) { + values.push(idList); + } + + const result = await pool.query(queryText, values); + + function formatDate(date) { + const options = { + year: "2-digit", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }; + + const dateString = date.toISOString().replace("T", " ").slice(0, 19); + + const [datePart, timePart] = dateString.split(' '); + const [year, month, day] = datePart.split('-'); + const [hour, minute, second] = timePart.split(':'); + +const formattedDate = `${("0" + day).slice(-2)}.${("0" + month).slice(-2)}.${year.slice(-2)} ${("0" + hour).slice(-2)}:${("0" + minute).slice(-2)}`; + + return formattedDate; + } + + + const Alarms = result.rows.map((alarm) => { + let type; + switch (alarm.st) { + case "0": + type = "Усталость"; + break; + case "1": + type = "Водитель пропал"; + break; + case "2": + type = "Разговор по телефону"; + break; + case "3": + type = "Курение за рулём"; + break; + case "4": + type = "Водитель отвлекся"; + break; + case "5": + type = "Выезд с полосы движения"; + break; + case "6": + type = "!!! Лобовое столкновение"; + break; + case "7": + type = "Скорость превышена"; + break; + case "8": + type = "Распознавание номерных знаков"; + break; + case "9": + type = "!! Маленькое расстояние спереди"; + break; + case "10": + type = "Водитель зевает"; + break; + case "11": + type = "!!! Столкновение с пешеходом"; + break; + case "12": + type = "Проходы переполнены"; + break; + case "13": + type = "!! Посадка/высадка вне остановки"; + break; + case "14": + type = "!! Смена полосы с нарушением ПДД"; + break; + case "15": + type = "! Включенный телефон у водителя"; + break; + case "16": + type = "!!! Ремень безопасности"; + break; + case "17": + type = "Проверка не удалась"; + break; + case "18": + type = "Слепые зоны справа"; + break; + case "19": + type = "!!! Заднее столкновение"; + break; + case "20": + type = "!!! Управление без рук"; + break; + case "21": + type = "!! Управление одной рукой"; + break; + case "22": + type = "Очки, блокирующие инфракрасное излучение"; + break; + case "23": + type = "Слепые зоны слева"; + break; + case "24": + type = "Помехи для пассажиров"; + break; + case "25": + type = "На перекрестке ограничена скорость"; + break; + case "26": + type = "Обнаружен перекресток"; + break; + case "27": + type = "Пешеходы на переходе"; + break; + case "28": + type = "! Неучтивое отношение к пешеходам"; + break; + case "29": + type = "Обнаружен пешеходный переход"; + break; + case "30": + type = "Водитель матерится"; + break; + default: + type = "Неизвестный тип"; + } + + return { + id: alarm.id, + cmdno: alarm.cmdno, + time: formatDate(alarm.time), + number: alarm.number, + serial: alarm.serial, + st: alarm.st, + type: type, + plate: alarm.plate, + latitude: (alarm.latitude).toFixed(6), + longitude: (alarm.longitude).toFixed(6), + geo: (alarm.latitude).toFixed(6) + "," + (alarm.longitude).toFixed(6), + }; + }) + + res.json({ + total: totalCount, + data: Alarms, + }); + } catch (error) { + console.error('Error handling request:', error); + res.status(500).send('Internal Server Error'); + } +}); + + app.get("/api/devices", async (req, res) => { if (req.session.userId === undefined) { return res.redirect("/signin?page=live"); diff --git a/static/scripts/table-reports.js b/static/scripts/table-reports.js index 31ed24d..857f354 100644 --- a/static/scripts/table-reports.js +++ b/static/scripts/table-reports.js @@ -1,25 +1,11 @@ - -// Получаем высоту таблицы и определяем, сколько строк помещается на странице -let currentPage = 1; -let tableHeight = document.getElementById("table-area").offsetHeight; -let rowHeight = 60; -let rowsPerPage = Math.floor(tableHeight / rowHeight) - 3; -let filteredDevices = [...devices]; -let timeRangeStart = null; -let timeRangeEnd = null; - const createTable = () => { const table = document.getElementById("deviceTable"); const tbody = table.querySelector("tbody"); // Очищаем таблицу tbody.innerHTML = ""; - // Добавляем строки таблицы - const startIndex = (currentPage - 1) * rowsPerPage; - const endIndex = startIndex + rowsPerPage; - const devicesToDisplay = filteredDevices.slice(startIndex, endIndex); - devicesToDisplay.forEach((device) => { + devices.forEach((device) => { const row = document.createElement("tr"); // Добавляем ячейки с данными @@ -57,137 +43,5 @@ const createTable = () => { }); }; -window.addEventListener("resize", function (event) { - tableHeight = document.getElementById("table-area").offsetHeight; - rowHeight = 60; - rowsPerPage = Math.floor(tableHeight / rowHeight) - 3; - createTable(); - createPagination(); -}); - -const createPagination = () => { - const count = document.getElementById("count"); - count.textContent = `Всего результатов: ${filteredDevices.length}`; - - const pagination = document.getElementById("pagination"); - pagination.innerHTML = ""; - - const pageCount = Math.ceil(filteredDevices.length / rowsPerPage); - for (let i = 1; i <= pageCount; i++) { - const pageLink = document.createElement("a"); - pageLink.href = "#"; - if (i === currentPage) { - pageLink.classList.add("active"); - } - pageLink.textContent = i; - pageLink.addEventListener("click", (event) => { - event.preventDefault(); - currentPage = i - 1; - currentPage = i; - createTable(); - createPagination(); - }); - pagination.appendChild(pageLink); - } - - // var currentPageSpan = document.createElement("span"); - // currentPageSpan.textContent = currentPage; - // pagination.appendChild(currentPageSpan); - - // Добавляем кнопки "Next" и "Previous" - - // const prevButton = document.createElement("button"); - // prevButton.innerText = "Previous"; - // prevButton.onclick = () => { - // if (currentPage === 1) return; - // currentPage--; - // createTable(); - // }; - // pagination.appendChild(prevButton); - - // const nextButton = document.createElement("button"); - // nextButton.innerText = "Next"; - // nextButton.onclick = () => { - // if (currentPage === pageCount) return; - // currentPage++; - // createTable(); - // }; - // pagination.appendChild(nextButton); -}; - -const applyFilterAndSearch = () => { - const searchValue = searchInput.value.toLowerCase(); - const groupFilters = Array.from( - document.querySelectorAll('input[type="checkbox"].device-filter:checked') - ).map((checkbox) => checkbox.value); - - filteredDevices = devices.filter((device) => { - const searchString = - `${device.group} ${device.name} ${device.id} ${device.place} ${device.numberTS} ${device.time} ${device.place} ${device.geo} ${device.serial}`.toLowerCase(); - const matchGroup = - groupFilters.length === 0 || groupFilters.includes(device.group) || groupFilters.includes(device.serial); - const matchSearch = !searchValue || searchString.includes(searchValue); - - // Фильтр по временному диапазону - let matchTimeRange = true; - if (timeRangeStart) { - const startTimestamp = new Date(timeRangeStart).getTime(); - const deviceTimestamp = parseTableTime(device.time); // Преобразование времени из таблицы - matchTimeRange = startTimestamp <= deviceTimestamp; - } - if (timeRangeEnd) { - const endTimestamp = new Date(timeRangeEnd).getTime(); - const deviceTimestamp = parseTableTime(device.time); // Преобразование времени из таблицы - matchTimeRange = matchTimeRange && deviceTimestamp <= endTimestamp; - } - - return matchGroup && matchSearch && matchTimeRange; - }); - - currentPage = 1; - createTable(); - createPagination(); -}; - -// Функция для преобразования времени из таблицы в миллисекунды -function parseTableTime(tableTime) { - // Парсинг даты и времени из строки формата "12.03.23 17:33" - const parts = tableTime.split(" "); - const dateParts = parts[0].split("."); - const timeParts = parts[1].split(":"); - const year = 2000 + parseInt(dateParts[2]); - const month = parseInt(dateParts[1]) - 1; // Месяцы в JavaScript начинаются с 0 - const day = parseInt(dateParts[0]); - const hours = parseInt(timeParts[0]); - const minutes = parseInt(timeParts[1]); - return new Date(year, month, day, hours, minutes).getTime(); -} - -const searchInput = document.getElementById("table-search"); -searchInput.addEventListener("input", applyFilterAndSearch); - -const filterCheckboxes = Array.from( - document.querySelectorAll('input[type="checkbox"].device-filter') -); -filterCheckboxes.forEach((checkbox) => { - checkbox.addEventListener("change", applyFilterAndSearch); -}); - -// Обработчик изменения значения в поле начала временного диапазона -const timeRangeStartInput = document.getElementById("timeRangeStart"); -timeRangeStartInput.addEventListener("change", () => { - timeRangeStart = timeRangeStartInput.value; - console.log(timeRangeStart); - applyFilterAndSearch(); -}); - -// Обработчик изменения значения в поле конца временного диапазона -const timeRangeEndInput = document.getElementById("timeRangeEnd"); -timeRangeEndInput.addEventListener("change", () => { - timeRangeEnd = timeRangeEndInput.value; - console.log(timeRangeEnd); - applyFilterAndSearch(); -}); createTable(); -createPagination(); diff --git a/static/styles/main.css b/static/styles/main.css index 4a7366e..9456316 100644 --- a/static/styles/main.css +++ b/static/styles/main.css @@ -20,6 +20,16 @@ body { min-height: 100%; } +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} + header { position: fixed; top: 0; @@ -1687,10 +1697,24 @@ input[type="datetime-local"] { } .time-range input:hover, -.time-range input:focus { +.time-range input:focus, +#pagination input:hover, +#pagination input:focus { border: 1px solid rgba(0, 0, 0, 0.3); } +#pagination input { + width: 20px; + height: 15px; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 10px; + padding: 10px; + outline: none; + color: rgba(0, 0, 0, 0.9); + transition: 0.2s; + text-align: center; +} + .return-name { color: #8086f9 !important; } @@ -1933,6 +1957,29 @@ input[type="datetime-local"] { background-color: rgba(0, 0, 0, 0.1); } +#pagination #left-slider { + padding: 5px; + border-radius: 5px; + cursor: pointer; + margin: 4px; + transition: 0.2s; + float: left; +} + +#pagination #left-slider:hover, +#pagination #right-slider:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +#pagination #right-slider { + padding: 5px; + border-radius: 5px; + cursor: pointer; + margin: 4px; + transition: 0.2s; + float: right; +} + .signals-list .search { float: left; width: calc(305px - 30px); diff --git a/static/templates/reports/index.html b/static/templates/reports/index.html index 805aa97..0d9d063 100644 --- a/static/templates/reports/index.html +++ b/static/templates/reports/index.html @@ -74,17 +74,17 @@
-
+

Организация

    {{#each Groups}} -
  • +
    • {{#each serials}} -
    -
    -
    +
    +
@@ -112,7 +112,7 @@

Список предупреждений

- + @@ -135,12 +135,44 @@ -
+
Всего результатов: {{Count}}
-
+ + + @@ -154,7 +186,7 @@ + +