From 02a256127429aeaf50b1d61bf28f42b08d2ac984 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Sat, 25 Oct 2025 21:31:36 +0700 Subject: [PATCH 1/7] repair: json all user_schedule --- db/user_schedule.db.js | 63 ++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/db/user_schedule.db.js b/db/user_schedule.db.js index dc7b403..d2dd384 100644 --- a/db/user_schedule.db.js +++ b/db/user_schedule.db.js @@ -19,7 +19,11 @@ const getAllUserScheduleDb = async (searchParams = {}) => { const { whereConditions, whereParamAnd } = pool.buildFilterQuery( [ { column: "a.user_id", param: searchParams.user_id, type: "int" }, - { column: "a.user_schedule_id", param: searchParams.user_schedule_id, type: "int" }, + { + column: "a.user_schedule_id", + param: searchParams.user_schedule_id, + type: "int", + }, { column: "a.schedule_id", param: searchParams.schedule_id, type: "int" }, { column: "a.shift_id", param: searchParams.shift_id, type: "int" }, ], @@ -29,24 +33,27 @@ const getAllUserScheduleDb = async (searchParams = {}) => { const queryText = ` SELECT - COUNT(*) OVER() AS total_data, - a.shift_id, + COUNT(*) OVER() AS total_data, + a.*, c.shift_name, c.start_time, c.end_time, - d.user_id, d.user_fullname, d.user_name, d.user_phone FROM user_schedule a LEFT JOIN schedule b ON a.schedule_id = b.schedule_id LEFT JOIN m_shift c ON a.shift_id = c.shift_id - LEFT JOIN m_users d ON a.user_id = d.user_id + LEFT JOIN m_users d ON a.user_id = d.user_id WHERE a.deleted_at IS NULL - ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${ + whereConditions.length > 0 + ? ` AND ${whereConditions.join(" AND ")}` + : "" + } ${whereOrConditions ? ` ${whereOrConditions}` : ""} ORDER BY c.shift_id ASC - ${searchParams.limit ? `OFFSET $2 * $1 ROWS FETCH NEXT $1 ROWS ONLY` : ''} + ${searchParams.limit ? `OFFSET $2 * $1 ROWS FETCH NEXT $1 ROWS ONLY` : ""} `; const result = await pool.query(queryText, queryParams); @@ -54,30 +61,32 @@ const getAllUserScheduleDb = async (searchParams = {}) => { const total = records.length > 0 ? parseInt(records[0].total_data, 10) : 0; - const groupedData = Object.values( - records.reduce((acc, row) => { - if (!acc[row.shift_id]) { - acc[row.shift_id] = { + const groupedShift = {}; + records.forEach((row) => { + if (!groupedShift[row.shift_id]) { + groupedShift[row.shift_id] = { + shift: { shift_id: row.shift_id, shift_name: row.shift_name, start_time: row.start_time, end_time: row.end_time, users: [], - }; - } + }, + }; + } - acc[row.shift_id].users.push({ - user_id: row.user_id, - user_fullname: row.user_fullname, - user_name: row.user_name, - user_phone: row.user_phone, - }); + groupedShift[row.shift_id].shift.users.push({ + user_schedule_id: row.user_schedule_id, + user_id: row.user_id, + user_fullname: row.user_fullname, + user_name: row.user_name, + user_phone: row.user_phone, + }); + }); - return acc; - }, {}) - ); + const data = Object.values(groupedShift); - return { data: groupedData, total }; + return { data, total }; }; const getUserScheduleById = async (userId, shiftId) => { @@ -95,8 +104,6 @@ const getUserScheduleById = async (userId, shiftId) => { return result.recordset || []; }; - - const getUserScheduleByIdDb = async (id) => { const queryText = ` SELECT @@ -119,7 +126,10 @@ const getUserScheduleByIdDb = async (id) => { }; const insertUserScheduleDb = async (store) => { - const { query: queryText, values } = pool.buildDynamicInsert("user_schedule", store); + const { query: queryText, values } = pool.buildDynamicInsert( + "user_schedule", + store + ); const result = await pool.query(queryText, values); const insertedId = result.recordset?.[0]?.inserted_id; @@ -150,7 +160,6 @@ const deleteUserScheduleDb = async (id, deletedBy) => { return true; }; - module.exports = { getAllUserScheduleDb, getUserScheduleByIdDb, -- 2.49.1 From 2fa263e9e4ab39280e9c1b023531471eb40bcb91 Mon Sep 17 00:00:00 2001 From: Fachba Date: Sat, 25 Oct 2025 23:50:49 +0700 Subject: [PATCH 2/7] api history list alarm --- controllers/history_value.controller.js | 16 ++++++ db/history_value.db.js | 66 +++++++++++++++++++++++++ routes/history_value.route.js | 17 +++++++ routes/index.js | 2 + services/history_value.service.js | 20 ++++++++ 5 files changed, 121 insertions(+) create mode 100644 controllers/history_value.controller.js create mode 100644 db/history_value.db.js create mode 100644 routes/history_value.route.js create mode 100644 services/history_value.service.js diff --git a/controllers/history_value.controller.js b/controllers/history_value.controller.js new file mode 100644 index 0000000..2a6751a --- /dev/null +++ b/controllers/history_value.controller.js @@ -0,0 +1,16 @@ +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'); + + res.status(response.statusCode).json(response); + } +} + +module.exports = HistoryValueController; diff --git a/db/history_value.db.js b/db/history_value.db.js new file mode 100644 index 0000000..8f12d7b --- /dev/null +++ b/db/history_value.db.js @@ -0,0 +1,66 @@ +const pool = require("../config"); + +// Get all tags +const getHistoryAlarmDb = 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 }; +}; + +module.exports = { + getHistoryAlarmDb, +}; diff --git a/routes/history_value.route.js b/routes/history_value.route.js new file mode 100644 index 0000000..277acce --- /dev/null +++ b/routes/history_value.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const HistoryValueController = require('../controllers/history_value.controller'); +const verifyToken = require('../middleware/verifyToken'); +const verifyAccess = require('../middleware/verifyAccess'); + +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) +router.route('/value-trending') + .get(verifyToken.verifyAccessToken, HistoryValueController.getAllHistoryAlarm) + +module.exports = router; diff --git a/routes/index.js b/routes/index.js index 8bbd80e..5437936 100644 --- a/routes/index.js +++ b/routes/index.js @@ -12,6 +12,7 @@ const schedule = require("./schedule.route"); const status = require("./status.route"); const unit = require("./unit.route") const UserSchedule = require("./user_schedule.route") +const historyValue = require("./history_value.route") router.use("/auth", auth); router.use("/user", users); @@ -26,6 +27,7 @@ router.use("/schedule", schedule); router.use("/status", status); router.use("/unit", unit); router.use("/user-schedule", UserSchedule) +router.use("/history", historyValue) module.exports = router; diff --git a/services/history_value.service.js b/services/history_value.service.js new file mode 100644 index 0000000..0fe1216 --- /dev/null +++ b/services/history_value.service.js @@ -0,0 +1,20 @@ +const { getHistoryAlarmDb } = require('../db/history_value.db'); +const { ErrorHandler } = require('../helpers/error'); + +class HistoryValue { + // Get all devices + static async getAllHistoryAlarm(param) { + try { + const results = await getHistoryAlarmDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = HistoryValue; -- 2.49.1 From 409e2d3750d137adf0e031e6d56224d35aa466f6 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Sun, 26 Oct 2025 18:26:22 +0700 Subject: [PATCH 3/7] fix: brand api --- controllers/brand.controller.js | 12 ++--- db/brand.db.js | 36 ++------------- db/brand_code.db.js | 30 ------------- db/brand_code_solution.db.js | 13 ------ services/brand.service.js | 80 +++++++++++++++++++++++++-------- validate/brand.schema.js | 24 +++++----- 6 files changed, 83 insertions(+), 112 deletions(-) diff --git a/controllers/brand.controller.js b/controllers/brand.controller.js index 9e1f73e..4820f71 100644 --- a/controllers/brand.controller.js +++ b/controllers/brand.controller.js @@ -4,7 +4,6 @@ const { createFileUploadDb } = require('../db/file_uploads.db'); const { insertBrandSchema, updateBrandSchema, - uploadSolutionSchema } = require('../validate/brand.schema'); class BrandController { @@ -23,6 +22,9 @@ class BrandController { const { id } = req.params; const results = await BrandService.getBrandById(id); + + // console.log('Brand response structure:', JSON.stringify(results, null, 2)); + const response = await setResponse(results, 'Brand found'); res.status(response.statusCode).json(response); @@ -98,14 +100,8 @@ class BrandController { // Soft delete brand by ID static async delete(req, res) { const { id } = req.params; - // Get brand by ID first to get name for deletion - const brand = await BrandService.getBrandById(id); - if (!brand) { - const response = await setResponse([], 'Brand not found', 404); - return res.status(response.statusCode).json(response); - } - const results = await BrandService.deleteBrand(brand.brand_name, req.user.user_id); + const results = await BrandService.deleteBrand(id, req.user.user_id); const response = await setResponse(results, 'Brand deleted successfully'); res.status(response.statusCode).json(response); diff --git a/db/brand.db.js b/db/brand.db.js index 0d985b4..e305bad 100644 --- a/db/brand.db.js +++ b/db/brand.db.js @@ -102,14 +102,14 @@ const updateBrandDb = async (brandName, data) => { return getBrandByNameDb(brandName); }; -// Soft delete brand by name -const deleteBrandDb = async (brandName, deletedBy) => { +// Soft delete brand +const deleteBrandDb = async (id, deletedBy) => { const queryText = ` UPDATE m_brands SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 - WHERE brand_name = $2 AND deleted_at IS NULL + WHERE brand_id = $2 AND deleted_at IS NULL `; - await pool.query(queryText, [deletedBy, brandName]); + await pool.query(queryText, [deletedBy, id]); return true; }; @@ -131,33 +131,6 @@ const checkBrandNameExistsDb = async (brandName, excludeId = null) => { return result.recordset.length > 0; }; -// Get brand with error codes count -const getBrandsWithErrorCodeCountDb = async (searchParams = {}) => { - let queryParams = []; - - const queryText = ` - SELECT - a.brand_id, - a.brand_name, - a.brand_type, - a.brand_manufacture, - a.brand_model, - a.brand_code, - a.is_active, - a.created_at, - COUNT(bc.error_code_id) as error_code_count - FROM m_brands a - LEFT JOIN brand_code bc ON a.brand_id = bc.brand_id AND bc.deleted_at IS NULL - WHERE a.deleted_at IS NULL - GROUP BY - a.brand_id, a.brand_name, a.brand_type, a.brand_manufacture, - a.brand_model, a.brand_code, a.is_active, a.created_at - ORDER BY a.brand_name - `; - - const result = await pool.query(queryText, queryParams); - return result.recordset; -}; module.exports = { getAllBrandsDb, @@ -167,5 +140,4 @@ module.exports = { updateBrandDb, deleteBrandDb, checkBrandNameExistsDb, - getBrandsWithErrorCodeCountDb, }; \ No newline at end of file diff --git a/db/brand_code.db.js b/db/brand_code.db.js index aac38b7..ec64d0c 100644 --- a/db/brand_code.db.js +++ b/db/brand_code.db.js @@ -13,17 +13,6 @@ const getErrorCodesByBrandIdDb = async (brandId) => { return result.recordset; }; -// Get error code by brand ID and error code -const getErrorCodeByBrandIdAndCodeDb = async (brandId, errorCode) => { - const queryText = ` - SELECT - a.* - FROM brand_code a - WHERE a.brand_id = $1 AND a.error_code = $2 AND a.deleted_at IS NULL - `; - const result = await pool.query(queryText, [brandId, errorCode]); - return result.recordset[0]; -}; // Create error code for brand const createErrorCodeDb = async (brandId, data) => { @@ -66,29 +55,10 @@ const deleteErrorCodeDb = async (brandId, errorCode, deletedBy) => { return true; }; -// Check if error code exists for brand -const checkErrorCodeExistsDb = async (brandId, errorCode, excludeId = null) => { - let queryText = ` - SELECT error_code_id - FROM brand_code - WHERE brand_id = $1 AND error_code = $2 AND deleted_at IS NULL - `; - let values = [brandId, errorCode]; - - if (excludeId) { - queryText += ` AND error_code_id != $3`; - values.push(excludeId); - } - - const result = await pool.query(queryText, values); - return result.recordset.length > 0; -}; module.exports = { getErrorCodesByBrandIdDb, - getErrorCodeByBrandIdAndCodeDb, createErrorCodeDb, updateErrorCodeDb, deleteErrorCodeDb, - checkErrorCodeExistsDb, }; \ No newline at end of file diff --git a/db/brand_code_solution.db.js b/db/brand_code_solution.db.js index 92283bd..7c252bb 100644 --- a/db/brand_code_solution.db.js +++ b/db/brand_code_solution.db.js @@ -52,22 +52,9 @@ const deleteSolutionDb = async (solutionId, deletedBy) => { return true; }; -// Get solution by ID -const getSolutionByIdDb = async (solutionId) => { - const queryText = ` - SELECT - a.* - FROM brand_code_solution a - WHERE a.brand_code_solution_id = $1 AND a.deleted_at IS NULL - `; - const result = await pool.query(queryText, [solutionId]); - return result.recordset[0]; -}; - module.exports = { getSolutionsByErrorCodeIdDb, createSolutionDb, updateSolutionDb, deleteSolutionDb, - getSolutionByIdDb, }; \ No newline at end of file diff --git a/services/brand.service.js b/services/brand.service.js index 9bb2884..33be0d4 100644 --- a/services/brand.service.js +++ b/services/brand.service.js @@ -15,7 +15,6 @@ const { createErrorCodeDb, updateErrorCodeDb, deleteErrorCodeDb, - checkErrorCodeExistsDb, } = require('../db/brand_code.db'); // Solution operations @@ -58,15 +57,31 @@ class BrandService { const solutionsWithFiles = await Promise.all( solutions.map(async (solution) => { let fileData = null; + // console.log('Processing solution:', { + // solution_id: solution.brand_code_solution_id, + // path_solution: solution.path_solution, + // type_solution: solution.type_solution + // }); + if (solution.path_solution && solution.type_solution !== 'text') { fileData = await getFileUploadByPathDb(solution.path_solution); + console.log('File data found:', fileData); } - return { + const enhancedSolution = { ...solution, file_upload_name: fileData?.file_upload_name || null, path_document: fileData?.path_document || null }; + + // console.log('Enhanced solution:', { + // solution_id: enhancedSolution.brand_code_solution_id, + // original_path_solution: enhancedSolution.path_solution, + // path_document: enhancedSolution.path_document, + // file_upload_name: enhancedSolution.file_upload_name + // }); + + return enhancedSolution; }) ); @@ -125,7 +140,6 @@ class BrandService { const brandId = createdBrand.brand_id; for (const errorCodeData of data.error_code) { - // Use separate db function for error codes const errorId = await createErrorCodeDb(brandId, { error_code: errorCodeData.error_code, error_code_name: errorCodeData.error_code_name, @@ -141,7 +155,6 @@ class BrandService { // Create solutions for this error code if (errorCodeData.solution && Array.isArray(errorCodeData.solution)) { for (const solutionData of errorCodeData.solution) { - // Use separate db function for solutions await createSolutionDb(errorId, { solution_name: solutionData.solution_name, type_solution: solutionData.type_solution, @@ -161,17 +174,16 @@ class BrandService { } } - // Soft delete brand by name (convert to ID for database operation) - static async deleteBrand(brandName, userId) { + // Soft delete brand by ID + static async deleteBrand(id, userId) { try { - // Get brand by name first to get ID - const brandExist = await getBrandByNameDb(brandName); + const brandExist = await getBrandByIdDb(id); if (!brandExist) { throw new ErrorHandler(404, 'Brand not found'); } - const result = await deleteBrandDb(brandName, userId); + const result = await deleteBrandDb(id, userId); return result; } catch (error) { @@ -185,7 +197,6 @@ class BrandService { const existingBrand = await getBrandByIdDb(id); if (!existingBrand) throw new ErrorHandler(404, 'Brand not found'); - // Check if brand name already exists (excluding current brand) if (data.brand_name && data.brand_name !== existingBrand.brand_name) { const brandExists = await checkBrandNameExistsDb(data.brand_name, id); if (brandExists) { @@ -206,6 +217,7 @@ class BrandService { if (data.error_code && Array.isArray(data.error_code)) { const existingErrorCodes = await getErrorCodesByBrandIdDb(id); + const incomingErrorCodes = data.error_code.map(ec => ec.error_code); // Create/update/delete error codes for (const errorCodeData of data.error_code) { @@ -222,15 +234,41 @@ class BrandService { }); if (errorCodeData.solution && Array.isArray(errorCodeData.solution)) { + const existingSolutions = await getSolutionsByErrorCodeIdDb(existingEC.error_code_id); + const incomingSolutionNames = errorCodeData.solution.map(s => s.solution_name); + + // Update or create solutions for (const solutionData of errorCodeData.solution) { - await createSolutionDb(existingEC.error_code_id, { - solution_name: solutionData.solution_name, - type_solution: solutionData.type_solution, - text_solution: solutionData.text_solution || null, - path_solution: solutionData.path_solution || null, - is_active: solutionData.is_active, - created_by: data.updated_by - }); + const existingSolution = existingSolutions.find(s => s.solution_name === solutionData.solution_name); + + if (existingSolution) { + // Update existing solution + await updateSolutionDb(existingSolution.brand_code_solution_id, { + solution_name: solutionData.solution_name, + type_solution: solutionData.type_solution, + text_solution: solutionData.text_solution || null, + path_solution: solutionData.path_solution || null, + is_active: solutionData.is_active, + updated_by: data.updated_by + }); + } else { + // Create new solution + await createSolutionDb(existingEC.error_code_id, { + solution_name: solutionData.solution_name, + type_solution: solutionData.type_solution, + text_solution: solutionData.text_solution || null, + path_solution: solutionData.path_solution || null, + is_active: solutionData.is_active, + created_by: data.updated_by + }); + } + } + + // Delete solutions that are not in the incoming request + for (const existingSolution of existingSolutions) { + if (!incomingSolutionNames.includes(existingSolution.solution_name)) { + await deleteSolutionDb(existingSolution.brand_code_solution_id, data.updated_by); + } } } } else { @@ -256,6 +294,12 @@ class BrandService { } } } + + for (const existingEC of existingErrorCodes) { + if (!incomingErrorCodes.includes(existingEC.error_code)) { + await deleteErrorCodeDb(id, existingEC.error_code, data.updated_by); + } + } } return await this.getBrandById(id); diff --git a/validate/brand.schema.js b/validate/brand.schema.js index ac6cf98..450972e 100644 --- a/validate/brand.schema.js +++ b/validate/brand.schema.js @@ -5,17 +5,18 @@ const Joi = require("joi"); // ======================== const insertBrandSchema = Joi.object({ brand_name: Joi.string().max(100).required(), - brand_type: Joi.string().max(50).optional(), - brand_manufacture: Joi.string().max(100).optional(), - brand_model: Joi.string().max(100).optional(), + brand_type: Joi.string().max(50).optional().allow(''), + brand_manufacture: Joi.string().max(100).required(), + brand_model: Joi.string().max(100).optional().allow(''), is_active: Joi.boolean().required(), - description: Joi.string().max(255).optional(), + description: Joi.string().max(255).optional().allow(''), error_code: Joi.array().items( Joi.object({ error_code: Joi.string().max(100).required(), error_code_name: Joi.string().max(100).required(), - error_code_description: Joi.string().optional(), + error_code_description: Joi.string().optional().allow(''), is_active: Joi.boolean().required(), + what_action_to_take: Joi.string().optional().allow(''), solution: Joi.array().items( Joi.object({ solution_name: Joi.string().max(100).required(), @@ -40,17 +41,18 @@ const insertBrandSchema = Joi.object({ // Update Brand Validation const updateBrandSchema = Joi.object({ brand_name: Joi.string().max(100).required(), - brand_type: Joi.string().max(50).optional(), - brand_manufacture: Joi.string().max(100).optional(), - brand_model: Joi.string().max(100).optional(), + brand_type: Joi.string().max(50).optional().allow(''), + brand_manufacture: Joi.string().max(100).required(), + brand_model: Joi.string().max(100).optional().allow(''), is_active: Joi.boolean().required(), - description: Joi.string().max(255).optional(), + description: Joi.string().max(255).optional().allow(''), error_code: Joi.array().items( Joi.object({ error_code: Joi.string().max(100).required(), error_code_name: Joi.string().max(100).required(), - error_code_description: Joi.string().optional(), + error_code_description: Joi.string().optional().allow(''), is_active: Joi.boolean().required(), + what_action_to_take: Joi.string().optional().allow(''), solution: Joi.array().items( Joi.object({ solution_name: Joi.string().max(100).required(), @@ -69,7 +71,7 @@ const updateBrandSchema = Joi.object({ }) ).min(1).required() }) - ).optional() + ).optional() }).min(1); module.exports = { -- 2.49.1 From a3b7f795460ac38244be560a9ef14282565918d6 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Sun, 26 Oct 2025 18:26:38 +0700 Subject: [PATCH 4/7] update verify token --- middleware/verifyToken.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/middleware/verifyToken.js b/middleware/verifyToken.js index 020e967..71540fe 100644 --- a/middleware/verifyToken.js +++ b/middleware/verifyToken.js @@ -19,10 +19,15 @@ function verifyAccessToken(req, res, next) { if (!token) { const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer')) { - throw new ErrorHandler(401, 'Access Token is required'); + if (authHeader && authHeader.startsWith('Bearer')) { + token = authHeader.split(' ')[1]; + } else { + token = req.query.token; } - token = authHeader.split(' ')[1]; + } + + if (!token) { + throw new ErrorHandler(401, 'Access Token is required'); } const decoded = JWTService.verifyToken(token); -- 2.49.1 From cd77fda212386a831072067e9b0d65546a5af739 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Sun, 26 Oct 2025 18:26:58 +0700 Subject: [PATCH 5/7] fix: file uploads api --- controllers/file_uploads.controller.js | 115 +++++++++---------------- routes/file_uploads.route.js | 14 ++- 2 files changed, 47 insertions(+), 82 deletions(-) diff --git a/controllers/file_uploads.controller.js b/controllers/file_uploads.controller.js index d6ce4c0..1c40607 100644 --- a/controllers/file_uploads.controller.js +++ b/controllers/file_uploads.controller.js @@ -5,9 +5,6 @@ const { createFileUploadDb, deleteFileUploadByPathDb, } = require("../db/file_uploads.db"); -const { - createSolutionDb, -} = require("../db/brand.db"); const uploadFile = async (req, res) => { try { @@ -23,7 +20,6 @@ const uploadFile = async (req, res) => { const pathDocument = `${folder}/${file.filename}`; - // Insert ke DB via DB layer const fileData = { file_upload_name: file.originalname, path_document: pathDocument, @@ -34,7 +30,11 @@ const uploadFile = async (req, res) => { await createFileUploadDb(fileData); const response = await setResponse( - { name: file.originalname, path: pathDocument }, + { + file_upload_name: file.originalname, + path_document: pathDocument, + path_solution: pathDocument + }, "File berhasil diunggah" ); res.status(200).json(response); @@ -44,27 +44,54 @@ const uploadFile = async (req, res) => { } }; -const getFile = (folder) => async (req, res) => { + + + +const getFileByPath = async (req, res) => { try { - const { filename } = req.params; - const filePath = path.join(__dirname, "../uploads", folder, filename); + const { folder, filename } = req.params; + + // Decode filename from URL encoding + const decodedFilename = decodeURIComponent(filename); + const filePath = path.join(__dirname, "../uploads", folder, decodedFilename); + + console.log('getFileByPath Debug:', { + folder, + originalFilename: filename, + decodedFilename, + filePath + }); if (!fs.existsSync(filePath)) { + console.log('File not found at path:', filePath); + + // try { + // const folderPath = path.join(__dirname, "../uploads", folder); + // const availableFiles = fs.readdirSync(folderPath); + // console.log('Available files in', folderPath, ':', availableFiles); + // } catch (listError) { + // console.log('Could not list files in folder:', listError.message); + // } + const response = await setResponse([], "File tidak ditemukan", 404); return res.status(404).json(response); } res.sendFile(filePath); } catch (error) { + console.error('getFileByPath Error:', error); const response = await setResponse([], error.message, 500); res.status(500).json(response); } }; -const deleteFile = (folder) => async (req, res) => { +const deleteFileByPath = async (req, res) => { try { - const { filename } = req.params; - const filePath = path.join(__dirname, "../uploads", folder, filename); + const { folder, filename } = req.params; + + // Decode filename from URL encoding + const decodedFilename = decodeURIComponent(filename); + const filePath = path.join(__dirname, "../uploads", folder, decodedFilename); if (!fs.existsSync(filePath)) { const response = await setResponse([], "File tidak ditemukan", 404); @@ -74,8 +101,8 @@ const deleteFile = (folder) => async (req, res) => { // Delete physical file fs.unlinkSync(filePath); - const pathDocument = `${folder}/${filename}`; - const deletedBy = req.user?.user_id || null; + const pathDocument = `${folder}/${decodedFilename}`; + const deletedBy = req.user?.user_id || null; await deleteFileUploadByPathDb(pathDocument, deletedBy); const response = await setResponse([], "File berhasil dihapus"); @@ -86,66 +113,8 @@ const deleteFile = (folder) => async (req, res) => { } }; -const uploadSolutionFile = async (req, res) => { - try { - if (!req.file) { - const response = await setResponse([], "Tidak ada file yang diunggah", 400); - return res.status(400).json(response); - } - - const { error_code_id, solution_name } = req.body; - - if (!error_code_id || !solution_name) { - const response = await setResponse([], "error_code_id dan solution_name harus diisi", 400); - return res.status(400).json(response); - } - - const file = req.file; - const ext = path.extname(file.originalname).toLowerCase(); - const typeDoc = ext === ".pdf" ? "PDF" : "IMAGE"; - const folder = typeDoc === "PDF" ? "pdf" : "images"; - const pathDocument = `${folder}/${file.filename}`; - - const fileData = { - file_upload_name: file.originalname, - path_document: pathDocument, - type_document: typeDoc, - createdBy: req.user?.user_id || null, - }; - - await createFileUploadDb(fileData); - - const solutionData = { - solution_name: solution_name, - type_solution: typeDoc.toLowerCase(), - path_solution: pathDocument, - is_active: true, - created_by: req.user?.user_id || null - }; - - const solutionId = await createSolutionDb(error_code_id, solutionData); - - const response = await setResponse( - { - solution_id: solutionId, - solution_name: solution_name, - error_code_id: error_code_id, - file_name: file.originalname, - file_path: pathDocument, - file_type: typeDoc.toLowerCase() - }, - "Solution file berhasil diunggah" - ); - res.status(200).json(response); - } catch (error) { - const response = await setResponse([], error.message, 500); - res.status(500).json(response); - } -}; - module.exports = { uploadFile, - uploadSolutionFile, - getFile, - deleteFile, + getFileByPath, + deleteFileByPath, }; \ No newline at end of file diff --git a/routes/file_uploads.route.js b/routes/file_uploads.route.js index 53f4fbc..1a35fe5 100644 --- a/routes/file_uploads.route.js +++ b/routes/file_uploads.route.js @@ -4,18 +4,14 @@ const verifyToken = require("../middleware/verifyToken"); const verifyAccess = require("../middleware/verifyAccess"); const { uploadFile, - getFile, - deleteFile, + getFileByPath, + deleteFileByPath, } = require("../controllers/file_uploads.controller"); router.post("/", verifyToken.verifyAccessToken, verifyAccess(), upload.single("file"), uploadFile); -router.route("/pdf/:filename") - .get(verifyToken.verifyAccessToken, getFile("pdf")) - .delete(verifyToken.verifyAccessToken, verifyAccess(), deleteFile("pdf")); - -router.route("/images/:filename") - .get(verifyToken.verifyAccessToken, getFile("images")) - .delete(verifyToken.verifyAccessToken, verifyAccess(), deleteFile("images")); +router.route("/:folder/:filename") + .get(verifyToken.verifyAccessToken, getFileByPath) + .delete(verifyToken.verifyAccessToken, verifyAccess(), deleteFileByPath); module.exports = router; \ No newline at end of file -- 2.49.1 From afebb64e47ca6faab1a00005a2af61b4aad0f9a2 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Sun, 26 Oct 2025 18:27:06 +0700 Subject: [PATCH 6/7] dummy pdf --- uploads/pdf/pdf-d0ded6ba-2025-10-26_17-11-14.pdf | Bin 0 -> 7478 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 uploads/pdf/pdf-d0ded6ba-2025-10-26_17-11-14.pdf diff --git a/uploads/pdf/pdf-d0ded6ba-2025-10-26_17-11-14.pdf b/uploads/pdf/pdf-d0ded6ba-2025-10-26_17-11-14.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8da27b526712a5e417279d7dd2fd7e913b45d47e GIT binary patch literal 7478 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 E2LO^uS^xk5 literal 0 HcmV?d00001 -- 2.49.1 From 85ee767451bf4e138ffcbe9b9f1b4864db598aef Mon Sep 17 00:00:00 2001 From: Fachba Date: Mon, 27 Oct 2025 10:29:16 +0700 Subject: [PATCH 7/7] 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; -- 2.49.1