From 85ee767451bf4e138ffcbe9b9f1b4864db598aef Mon Sep 17 00:00:00 2001 From: Fachba Date: Mon, 27 Oct 2025 10:29:16 +0700 Subject: [PATCH] api history report --- config/index.js | 3 +- controllers/history_value.controller.js | 44 +++- db/history_value.db.js | 313 +++++++++++++++++++++++- routes/history_value.route.js | 12 +- services/history_value.service.js | 84 ++++++- 5 files changed, 444 insertions(+), 12 deletions(-) diff --git a/config/index.js b/config/index.js index 7c699b1..15c4e5a 100644 --- a/config/index.js +++ b/config/index.js @@ -101,7 +101,7 @@ function buildFilterQuery(filterQuery = [], fixedParams = []) { queryParams.push(from); queryParams.push(to); whereConditions.push( - `${f.column} BETWEEN $${queryParams.length - 1} AND $${queryParams.length}` + `CAST(${f.column} AS DATE) BETWEEN $${queryParams.length - 1} AND $${queryParams.length}` ); } } @@ -184,7 +184,6 @@ function buildDateFilter(column, type, dateValue, fixedParams = []) { return { whereDateCondition: whereCondition, whereDateParams: queryParams }; } - /** * Build dynamic UPDATE */ diff --git a/controllers/history_value.controller.js b/controllers/history_value.controller.js index 2a6751a..e4f3665 100644 --- a/controllers/history_value.controller.js +++ b/controllers/history_value.controller.js @@ -2,12 +2,52 @@ const HistoryValue = require('../services/history_value.service'); const { setResponsePaging } = require('../helpers/utils'); class HistoryValueController { - // Get all units + static async getAllHistoryAlarm(req, res) { const queryParams = req.query; const results = await HistoryValue.getAllHistoryAlarm(queryParams); - const response = await setResponsePaging(queryParams, results, 'Unit found'); + const response = await setResponsePaging(queryParams, results, 'Data found'); + + res.status(response.statusCode).json(response); + } + + static async getAllHistoryEvent(req, res) { + const queryParams = req.query; + + const results = await HistoryValue.getAllHistoryEvent(queryParams); + const response = await setResponsePaging(queryParams, results, 'Data found'); + + res.status(response.statusCode).json(response); + } + + static async getHistoryValueReport(req, res) { + const queryParams = req.query; + + const results = await HistoryValue.getHistoryValueReport(queryParams); + const response = await setResponsePaging(queryParams, results, 'Data found'); + + res.status(response.statusCode).json(response); + } + + static async getHistoryValueReportPivot(req, res) { + const queryParams = req.query; + + const results = await HistoryValue.getHistoryValueReportPivot(queryParams); + const response = await setResponsePaging(queryParams, results, 'Data found'); + + response.columns = results.column + + res.status(response.statusCode).json(response); + } + + static async getHistoryValueTrendingPivot(req, res) { + const queryParams = req.query; + + const results = await HistoryValue.getHistoryValueTrendingPivot(queryParams); + const response = await setResponsePaging(queryParams, results, 'Data found'); + + response.columns = results.column res.status(response.statusCode).json(response); } diff --git a/db/history_value.db.js b/db/history_value.db.js index 8f12d7b..3585ad9 100644 --- a/db/history_value.db.js +++ b/db/history_value.db.js @@ -1,6 +1,6 @@ +const { columns } = require("mssql"); const pool = require("../config"); -// Get all tags const getHistoryAlarmDb = async (searchParams = {}) => { let queryParams = []; @@ -61,6 +61,317 @@ const getHistoryAlarmDb = async (searchParams = {}) => { return { data: result.recordset, total }; }; +const getHistoryEventDb = async (searchParams = {}) => { + let queryParams = []; + + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + [ + "b.tag_name", + "a.tagnum" + ], + searchParams.criteria, + queryParams + ); + + if (whereParamOr) queryParams = whereParamOr; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "b.tag_name", param: searchParams.name, type: "string" }, + { column: "b.tag_number", param: searchParams.name, type: "number" }, + ], + queryParams + ); + + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + b.tag_name, + b.tag_number, + b.lim_low_crash, + b.lim_low, + b.lim_high, + b.lim_high_crash, + c.status_color + FROM alarm_history a + LEFT JOIN m_tags b ON a.tagnum = b.tag_number AND b.deleted_at IS NULL + LEFT JOIN m_status c ON a.status = c.status_number AND c.deleted_at IS NULL + WHERE a.datetime IS NOT NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ORDER BY a.datetime DESC + ${searchParams.limit ? `OFFSET $2 * $1 ROWS FETCH NEXT $1 ROWS ONLY` : ''} + `; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +const checkTableNamedDb = async (tableName) => { + const queryText = ` + SELECT * + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_NAME = $1;`; + const result = await pool.query(queryText, [tableName]); + return result.recordset; +}; + +const getHistoryValueReportDb = async (tableName, searchParams = {}) => { + let queryParams = []; + + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + [ + "b.tag_name", + "a.tagnum" + ], + searchParams.criteria, + queryParams + ); + + if (whereParamOr) queryParams = whereParamOr; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "b.tag_name", param: searchParams.name, type: "string" }, + { column: "b.tag_number", param: searchParams.name, type: "number" }, + { column: "a.datetime", param: [searchParams.from, searchParams.to], type: "between" }, + ], + queryParams + ); + + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + b.tag_name, + b.tag_number, + b.lim_low_crash, + b.lim_low, + b.lim_high, + b.lim_high_crash, + c.status_color + FROM ${tableName} a + LEFT JOIN m_tags b ON a.tagnum = b.tag_number AND b.deleted_at IS NULL + LEFT JOIN m_status c ON a.status = c.status_number AND c.deleted_at IS NULL + WHERE a.datetime IS NOT NULL AND b.is_report = 1 + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ORDER BY a.datetime DESC + ${searchParams.limit ? `OFFSET $2 * $1 ROWS FETCH NEXT $1 ROWS ONLY` : ''} + `; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +const getHistoryValueReportPivotDb = async (tableName, searchParams = {}) => { + let from = searchParams.from; + let to = searchParams.to; + const interval = Number(searchParams.interval ?? 10); // menit + const limit = Number(searchParams.limit ?? 10); + const page = Number(searchParams.page ?? 1); + + // --- Normalisasi tanggal + if (from.length === 10) from += ' 00:00:00'; + if (to.length === 10) to += ' 23:59:59'; + + // --- Ambil semua tag yang di-report + const tags = await pool.query(` + SELECT tag_name + FROM m_tags + WHERE is_report = 1 AND deleted_at IS NULL + `); + + if (tags.recordset.length === 0) { + return { data: [], total: 0 }; + } + + const tagNames = tags.recordset.map(r => `[${r.tag_name}]`).join(', '); + const tagNamesColumn = tags.recordset.map(r => `${r.tag_name}`).join(', '); + + // --- Query utama + const queryText = ` + DECLARE + @fromParam DATETIME = '${from}', + @toParam DATETIME = '${to}', + @intervalParam INT = ${interval}; + + ;WITH TimeSeries AS ( + SELECT @fromParam AS waktu + UNION ALL + SELECT DATEADD(MINUTE, @intervalParam, waktu) + FROM TimeSeries + WHERE DATEADD(MINUTE, @intervalParam, waktu) <= @toParam + ), + Averaged AS ( + SELECT + DATEADD(MINUTE, DATEDIFF(MINUTE, 0, CAST(a.datetime AS DATETIME)) / @intervalParam * @intervalParam, 0) AS waktu_group, + b.tag_name, + AVG(a.val) AS avg_val + FROM ${tableName} a + LEFT JOIN m_tags b ON a.tagnum = b.tag_number AND b.deleted_at IS NULL + WHERE a.datetime BETWEEN @fromParam AND @toParam + GROUP BY + DATEADD(MINUTE, DATEDIFF(MINUTE, 0, CAST(a.datetime AS DATETIME)) / @intervalParam * @intervalParam, 0), + b.tag_name + ), + Pivoted AS ( + SELECT + waktu_group, + ${tagNames} + FROM Averaged + PIVOT ( + MAX(avg_val) + FOR tag_name IN (${tagNames}) + ) AS p + ), + FinalResult AS ( + SELECT + CONVERT(VARCHAR(16), ts.waktu, 120) AS datetime, + ${tagNames} + FROM TimeSeries ts + LEFT JOIN Pivoted p ON ts.waktu = p.waktu_group + ) + SELECT + COUNT(*) OVER() AS total_data, + * + FROM FinalResult + ORDER BY datetime + ${searchParams.limit ? `OFFSET ${(page - 1) * limit} ROWS FETCH NEXT ${limit} ROWS ONLY` : ''} + OPTION (MAXRECURSION 0); + `; + + const result = await pool.query(queryText); + + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, column: tagNamesColumn, total }; +}; + +const getHistoryValueTrendingPivotDb = async (tableName, searchParams = {}) => { + let from = searchParams.from; + let to = searchParams.to; + + // pastikan interval berupa number dan ada nilai default + const interval = Number(searchParams.interval) > 0 ? Number(searchParams.interval) : 10; + + // --- Normalisasi tanggal (kalau cuma tanggal tanpa jam) + if (from.length === 10) from += ' 00:00:00'; + if (to.length === 10) to += ' 23:59:59'; + + // --- Ambil semua tag yang di-report + const tags = await pool.query(` + SELECT tag_name + FROM m_tags + WHERE is_report = 1 AND deleted_at IS NULL + `); + + if (tags.recordset.length === 0) { + return { data: [] }; + } + + const tagNames = tags.recordset.map(r => `[${r.tag_name}]`).join(', '); + + const queryText = ` + DECLARE + @fromParam DATETIME = '${from}', + @toParam DATETIME = '${to}', + @intervalParam INT = ${interval}; + + ;WITH TimeSeries AS ( + SELECT @fromParam AS waktu + UNION ALL + SELECT DATEADD(MINUTE, @intervalParam, waktu) + FROM TimeSeries + WHERE DATEADD(MINUTE, @intervalParam, waktu) <= @toParam + ), + Averaged AS ( + SELECT + DATEADD(MINUTE, DATEDIFF(MINUTE, 0, CAST(a.datetime AS DATETIME)) / @intervalParam * @intervalParam, 0) AS waktu_group, + b.tag_name, + AVG(a.val) AS avg_val + FROM ${tableName} a + LEFT JOIN m_tags b ON a.tagnum = b.tag_number AND b.deleted_at IS NULL + WHERE a.datetime BETWEEN @fromParam AND @toParam + GROUP BY + DATEADD(MINUTE, DATEDIFF(MINUTE, 0, CAST(a.datetime AS DATETIME)) / @intervalParam * @intervalParam, 0), + b.tag_name + ), + Pivoted AS ( + SELECT + waktu_group, + ${tagNames} + FROM Averaged + PIVOT ( + MAX(avg_val) + FOR tag_name IN (${tagNames}) + ) AS p + ) + SELECT + CONVERT(VARCHAR(16), ts.waktu, 120) AS waktu, + ${tagNames} + FROM TimeSeries ts + LEFT JOIN Pivoted p ON ts.waktu = p.waktu_group + ORDER BY ts.waktu + OPTION (MAXRECURSION 0); + `; + + const result = await pool.query(queryText); + const rows = result.recordset; + + if (!rows || rows.length === 0) return { data: [] }; + + // --- Bentuk data untuk Nivo Chart + const timeKey = 'waktu'; + const tagList = Object.keys(rows[0]).filter(k => k !== timeKey); + + const nivoData = tagList.map(tag => ({ + id: tag, + data: rows.map(row => ({ + x: row[timeKey], + y: row[tag] !== null ? Number(row[tag]) : null + })) + })); + + return { data: nivoData }; +}; + + module.exports = { getHistoryAlarmDb, + getHistoryEventDb, + checkTableNamedDb, + getHistoryValueReportDb, + getHistoryValueReportPivotDb, + getHistoryValueTrendingPivotDb }; diff --git a/routes/history_value.route.js b/routes/history_value.route.js index 277acce..53771d2 100644 --- a/routes/history_value.route.js +++ b/routes/history_value.route.js @@ -8,10 +8,12 @@ const router = express.Router(); router.route('/alarm') .get(verifyToken.verifyAccessToken, HistoryValueController.getAllHistoryAlarm) router.route('/event') - .get(verifyToken.verifyAccessToken, HistoryValueController.getAllHistoryAlarm) -router.route('/value-table') - .get(verifyToken.verifyAccessToken, HistoryValueController.getAllHistoryAlarm) + .get(verifyToken.verifyAccessToken, HistoryValueController.getAllHistoryEvent) +router.route('/value-report') + .get(verifyToken.verifyAccessToken, HistoryValueController.getHistoryValueReport) +router.route('/value-report-pivot') + .get(verifyToken.verifyAccessToken, HistoryValueController.getHistoryValueReportPivot) router.route('/value-trending') - .get(verifyToken.verifyAccessToken, HistoryValueController.getAllHistoryAlarm) - + .get(verifyToken.verifyAccessToken, HistoryValueController.getHistoryValueTrendingPivot) + module.exports = router; diff --git a/services/history_value.service.js b/services/history_value.service.js index 0fe1216..d3f71d9 100644 --- a/services/history_value.service.js +++ b/services/history_value.service.js @@ -1,8 +1,9 @@ -const { getHistoryAlarmDb } = require('../db/history_value.db'); +const { getHistoryAlarmDb, getHistoryEventDb, checkTableNamedDb, getHistoryValueReportDb, getHistoryValueReportPivotDb, getHistoryValueTrendingPivotDb } = require('../db/history_value.db'); +const { getSubSectionByIdDb } = require('../db/plant_sub_section.db'); const { ErrorHandler } = require('../helpers/error'); class HistoryValue { - // Get all devices + static async getAllHistoryAlarm(param) { try { const results = await getHistoryAlarmDb(param); @@ -15,6 +16,85 @@ class HistoryValue { throw new ErrorHandler(error.statusCode, error.message); } } + + static async getAllHistoryEvent(param) { + try { + const results = await getHistoryEventDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + static async getHistoryValueReport(param) { + try { + + const plantSubSection = await getSubSectionByIdDb(param.plant_sub_section_id); + + if (plantSubSection.length < 1) throw new ErrorHandler(404, 'Plant sub section not found'); + + const tabelExist = await checkTableNamedDb(plantSubSection[0]?.table_name_value); + + if (tabelExist.length < 1) throw new ErrorHandler(404, 'Value not found'); + + const results = await getHistoryValueReportDb(tabelExist[0]?.TABLE_NAME, param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + static async getHistoryValueReportPivot(param) { + try { + + const plantSubSection = await getSubSectionByIdDb(param.plant_sub_section_id); + + if (plantSubSection.length < 1) throw new ErrorHandler(404, 'Plant sub section not found'); + + const tabelExist = await checkTableNamedDb(plantSubSection[0]?.table_name_value); + + if (tabelExist.length < 1) throw new ErrorHandler(404, 'Value not found'); + + const results = await getHistoryValueReportPivotDb(tabelExist[0]?.TABLE_NAME, param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + static async getHistoryValueTrendingPivot(param) { + try { + + const plantSubSection = await getSubSectionByIdDb(param.plant_sub_section_id); + + if (plantSubSection.length < 1) throw new ErrorHandler(404, 'Plant sub section not found'); + + const tabelExist = await checkTableNamedDb(plantSubSection[0]?.table_name_value); + + if (tabelExist.length < 1) throw new ErrorHandler(404, 'Value not found'); + + const results = await getHistoryValueTrendingPivotDb(tabelExist[0]?.TABLE_NAME, param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } } module.exports = HistoryValue;