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/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/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/controllers/history_value.controller.js b/controllers/history_value.controller.js new file mode 100644 index 0000000..e4f3665 --- /dev/null +++ b/controllers/history_value.controller.js @@ -0,0 +1,56 @@ +const HistoryValue = require('../services/history_value.service'); +const { setResponsePaging } = require('../helpers/utils'); + +class HistoryValueController { + + static async getAllHistoryAlarm(req, res) { + const queryParams = req.query; + + const results = await HistoryValue.getAllHistoryAlarm(queryParams); + 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); + } +} + +module.exports = HistoryValueController; 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/db/history_value.db.js b/db/history_value.db.js new file mode 100644 index 0000000..3585ad9 --- /dev/null +++ b/db/history_value.db.js @@ -0,0 +1,377 @@ +const { columns } = require("mssql"); +const pool = require("../config"); + +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 }; +}; + +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/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, 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); 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 diff --git a/routes/history_value.route.js b/routes/history_value.route.js new file mode 100644 index 0000000..53771d2 --- /dev/null +++ b/routes/history_value.route.js @@ -0,0 +1,19 @@ +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.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.getHistoryValueTrendingPivot) + +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/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/services/history_value.service.js b/services/history_value.service.js new file mode 100644 index 0000000..d3f71d9 --- /dev/null +++ b/services/history_value.service.js @@ -0,0 +1,100 @@ +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 { + + 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); + } + } + + 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; 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 0000000..8da27b5 Binary files /dev/null and b/uploads/pdf/pdf-d0ded6ba-2025-10-26_17-11-14.pdf differ 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 = {