From 920b24bfd28fdbbe6e4a84b4741df2c758196c3c Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Mon, 1 Dec 2025 10:35:25 +0700 Subject: [PATCH 01/37] repair: brand device connect to sparepart --- controllers/brand.controller.js | 60 +++++++++++---------------------- db/sparepart.db.js | 30 +++++++++++++++++ routes/brand.route.js | 3 +- services/brand.service.js | 26 ++++++++++++-- 4 files changed, 75 insertions(+), 44 deletions(-) diff --git a/controllers/brand.controller.js b/controllers/brand.controller.js index 4820f71..c84a8bd 100644 --- a/controllers/brand.controller.js +++ b/controllers/brand.controller.js @@ -1,6 +1,5 @@ const BrandService = require('../services/brand.service'); const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); -const { createFileUploadDb } = require('../db/file_uploads.db'); const { insertBrandSchema, updateBrandSchema, @@ -46,55 +45,36 @@ class BrandController { return res.status(response.statusCode).json(response); } - // Update brand + // Update brand static async update(req, res) { const { id } = req.params; + // Debug logging untuk lihat request body + console.log('🔍 BE Raw Request Body:', req.body); + console.log('🔍 BE Request Headers:', req.headers); + console.log('🔍 BE Request Method:', req.method); + const { error, value } = await checkValidate(updateBrandSchema, req); + if (error) { + console.log('❌ BE Validation Error:', { + error, + details: error.details?.map(d => ({ + field: d.path.join('.'), + message: d.message, + value: d.context?.value + })), + requestBody: req.body + }); return res.status(400).json(setResponse(error, 'Validation failed', 400)); } - try { - if (req.file) { - const file = req.file; - const ext = require('path').extname(file.originalname).toLowerCase(); - const typeDoc = ext === ".pdf" ? "PDF" : "IMAGE"; - const folder = typeDoc === "PDF" ? "pdf" : "images"; - const pathDocument = `${folder}/${file.filename}`; + value.updated_by = req.user?.user_id || null; - // Insert to file_upload table - const fileData = { - file_upload_name: file.originalname, - createdBy: req.user?.user_id || null, - }; - await createFileUploadDb(fileData); - - if (value.error_code && Array.isArray(value.error_code)) { - for (const errorCode of value.error_code) { - if (errorCode.solution && Array.isArray(errorCode.solution)) { - for (const solution of errorCode.solution) { - if (solution.type_solution !== 'text' && (!solution.path_solution || solution.path_solution === '')) { - solution.path_solution = pathDocument; - solution.type_solution = typeDoc.toLowerCase(); - } - } - } - } - } - } + const results = await BrandService.updateBrandWithFullData(id, value); + const response = await setResponse(results, 'Brand updated successfully'); - value.updated_by = req.user?.user_id || null; - - const results = await BrandService.updateBrandWithFullData(id, value); - const response = await setResponse(results, 'Brand updated successfully'); - - res.status(response.statusCode).json(response); - - } catch (error) { - const response = setResponse([], error.message, error.statusCode || 500); - res.status(response.statusCode).json(response); - } + res.status(response.statusCode).json(response); } // Soft delete brand by ID diff --git a/db/sparepart.db.js b/db/sparepart.db.js index 3dc92b6..c5296b5 100644 --- a/db/sparepart.db.js +++ b/db/sparepart.db.js @@ -147,9 +147,39 @@ const deleteSparepartDb = async (id, deletedBy) => { return true; }; +// Get multiple spareparts by IDs +const getSparepartsByIdsDb = async (sparepartIds) => { + if (!sparepartIds || sparepartIds.length === 0) return []; + + const placeholders = sparepartIds.map((_, index) => `$${index + 1}`).join(', '); + const queryText = ` + SELECT + sparepart_id, + sparepart_name, + sparepart_code, + sparepart_description, + sparepart_model, + sparepart_foto, + sparepart_item_type, + sparepart_qty, + sparepart_unit, + sparepart_merk, + sparepart_stok, + created_at, + updated_at + FROM m_sparepart + WHERE sparepart_id IN (${placeholders}) + AND deleted_at IS NULL + `; + + const result = await pool.query(queryText, sparepartIds); + return result.recordset; +}; + module.exports = { getAllSparepartDb, getSparepartByIdDb, + getSparepartsByIdsDb, checkSparepartNameExistsDb, createSparepartDb, updateSparepartDb, diff --git a/routes/brand.route.js b/routes/brand.route.js index fa71fa0..a467d8b 100644 --- a/routes/brand.route.js +++ b/routes/brand.route.js @@ -2,7 +2,6 @@ const express = require('express'); const BrandController = require('../controllers/brand.controller'); const verifyToken = require('../middleware/verifyToken'); const verifyAccess = require('../middleware/verifyAccess'); -const upload = require('../middleware/uploads'); const router = express.Router(); @@ -12,7 +11,7 @@ router.route('/') router.route('/:id') .get(verifyToken.verifyAccessToken, BrandController.getById) - .put(verifyToken.verifyAccessToken, verifyAccess(), upload.single('file'), BrandController.update) + .put(verifyToken.verifyAccessToken, verifyAccess(), BrandController.update) .delete(verifyToken.verifyAccessToken, verifyAccess(), BrandController.delete); module.exports = router; \ No newline at end of file diff --git a/services/brand.service.js b/services/brand.service.js index 883a62d..84c498d 100644 --- a/services/brand.service.js +++ b/services/brand.service.js @@ -2,7 +2,6 @@ const { getAllBrandsDb, getBrandByIdDb, - getBrandByNameDb, createBrandDb, updateBrandDb, deleteBrandDb, @@ -12,7 +11,6 @@ const { const { insertMultipleBrandSparepartsDb, updateBrandSparepartsDb, - deleteAllBrandSparepartsDb, getSparepartsByBrandIdDb, } = require("../db/brand_sparepart.db"); @@ -24,6 +22,9 @@ const { deleteErrorCodeDb, } = require("../db/brand_code.db"); +// Sparepart operations +const { getSparepartsByIdsDb } = require("../db/sparepart.db"); + // Solution operations const { getSolutionsByErrorCodeIdDb, @@ -236,12 +237,33 @@ class BrandService { } } + // Validate sparepart id + static async validateSparepartIds(sparepartIds) { + if (!sparepartIds || !Array.isArray(sparepartIds) || sparepartIds.length === 0) { + return []; + } + + const existingSpareparts = await getSparepartsByIdsDb(sparepartIds); + + if (existingSpareparts.length !== sparepartIds.length) { + const existingIds = existingSpareparts.map(sp => sp.sparepart_id); + const invalidIds = sparepartIds.filter(id => !existingIds.includes(id)); + throw new ErrorHandler(400, `Invalid sparepart IDs: ${invalidIds.join(', ')}`); + } + + return existingSpareparts; + } + // Update brand static async updateBrandWithFullData(id, data) { try { const existingBrand = await getBrandByIdDb(id); if (!existingBrand) throw new ErrorHandler(404, "Brand not found"); + if (data.spareparts) { + await this.validateSparepartIds(data.spareparts); + } + if (data.brand_name && data.brand_name !== existingBrand.brand_name) { const brandExists = await checkBrandNameExistsDb(data.brand_name, id); if (brandExists) { From 28e99c2a0dc7ce61ad40bc8e373a34e7ee2044b2 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Tue, 2 Dec 2025 10:44:07 +0700 Subject: [PATCH 02/37] add: import , repair: change save local storage to imagekit --- controllers/sparepart.controller.js | 241 +++++++++++++++++++++------- middleware/upload.js | 5 + routes/sparepart.route.js | 13 +- 3 files changed, 202 insertions(+), 57 deletions(-) create mode 100644 middleware/upload.js diff --git a/controllers/sparepart.controller.js b/controllers/sparepart.controller.js index e0cf690..2f5880c 100644 --- a/controllers/sparepart.controller.js +++ b/controllers/sparepart.controller.js @@ -1,8 +1,15 @@ const ExcelJS = require("exceljs"); -const fs = require("fs"); const path = require("path"); +const ImageKit = require("imagekit"); +const imagekit = new ImageKit({ + publicKey: process.env.IMAGEKIT_PUBLIC_KEY, + privateKey: process.env.IMAGEKIT_PRIVATE_KEY, + urlEndpoint: process.env.IMAGEKIT_URL_ENDPOINT, +}); + const SparepartService = require("../services/sparepart.service"); + const { setResponse, setResponsePaging, @@ -38,25 +45,28 @@ class SparepartController { if (error) { return res.status(400).json(setResponse(error, "Validation failed", 400)); } + try { if (req.file) { - const file = req.file; - const ext = require("path").extname(file.originalname).toLowerCase(); - const typeDoc = ext === ".pdf" ? "PDF" : "IMAGE"; - const folder = typeDoc === "PDF" ? "pdf" : "images"; - const pathDocument = `${folder}/${file.filename}`; - value.sparepart_foto = pathDocument; + const upload = await imagekit.upload({ + file: req.file.buffer, + fileName: req.file.originalname, + folder: "/sparepart", + }); + + value.sparepart_foto = upload.url; } + value.userId = req.user.user_id; + const results = await SparepartService.createSparepart(value); - const response = await setResponse( - results, - "Sparepart created successfully" - ); - return res.status(response.statusCode).json(response); + return res + .status(201) + .json(setResponse(results, "Sparepart created successfully")); } catch (err) { - const response = setResponse([], err.message, err.statusCode || 500); - res.status(response.statusCode).json(response); + return res + .status(err.statusCode || 500) + .json(setResponse([], err.message, err.statusCode || 500)); } } @@ -66,25 +76,28 @@ class SparepartController { if (error) { return res.status(400).json(setResponse(error, "Validation failed", 400)); } + try { if (req.file) { - const file = req.file; - const ext = require("path").extname(file.originalname).toLowerCase(); - const typeDoc = ext === ".pdf" ? "PDF" : "IMAGE"; - const folder = typeDoc === "PDF" ? "pdf" : "images"; - const pathDocument = `${folder}/${file.filename}`; - value.sparepart_foto = pathDocument; + const upload = await imagekit.upload({ + file: req.file.buffer, + fileName: req.file.originalname, + folder: "/sparepart", + }); + + value.sparepart_foto = upload.url; } + value.userId = req.user.user_id; + const results = await SparepartService.updateSparepart(id, value); - const response = await setResponse( - results, - "Sparepart updated successfully" - ); - res.status(response.statusCode).json(response); + return res + .status(200) + .json(setResponse(results, "Sparepart updated successfully")); } catch (err) { - const response = setResponse([], err.message, err.statusCode || 500); - res.status(response.statusCode).json(response); + return res + .status(err.statusCode || 500) + .json(setResponse([], err.message, err.statusCode || 500)); } } @@ -113,16 +126,8 @@ class SparepartController { worksheet.addRows( results.data.map((item) => ({ - sparepart_name: item.sparepart_name, - sparepart_code: item.sparepart_code, - sparepart_qty: item.sparepart_qty, - sparepart_merk: item.sparepart_merk, - sparepart_model: item.sparepart_model, - sparepart_unit: item.sparepart_unit, - sparepart_stok: item.sparepart_stok, + ...item, sparepart_foto: "", - sparepart_item_type: item.sparepart_item_type, - created_at: item.created_at, })) ); @@ -132,27 +137,38 @@ class SparepartController { if (!item.sparepart_foto) continue; - let foto = item.sparepart_foto.trim(); + let imageUrl = item.sparepart_foto; - foto = foto.replace(/^images\//, ""); + if (!imageUrl.startsWith("https")) continue; - const fullPath = path.resolve(process.cwd(), "uploads", "images", foto); + let ext = path.extname(imageUrl).toLowerCase().replace(".", ""); - if (fs.existsSync(fullPath)) { - const ext = fullPath.split(".").pop().toLowerCase(); + if (!ext) ext = "jpg"; - const imageId = workbook.addImage({ - filename: fullPath, - extension: ext, - }); - - worksheet.addImage(imageId, { - tl: { col: 7, row: rowNumber - 1 }, - ext: { width: 80, height: 80 }, - }); - - worksheet.getRow(rowNumber).height = 70; + const supported = ["jpg", "jpeg", "png"]; + if (!supported.includes(ext)) { + ext = "png"; } + + let buffer; + try { + const resp = await fetch(imageUrl); + buffer = Buffer.from(await resp.arrayBuffer()); + } catch (e) { + continue; + } + + const imageId = workbook.addImage({ + buffer, + extension: ext === "jpg" ? "jpeg" : ext, + }); + + worksheet.addImage(imageId, { + tl: { col: 7, row: rowNumber - 1 }, + ext: { width: 80, height: 80 }, + }); + + worksheet.getRow(rowNumber).height = 70; } worksheet.getRow(1).eachCell((cell) => { @@ -173,10 +189,124 @@ class SparepartController { return res.send(buffer); } catch (error) { - console.log("Export Excel Error:", error); - return res.status(500).json({ - message: error.message, + return res.status(500).json({ message: error.message }); + } + } + + static async importExcel(req, res) { + try { + + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(req.file.buffer); + + const worksheet = workbook.getWorksheet(1); + + const images = worksheet.getImages(); + const imageMap = {}; + images.forEach((imgObj) => { + const imageId = imgObj.imageId; + const range = imgObj.range; + const row = range.tl.nativeRow + 1; + const image = workbook.getImage(imageId); + imageMap[row] = image; }); + + const spareparts = []; + + worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => { + if (rowNumber === 1) return; + + const [ + sparepart_name, + sparepart_code, + sparepart_description, + sparepart_qty_excel, + sparepart_merk, + sparepart_model, + sparepart_unit, + sparepart_stok_excel, + sparepart_foto_excel, + sparepart_item_type, + ] = row.values.slice(1); + + if (!sparepart_name) return; + + if (!sparepart_code) { + return; + } + + spareparts.push({ + sparepart_name: sparepart_name || "", + sparepart_code: sparepart_code || "", + sparepart_description: sparepart_description || "", + sparepart_qty: Number(sparepart_qty_excel) || 0, + sparepart_merk: sparepart_merk || "", + sparepart_model: sparepart_model || "", + sparepart_unit: sparepart_unit || "", + sparepart_stok: sparepart_stok_excel || "", + sparepart_foto: sparepart_foto_excel || "", + sparepart_item_type: sparepart_item_type || "", + rowNumber, + }); + }); + + if (spareparts.length === 0) { + return res + .status(400) + .json(setResponse([], "Tidak ada data valid untuk diimport", 400)); + } + + const results = []; + + for (const data of spareparts) { + let uploadedUrl = ""; + + try { + const image = imageMap[data.rowNumber]; + if (image) { + const fileName = `sparepart_${Date.now()}_${ + data.sparepart_code + }.jpg`; + const uploadResult = await imagekit.upload({ + file: image.buffer, + fileName: fileName, + folder: "/sparepart", + }); + + uploadedUrl = uploadResult.url; + } + } catch (err) { + err; + } + + data.sparepart_foto = uploadedUrl || ""; + + const { rowNumber, ...dbData } = data; + + const created = await SparepartService.createSparepart(dbData); + + if (created && created[0]) { + results.push({ + sparepart_id: created[0].sparepart_id, + sparepart_name: created[0].sparepart_name, + sparepart_code: created[0].sparepart_code, + sparepart_description: created[0].sparepart_description, + sparepart_qty: created[0].sparepart_qty, + sparepart_merk: created[0].sparepart_merk, + sparepart_model: created[0].sparepart_model, + sparepart_unit: created[0].sparepart_unit, + sparepart_stok: created[0].sparepart_stok, + sparepart_foto: created[0].sparepart_foto, + sparepart_item_type: created[0].sparepart_item_type, + }); + } + } + + return res.json( + setResponse(results, `${results.length} Sparepart berhasil diimport`) + ); + } catch (error) { + return res.status(500).json({ message: error.message }); } } @@ -193,4 +323,5 @@ class SparepartController { res.status(response.statusCode).json(response); } } + module.exports = SparepartController; diff --git a/middleware/upload.js b/middleware/upload.js new file mode 100644 index 0000000..1b21e11 --- /dev/null +++ b/middleware/upload.js @@ -0,0 +1,5 @@ +const multer = require("multer"); + +const storage = multer.memoryStorage(); + +module.exports = multer({ storage }); diff --git a/routes/sparepart.route.js b/routes/sparepart.route.js index c151538..9781a01 100644 --- a/routes/sparepart.route.js +++ b/routes/sparepart.route.js @@ -2,7 +2,8 @@ const express = require("express"); const SparepartController = require("../controllers/sparepart.controller"); const verifyToken = require("../middleware/verifyToken"); const verifyAccess = require("../middleware/verifyAccess"); -const upload = require("../middleware/uploads"); +const upload = require("../middleware/upload"); + const router = express.Router(); router.get( @@ -11,6 +12,14 @@ router.get( SparepartController.exportExcel ); +router.post( + "/import", + verifyToken.verifyAccessToken, + verifyAccess(), + upload.single("file"), + SparepartController.importExcel +); + router .route("/") .get(verifyToken.verifyAccessToken, SparepartController.getAll) @@ -36,4 +45,4 @@ router SparepartController.delete ); -module.exports = router; +module.exports = router; \ No newline at end of file From 097030029f6f355dd35292d66a1555bc88f7f011 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Tue, 2 Dec 2025 10:45:40 +0700 Subject: [PATCH 03/37] env imagekit --- .env.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env.example b/.env.example index 8368363..cfb3f65 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,11 @@ SECRET=secret # JWT refresh secret REFRESH_SECRET=refreshsecret +# IMAGEKIT +IMAGEKIT_URL_ENDPOINT=https://ik.imagekit.io/j0hxk7x3p +IMAGEKIT_PUBLIC_KEY=public_iMPQFBnXmdQy73TTB9w4SMQO4Jk= +IMAGEKIT_PRIVATE_KEY=private_vhO/jXHnEoaVYptOHIuZDPMbxIA= + # mail server settings # SMTP_FROM=youremail # SMTP_USER=youremail From feff905d8f02e35d6028e7decbb38c794cfa285a Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 2 Dec 2025 14:45:29 +0700 Subject: [PATCH 04/37] repair: sparepart per error code --- db/brand_sparepart.db.js | 222 ++++++++++++++++++++++++-------------- services/brand.service.js | 76 +++++-------- validate/brand.schema.js | 4 +- 3 files changed, 169 insertions(+), 133 deletions(-) diff --git a/db/brand_sparepart.db.js b/db/brand_sparepart.db.js index 850879d..3cc37f1 100644 --- a/db/brand_sparepart.db.js +++ b/db/brand_sparepart.db.js @@ -1,9 +1,128 @@ const pool = require("../config"); -// Get spareparts by brand_id -const getSparepartsByBrandIdDb = async (brandId) => { +// Get spareparts by error_code_id +const getSparepartsByErrorCodeIdDb = async (errorCodeId) => { const queryText = ` SELECT + s.sparepart_id, + s.sparepart_name, + s.sparepart_code, + s.sparepart_description, + s.sparepart_model, + s.sparepart_foto, + s.sparepart_item_type, + s.sparepart_qty, + s.sparepart_unit, + s.sparepart_merk, + s.sparepart_stok, + bs.created_at, + bs.created_by + FROM brand_spareparts bs + JOIN m_sparepart s ON bs.sparepart_id = s.sparepart_id + WHERE bs.error_code_id = $1 + AND s.deleted_at IS NULL + ORDER BY s.sparepart_name + `; + const result = await pool.query(queryText, [errorCodeId]); + return result.recordset; +}; + +// Get error codes by sparepart_id +const getErrorCodesBySparepartIdDb = async (sparepartId) => { + const queryText = ` + SELECT + ec.error_code_id, + ec.error_code, + ec.error_code_name, + ec.error_code_description, + ec.error_code_color, + ec.path_icon, + ec.is_active, + ec.created_at, + ec.updated_at + FROM brand_spareparts bs + JOIN m_error_codes ec ON bs.error_code_id = ec.error_code_id + WHERE bs.sparepart_id = $1 + AND ec.deleted_at IS NULL + ORDER BY ec.error_code + `; + const result = await pool.query(queryText, [sparepartId]); + return result.recordset; +}; + +// Insert error_code-spareparts relationship +const insertErrorCodeSparepartDb = async (errorCodeId, sparepartId, createdBy) => { + const queryText = ` + INSERT INTO brand_spareparts (error_code_id, sparepart_id, created_by, created_at) + VALUES ($1, $2, $3, CURRENT_TIMESTAMP) + `; + const result = await pool.query(queryText, [errorCodeId, sparepartId, createdBy]); + return result.recordset; +}; + +// Insert multiple error_code-spareparts relationships +const insertMultipleErrorCodeSparepartsDb = async (errorCodeId, sparepartIds, createdBy) => { + if (!sparepartIds || sparepartIds.length === 0) return []; + + const values = sparepartIds.map((_, index) => `($1, $${index + 2}, $${sparepartIds.length + 2}, CURRENT_TIMESTAMP)`).join(', '); + const queryText = ` + INSERT INTO brand_spareparts (error_code_id, sparepart_id, created_by, created_at) + VALUES ${values} + `; + const params = [errorCodeId, ...sparepartIds, createdBy]; + const result = await pool.query(queryText, params); + return result.recordset; +}; + +// Delete specific error_code-sparepart relationship +const deleteErrorCodeSparepartDb = async (errorCodeId, sparepartId) => { + const queryText = ` + DELETE FROM brand_spareparts + WHERE error_code_id = $1 AND sparepart_id = $2 + `; + const result = await pool.query(queryText, [errorCodeId, sparepartId]); + return result.rowsAffected > 0; +}; + +// Delete all spareparts for an error_code +const deleteAllErrorCodeSparepartsDb = async (errorCodeId) => { + const queryText = ` + DELETE FROM brand_spareparts + WHERE error_code_id = $1 + `; + const result = await pool.query(queryText, [errorCodeId]); + return result.rowsAffected > 0; +}; + +// Update error_code-spareparts (replace all) +const updateErrorCodeSparepartsDb = async (errorCodeId, sparepartIds, updatedBy) => { + // Delete existing relationships + await deleteAllErrorCodeSparepartsDb(errorCodeId); + + // Insert new relationships + if (sparepartIds && sparepartIds.length > 0) { + return await insertMultipleErrorCodeSparepartsDb(errorCodeId, sparepartIds, updatedBy); + } + + return true; +}; + +// Check if error_code-sparepart relationship exists +const checkErrorCodeSparepartExistsDb = async (errorCodeId, sparepartId) => { + const queryText = ` + SELECT 1 + FROM brand_spareparts + WHERE error_code_id = $1 AND sparepart_id = $2 + `; + const result = await pool.query(queryText, [errorCodeId, sparepartId]); + return result.recordset.length > 0; +}; + +// Legacy functions for backward compatibility (deprecated) +// Get spareparts by brand_id (now using error_code_id mapping via error codes) +const getSparepartsByBrandIdDb = async (brandId) => { + const queryText = ` + SELECT DISTINCT s.sparepart_id, s.sparepart_name, s.sparepart_code, @@ -19,18 +138,20 @@ const getSparepartsByBrandIdDb = async (brandId) => { s.updated_at FROM brand_spareparts bs JOIN m_sparepart s ON bs.sparepart_id = s.sparepart_id - WHERE bs.brand_id = $1 + JOIN m_error_codes ec ON bs.error_code_id = ec.error_code_id + WHERE ec.brand_id = $1 AND s.deleted_at IS NULL + AND ec.deleted_at IS NULL ORDER BY s.sparepart_name `; const result = await pool.query(queryText, [brandId]); return result.recordset; }; -// Get brands by sparepart_id +// Get brands by sparepart_id (now using error_code_id mapping) const getBrandsBySparepartIdDb = async (sparepartId) => { const queryText = ` - SELECT + SELECT DISTINCT b.brand_id, b.brand_name, b.brand_type, @@ -41,8 +162,12 @@ const getBrandsBySparepartIdDb = async (sparepartId) => { b.created_at, b.updated_at FROM brand_spareparts bs - JOIN m_brands b ON bs.brand_id = b.brand_id + JOIN m_sparepart s ON bs.sparepart_id = s.sparepart_id + JOIN m_error_codes ec ON bs.error_code_id = ec.error_code_id + JOIN m_brands b ON ec.brand_id = b.brand_id WHERE bs.sparepart_id = $1 + AND s.deleted_at IS NULL + AND ec.deleted_at IS NULL AND b.deleted_at IS NULL ORDER BY b.brand_name `; @@ -50,81 +175,16 @@ const getBrandsBySparepartIdDb = async (sparepartId) => { return result.recordset; }; -// Insert brand-spareparts relationship -const insertBrandSparepartDb = async (brandId, sparepartId, createdBy) => { - const queryText = ` - INSERT INTO brand_spareparts (brand_id, sparepart_id, created_by, created_at) - VALUES ($1, $2, $3, CURRENT_TIMESTAMP) - `; - const result = await pool.query(queryText, [brandId, sparepartId, createdBy]); - return result.recordset; -}; - -// Insert multiple brand-spareparts relationships -const insertMultipleBrandSparepartsDb = async (brandId, sparepartIds, createdBy) => { - if (!sparepartIds || sparepartIds.length === 0) return []; - - const values = sparepartIds.map((_, index) => `($1, $${index + 2}, $${sparepartIds.length + 2}, CURRENT_TIMESTAMP)`).join(', '); - const queryText = ` - INSERT INTO brand_spareparts (brand_id, sparepart_id, created_by, created_at) - VALUES ${values} - `; - const params = [brandId, ...sparepartIds, createdBy]; - const result = await pool.query(queryText, params); - return result.recordset; -}; - -// Delete specific brand-sparepart relationship -const deleteBrandSparepartDb = async (brandId, sparepartId) => { - const queryText = ` - DELETE FROM brand_spareparts - WHERE brand_id = $1 AND sparepart_id = $2 - `; - const result = await pool.query(queryText, [brandId, sparepartId]); - return result.rowsAffected > 0; -}; - -// Delete all spareparts for a brand -const deleteAllBrandSparepartsDb = async (brandId) => { - const queryText = ` - DELETE FROM brand_spareparts - WHERE brand_id = $1 - `; - const result = await pool.query(queryText, [brandId]); - return result.rowsAffected > 0; -}; - -// Update brand-spareparts (replace all) -const updateBrandSparepartsDb = async (brandId, sparepartIds, updatedBy) => { - // Delete existing relationships - await deleteAllBrandSparepartsDb(brandId); - - // Insert new relationships - if (sparepartIds && sparepartIds.length > 0) { - return await insertMultipleBrandSparepartsDb(brandId, sparepartIds, updatedBy); - } - - return true; -}; - -// Check if brand-sparepart relationship exists -const checkBrandSparepartExistsDb = async (brandId, sparepartId) => { - const queryText = ` - SELECT 1 - FROM brand_spareparts - WHERE brand_id = $1 AND sparepart_id = $2 - `; - const result = await pool.query(queryText, [brandId, sparepartId]); - return result.recordset.length > 0; -}; +// Deprecated functions removed - table structure changed to use error_code_id instead of brand_id module.exports = { - getSparepartsByBrandIdDb, - getBrandsBySparepartIdDb, - insertBrandSparepartDb, - insertMultipleBrandSparepartsDb, - deleteBrandSparepartDb, - deleteAllBrandSparepartsDb, - updateBrandSparepartsDb, - checkBrandSparepartExistsDb, + // New functions using error_code_id + getSparepartsByErrorCodeIdDb, + getErrorCodesBySparepartIdDb, + insertErrorCodeSparepartDb, + insertMultipleErrorCodeSparepartsDb, + deleteErrorCodeSparepartDb, + deleteAllErrorCodeSparepartsDb, + updateErrorCodeSparepartsDb, + checkErrorCodeSparepartExistsDb, }; \ No newline at end of file diff --git a/services/brand.service.js b/services/brand.service.js index 84c498d..867f55a 100644 --- a/services/brand.service.js +++ b/services/brand.service.js @@ -9,9 +9,9 @@ const { } = require("../db/brand.db"); const { - insertMultipleBrandSparepartsDb, - updateBrandSparepartsDb, - getSparepartsByBrandIdDb, + insertMultipleErrorCodeSparepartsDb, + updateErrorCodeSparepartsDb, + getSparepartsByErrorCodeIdDb, } = require("../db/brand_sparepart.db"); // Error code operations @@ -41,20 +41,10 @@ class BrandService { try { const results = await getAllBrandsDb(param); - // Add spareparts data for each brand - const brandsWithSpareparts = await Promise.all( - results.data.map(async (brand) => { - const spareparts = await getSparepartsByBrandIdDb(brand.brand_id); - return { - ...brand, - spareparts: spareparts - }; - }) - ); - + // Return brands data - spareparts are now associated with error codes, not brands return { ...results, - data: brandsWithSpareparts + data: results.data }; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); @@ -67,17 +57,17 @@ class BrandService { const brand = await getBrandByIdDb(id); if (!brand) throw new ErrorHandler(404, "Brand not found"); - // Get spareparts for this brand - const spareparts = await getSparepartsByBrandIdDb(brand.brand_id); - const errorCodes = await getErrorCodesByBrandIdDb(brand.brand_id); - const errorCodesWithSolutions = await Promise.all( + const errorCodesWithSolutionsAndSpareparts = await Promise.all( errorCodes.map(async (errorCode) => { const solutions = await getSolutionsByErrorCodeIdDb( errorCode.error_code_id ); + // Get spareparts for this error code + const errorCodeSpareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); + const solutionsWithFiles = await Promise.all( solutions.map(async (solution) => { let fileData = null; @@ -112,14 +102,14 @@ class BrandService { return { ...errorCode, solution: solutionsWithFiles, + spareparts: errorCodeSpareparts, }; }) ); return { ...brand, - spareparts: spareparts, - error_code: errorCodesWithSolutions, + error_code: errorCodesWithSolutionsAndSpareparts, }; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); @@ -179,10 +169,6 @@ class BrandService { const brandId = createdBrand.brand_id; - if (data.spareparts && Array.isArray(data.spareparts) && data.spareparts.length > 0) { - await insertMultipleBrandSparepartsDb(brandId, data.spareparts, data.created_by); - } - for (const errorCodeData of data.error_code) { const errorId = await createErrorCodeDb(brandId, { error_code: errorCodeData.error_code, @@ -198,6 +184,11 @@ class BrandService { throw new Error("Failed to create error code"); } + // Create sparepart relationships for this error code + if (errorCodeData.spareparts && Array.isArray(errorCodeData.spareparts)) { + await insertMultipleErrorCodeSparepartsDb(errorId, errorCodeData.spareparts, data.created_by); + } + // Create solutions for this error code if (errorCodeData.solution && Array.isArray(errorCodeData.solution)) { for (const solutionData of errorCodeData.solution) { @@ -237,33 +228,14 @@ class BrandService { } } - // Validate sparepart id - static async validateSparepartIds(sparepartIds) { - if (!sparepartIds || !Array.isArray(sparepartIds) || sparepartIds.length === 0) { - return []; - } - - const existingSpareparts = await getSparepartsByIdsDb(sparepartIds); - - if (existingSpareparts.length !== sparepartIds.length) { - const existingIds = existingSpareparts.map(sp => sp.sparepart_id); - const invalidIds = sparepartIds.filter(id => !existingIds.includes(id)); - throw new ErrorHandler(400, `Invalid sparepart IDs: ${invalidIds.join(', ')}`); - } - - return existingSpareparts; - } - + // Update brand static async updateBrandWithFullData(id, data) { try { const existingBrand = await getBrandByIdDb(id); if (!existingBrand) throw new ErrorHandler(404, "Brand not found"); - if (data.spareparts) { - await this.validateSparepartIds(data.spareparts); - } - + if (data.brand_name && data.brand_name !== existingBrand.brand_name) { const brandExists = await checkBrandNameExistsDb(data.brand_name, id); if (brandExists) { @@ -282,10 +254,6 @@ class BrandService { await updateBrandDb(existingBrand.brand_name, brandData); - if (data.spareparts !== undefined) { - await updateBrandSparepartsDb(existingBrand.brand_id, data.spareparts || [], data.updated_by); - } - if (data.error_code && Array.isArray(data.error_code)) { const existingErrorCodes = await getErrorCodesByBrandIdDb(id); const incomingErrorCodes = data.error_code.map((ec) => ec.error_code); @@ -312,6 +280,10 @@ class BrandService { } ); + if (errorCodeData.spareparts && Array.isArray(errorCodeData.spareparts)) { + await updateErrorCodeSparepartsDb(existingEC.error_code_id, errorCodeData.spareparts, data.updated_by); + } + if ( errorCodeData.solution && Array.isArray(errorCodeData.solution) @@ -380,6 +352,10 @@ class BrandService { created_by: data.updated_by, }); + if (errorCodeData.spareparts && Array.isArray(errorCodeData.spareparts)) { + await insertMultipleErrorCodeSparepartsDb(errorId, errorCodeData.spareparts, data.updated_by); + } + if ( errorCodeData.solution && Array.isArray(errorCodeData.solution) diff --git a/validate/brand.schema.js b/validate/brand.schema.js index 5d9d30f..57fe595 100644 --- a/validate/brand.schema.js +++ b/validate/brand.schema.js @@ -10,7 +10,6 @@ const insertBrandSchema = Joi.object({ brand_model: Joi.string().max(100).optional().allow(""), is_active: Joi.boolean().required(), description: Joi.string().max(255).optional().allow(""), - spareparts: Joi.array().items(Joi.number().integer()).optional(), // Array of sparepart_id error_code: Joi.array() .items( Joi.object({ @@ -21,6 +20,7 @@ const insertBrandSchema = Joi.object({ path_icon: Joi.string().optional().allow(""), is_active: Joi.boolean().required(), what_action_to_take: Joi.string().optional().allow(""), + spareparts: Joi.array().items(Joi.number().integer()).optional(), solution: Joi.array() .items( Joi.object({ @@ -57,7 +57,6 @@ const updateBrandSchema = Joi.object({ brand_model: Joi.string().max(100).optional().allow(""), is_active: Joi.boolean().required(), description: Joi.string().max(255).optional().allow(""), - spareparts: Joi.array().items(Joi.number().integer()).optional(), // Array of sparepart_id error_code: Joi.array() .items( Joi.object({ @@ -68,6 +67,7 @@ const updateBrandSchema = Joi.object({ path_icon: Joi.string().optional().allow(""), is_active: Joi.boolean().required(), what_action_to_take: Joi.string().optional().allow(""), + spareparts: Joi.array().items(Joi.number().integer()).optional(), solution: Joi.array() .items( Joi.object({ From f797685a4f180fcfe268287be97a9f7fdfaaf868 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 2 Dec 2025 15:22:20 +0700 Subject: [PATCH 05/37] add: error_code api --- controllers/error_code.controller.js | 105 ++++++++++ db/brand_code.db.js | 66 ++++++ routes/error_code.route.js | 19 ++ routes/index.js | 2 + services/brand.service.js | 57 +----- services/error_code.service.js | 294 +++++++++++++++++++++++++++ 6 files changed, 488 insertions(+), 55 deletions(-) create mode 100644 controllers/error_code.controller.js create mode 100644 routes/error_code.route.js create mode 100644 services/error_code.service.js diff --git a/controllers/error_code.controller.js b/controllers/error_code.controller.js new file mode 100644 index 0000000..07ff945 --- /dev/null +++ b/controllers/error_code.controller.js @@ -0,0 +1,105 @@ +const ErrorCodeService = require('../services/error_code.service'); +const { setResponse, setResponsePaging } = require('../helpers/utils'); +const { ErrorHandler } = require('../helpers/error'); + +class ErrorCodeController { + // Get all error codes with pagination and search + static async getAll(req, res) { + try { + const queryParams = req.query; + const results = await ErrorCodeService.getAllErrorCodes(queryParams); + const response = await setResponsePaging(queryParams, results, 'Error codes found'); + res.status(response.statusCode).json(response); + } catch (error) { + const response = setResponse(error.message, error.statusCode || 500); + res.status(response.statusCode).json(response); + } + } + + // Get error code by ID + static async getById(req, res) { + try { + const { id } = req.params; + const result = await ErrorCodeService.getErrorCodeById(id); + const response = setResponse(result, 200, 'Error code found'); + res.status(response.statusCode).json(response); + } catch (error) { + const response = setResponse(error.message, error.statusCode || 500); + res.status(response.statusCode).json(response); + } + } + + // Get error codes by brand ID with pagination and search + static async getByBrandId(req, res) { + try { + const { brandId } = req.params; + const queryParams = { + ...req.query, + brand_id: brandId + }; + const results = await ErrorCodeService.getAllErrorCodes(queryParams); + const response = await setResponsePaging(queryParams, results, 'Error codes found'); + res.status(response.statusCode).json(response); + } catch (error) { + const response = setResponse(error.message, error.statusCode || 500); + res.status(response.statusCode).json(response); + } + } + + // Create error code with solutions and spareparts + static async create(req, res) { + try { + const { brandId } = req.params; + const createdBy = req.user?.user_id || null; + + const data = { + ...req.body, + created_by: createdBy + }; + + const result = await ErrorCodeService.createErrorCodeWithFullData(brandId, data); + const response = setResponse(result, 201, 'Error code created successfully'); + res.status(response.statusCode).json(response); + } catch (error) { + const response = setResponse(error.message, error.statusCode || 500); + res.status(response.statusCode).json(response); + } + } + + // Update error code with solutions and spareparts + static async update(req, res) { + try { + const { brandId, errorCode } = req.params; + const updatedBy = req.user?.user_id || null; + + const data = { + ...req.body, + updated_by: updatedBy + }; + + const result = await ErrorCodeService.updateErrorCodeWithFullData(brandId, errorCode, data); + const response = setResponse(result, 200, 'Error code updated successfully'); + res.status(response.statusCode).json(response); + } catch (error) { + const response = setResponse(error.message, error.statusCode || 500); + res.status(response.statusCode).json(response); + } + } + + // Soft delete error code + static async delete(req, res) { + try { + const { brandId, errorCode } = req.params; + const deletedBy = req.user?.user_id || null; + + const result = await ErrorCodeService.deleteErrorCode(brandId, errorCode, deletedBy); + const response = setResponse(result, 200, 'Error code deleted successfully'); + res.status(response.statusCode).json(response); + } catch (error) { + const response = setResponse(error.message, error.statusCode || 500); + res.status(response.statusCode).json(response); + } + } +} + +module.exports = ErrorCodeController; \ No newline at end of file diff --git a/db/brand_code.db.js b/db/brand_code.db.js index 4d8925a..1a0e3e3 100644 --- a/db/brand_code.db.js +++ b/db/brand_code.db.js @@ -64,10 +64,76 @@ const getErrorCodeByIdDb = async (error_code_id) => { return result.recordset[0]; }; +const getErrorCodeByBrandAndCodeDb = 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]; +}; + +// Get all error codes with pagination and search +const getAllErrorCodesDb = async (searchParams = {}) => { + let queryParams = []; + + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // Search across multiple columns + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.error_code", "a.error_code_name", "a.error_code_description"], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ? whereParamOr : queryParams; + + // Filter conditions + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.is_active", param: searchParams.status, type: "string" }, + { column: "a.brand_id", param: searchParams.brand_id, type: "number" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + b.brand_name, + b.brand_type, + b.brand_manufacture, + b.brand_model + FROM brand_code a + LEFT JOIN m_brands b ON a.brand_id = b.brand_id + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} + ORDER BY a.error_code_id 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 = { getErrorCodesByBrandIdDb, getErrorCodeByIdDb, + getErrorCodeByBrandAndCodeDb, createErrorCodeDb, updateErrorCodeDb, deleteErrorCodeDb, + getAllErrorCodesDb, }; \ No newline at end of file diff --git a/routes/error_code.route.js b/routes/error_code.route.js new file mode 100644 index 0000000..c7ba80c --- /dev/null +++ b/routes/error_code.route.js @@ -0,0 +1,19 @@ +const express = require('express'); +const ErrorCodeController = require('../controllers/error_code.controller'); +const verifyToken = require('../middleware/verifyToken'); +const verifyAccess = require('../middleware/verifyAccess'); + +const router = express.Router(); + +router.route('/brand/:brandId') + .get(verifyToken.verifyAccessToken, ErrorCodeController.getByBrandId) + .post(verifyToken.verifyAccessToken, verifyAccess(), ErrorCodeController.create); + +router.route('/brand/:brandId/:errorCode') + .put(verifyToken.verifyAccessToken, verifyAccess(), ErrorCodeController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), ErrorCodeController.delete); + +router.route('/:id') + .get(verifyToken.verifyAccessToken, ErrorCodeController.getById); + +module.exports = router; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index ee0e37e..e6121bf 100644 --- a/routes/index.js +++ b/routes/index.js @@ -18,6 +18,7 @@ const notificationError = require("./notification_error.route") const notificationErrorSparepart = require("./notification_error_sparepart.route") const sparepart = require("./sparepart.route") const notificationErrorLog = require("./notification_error_log.route") +const errorCode = require("./error_code.route") router.use("/auth", auth); router.use("/user", users); @@ -38,5 +39,6 @@ router.use("/notification", notificationError) router.use("/notification-sparepart", notificationErrorSparepart) router.use("/sparepart", sparepart) router.use("/notification-log", notificationErrorLog) +router.use("/error-code", errorCode) module.exports = router; diff --git a/services/brand.service.js b/services/brand.service.js index 867f55a..256c676 100644 --- a/services/brand.service.js +++ b/services/brand.service.js @@ -51,66 +51,13 @@ class BrandService { } } - // Get brand by ID with complete data + // Get brand by ID (without error codes) static async getBrandById(id) { try { const brand = await getBrandByIdDb(id); if (!brand) throw new ErrorHandler(404, "Brand not found"); - const errorCodes = await getErrorCodesByBrandIdDb(brand.brand_id); - - const errorCodesWithSolutionsAndSpareparts = await Promise.all( - errorCodes.map(async (errorCode) => { - const solutions = await getSolutionsByErrorCodeIdDb( - errorCode.error_code_id - ); - - // Get spareparts for this error code - const errorCodeSpareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); - - 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); - } - - 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; - }) - ); - - return { - ...errorCode, - solution: solutionsWithFiles, - spareparts: errorCodeSpareparts, - }; - }) - ); - - return { - ...brand, - error_code: errorCodesWithSolutionsAndSpareparts, - }; + return brand; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); } diff --git a/services/error_code.service.js b/services/error_code.service.js new file mode 100644 index 0000000..c8f03ba --- /dev/null +++ b/services/error_code.service.js @@ -0,0 +1,294 @@ +const { ErrorHandler } = require("../helpers/error"); +const { + getErrorCodesByBrandIdDb, + getErrorCodeByIdDb, + getErrorCodeByBrandAndCodeDb, + createErrorCodeDb, + updateErrorCodeDb, + deleteErrorCodeDb, + getAllErrorCodesDb, +} = require("../db/brand_code.db"); + +const { + getSolutionsByErrorCodeIdDb, + createSolutionDb, + updateSolutionDb, + deleteSolutionDb, +} = require("../db/brand_code_solution.db"); + +const { + getSparepartsByErrorCodeIdDb, + insertMultipleErrorCodeSparepartsDb, + updateErrorCodeSparepartsDb, +} = require("../db/brand_sparepart.db"); + +const { getFileUploadByPathDb } = require("../db/file_uploads.db"); + +class ErrorCodeService { + // Get all error codes with pagination and search + static async getAllErrorCodes(param) { + try { + const results = await getAllErrorCodesDb(param); + + // Enhance with solutions and spareparts for each error code + const errorCodesWithDetails = await Promise.all( + results.data.map(async (errorCode) => { + const solutions = await getSolutionsByErrorCodeIdDb(errorCode.error_code_id); + const spareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); + + const solutionsWithFiles = await Promise.all( + solutions.map(async (solution) => { + let fileData = null; + + if (solution.path_solution && solution.type_solution !== "text") { + fileData = await getFileUploadByPathDb(solution.path_solution); + } + + return { + ...solution, + file_upload_name: fileData?.file_upload_name || null, + path_document: fileData?.path_document || null, + }; + }) + ); + + return { + ...errorCode, + solution: solutionsWithFiles, + spareparts: spareparts, + }; + }) + ); + + return { + ...results, + data: errorCodesWithDetails, + }; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get error code by ID with complete data + static async getErrorCodeById(id) { + try { + const errorCode = await getErrorCodeByIdDb(id); + if (!errorCode) throw new ErrorHandler(404, "Error code not found"); + + const solutions = await getSolutionsByErrorCodeIdDb(errorCode.error_code_id); + const spareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); + + const solutionsWithFiles = await Promise.all( + solutions.map(async (solution) => { + let fileData = null; + + if (solution.path_solution && solution.type_solution !== "text") { + fileData = await getFileUploadByPathDb(solution.path_solution); + } + + return { + ...solution, + file_upload_name: fileData?.file_upload_name || null, + path_document: fileData?.path_document || null, + }; + }) + ); + + return { + ...errorCode, + solution: solutionsWithFiles, + spareparts: spareparts, + }; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get error codes by brand ID + static async getErrorCodesByBrandId(brandId) { + try { + const errorCodes = await getErrorCodesByBrandIdDb(brandId); + + const errorCodesWithDetails = await Promise.all( + errorCodes.map(async (errorCode) => { + const solutions = await getSolutionsByErrorCodeIdDb(errorCode.error_code_id); + const spareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); + + const solutionsWithFiles = await Promise.all( + solutions.map(async (solution) => { + let fileData = null; + + if (solution.path_solution && solution.type_solution !== "text") { + fileData = await getFileUploadByPathDb(solution.path_solution); + } + + return { + ...solution, + file_upload_name: fileData?.file_upload_name || null, + path_document: fileData?.path_document || null, + }; + }) + ); + + return { + ...errorCode, + solution: solutionsWithFiles, + spareparts: spareparts, + }; + }) + ); + + return errorCodesWithDetails; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create error code with solutions and spareparts + static async createErrorCodeWithFullData(brandId, data) { + try { + if (!data || typeof data !== "object") data = {}; + + if ( + !data.solution || + !Array.isArray(data.solution) || + data.solution.length === 0 + ) { + throw new ErrorHandler( + 400, + "Error code must have at least 1 solution" + ); + } + + const errorId = await createErrorCodeDb(brandId, { + error_code: data.error_code, + error_code_name: data.error_code_name, + error_code_description: data.error_code_description, + error_code_color: data.error_code_color, + path_icon: data.path_icon, + is_active: data.is_active, + created_by: data.created_by, + }); + + if (!errorId) { + throw new Error("Failed to create error code"); + } + + // Create sparepart relationships for this error code + if (data.spareparts && Array.isArray(data.spareparts)) { + await insertMultipleErrorCodeSparepartsDb(errorId, data.spareparts, data.created_by); + } + + // Create solutions for this error code + if (data.solution && Array.isArray(data.solution)) { + for (const solutionData of data.solution) { + await createSolutionDb(errorId, { + 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.created_by, + }); + } + } + + const createdErrorCode = await this.getErrorCodeById(errorId); + return createdErrorCode; + } catch (error) { + throw new ErrorHandler(500, `Create error code failed: ${error.message}`); + } + } + + // Update error code with solutions and spareparts + static async updateErrorCodeWithFullData(brandId, errorCode, data) { + try { + const existingErrorCode = await getErrorCodeByBrandAndCodeDb(brandId, errorCode); + if (!existingErrorCode) throw new ErrorHandler(404, "Error code not found"); + + // Update error code + await updateErrorCodeDb(brandId, errorCode, { + error_code_name: data.error_code_name, + error_code_description: data.error_code_description, + error_code_color: data.error_code_color, + path_icon: data.path_icon, + is_active: data.is_active, + updated_by: data.updated_by, + }); + + // Update spareparts if provided + if (data.spareparts && Array.isArray(data.spareparts)) { + await updateErrorCodeSparepartsDb(existingErrorCode.error_code_id, data.spareparts, data.updated_by); + } + + // Update solutions if provided + if (data.solution && Array.isArray(data.solution)) { + const existingSolutions = await getSolutionsByErrorCodeIdDb(existingErrorCode.error_code_id); + const incomingSolutionNames = data.solution.map((s) => s.solution_name); + + // Update or create solutions + for (const solutionData of data.solution) { + 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(existingErrorCode.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); + } + } + } + + const updatedErrorCode = await this.getErrorCodeById(existingErrorCode.error_code_id); + return updatedErrorCode; + } catch (error) { + throw new ErrorHandler(500, `Update error code failed: ${error.message}`); + } + } + + // Soft delete error code + static async deleteErrorCode(brandId, errorCode, deletedBy) { + try { + const errorCodeExist = await getErrorCodeByBrandAndCodeDb(brandId, errorCode); + + if (!errorCodeExist) { + throw new ErrorHandler(404, "Error code not found"); + } + + const result = await deleteErrorCodeDb(brandId, errorCode, deletedBy); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = ErrorCodeService; \ No newline at end of file From 790b94930276dd488e3fcd38e3973fc2781600fd Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 2 Dec 2025 15:29:43 +0700 Subject: [PATCH 06/37] repair: error code detail --- controllers/error_code.controller.js | 116 ++++++++++----------------- 1 file changed, 41 insertions(+), 75 deletions(-) diff --git a/controllers/error_code.controller.js b/controllers/error_code.controller.js index 07ff945..3e520ca 100644 --- a/controllers/error_code.controller.js +++ b/controllers/error_code.controller.js @@ -1,104 +1,70 @@ const ErrorCodeService = require('../services/error_code.service'); const { setResponse, setResponsePaging } = require('../helpers/utils'); -const { ErrorHandler } = require('../helpers/error'); class ErrorCodeController { - // Get all error codes with pagination and search - static async getAll(req, res) { - try { - const queryParams = req.query; - const results = await ErrorCodeService.getAllErrorCodes(queryParams); - const response = await setResponsePaging(queryParams, results, 'Error codes found'); - res.status(response.statusCode).json(response); - } catch (error) { - const response = setResponse(error.message, error.statusCode || 500); - res.status(response.statusCode).json(response); - } + static async getByBrandId(req, res) { + const { brandId } = req.params; + const queryParams = { + ...req.query, + brand_id: brandId + }; + + const results = await ErrorCodeService.getAllErrorCodes(queryParams); + const response = await setResponsePaging(queryParams, results, 'Error codes found'); + + res.status(response.statusCode).json(response); } // Get error code by ID static async getById(req, res) { - try { - const { id } = req.params; - const result = await ErrorCodeService.getErrorCodeById(id); - const response = setResponse(result, 200, 'Error code found'); - res.status(response.statusCode).json(response); - } catch (error) { - const response = setResponse(error.message, error.statusCode || 500); - res.status(response.statusCode).json(response); - } - } + const { id } = req.params; + const result = await ErrorCodeService.getErrorCodeById(id); + const response = setResponse(result, 'Error code found'); - // Get error codes by brand ID with pagination and search - static async getByBrandId(req, res) { - try { - const { brandId } = req.params; - const queryParams = { - ...req.query, - brand_id: brandId - }; - const results = await ErrorCodeService.getAllErrorCodes(queryParams); - const response = await setResponsePaging(queryParams, results, 'Error codes found'); - res.status(response.statusCode).json(response); - } catch (error) { - const response = setResponse(error.message, error.statusCode || 500); - res.status(response.statusCode).json(response); - } + res.status(response.statusCode).json(response); } // Create error code with solutions and spareparts static async create(req, res) { - try { - const { brandId } = req.params; - const createdBy = req.user?.user_id || null; + const { brandId } = req.params; + const createdBy = req.user?.user_id || null; - const data = { - ...req.body, - created_by: createdBy - }; + const data = { + ...req.body, + created_by: createdBy + }; - const result = await ErrorCodeService.createErrorCodeWithFullData(brandId, data); - const response = setResponse(result, 201, 'Error code created successfully'); - res.status(response.statusCode).json(response); - } catch (error) { - const response = setResponse(error.message, error.statusCode || 500); - res.status(response.statusCode).json(response); - } + const result = await ErrorCodeService.createErrorCodeWithFullData(brandId, data); + const response = setResponse(result, 'Error code created successfully'); + + res.status(response.statusCode).json(response); } // Update error code with solutions and spareparts static async update(req, res) { - try { - const { brandId, errorCode } = req.params; - const updatedBy = req.user?.user_id || null; + const { brandId, errorCode } = req.params; + const updatedBy = req.user?.user_id || null; - const data = { - ...req.body, - updated_by: updatedBy - }; + const data = { + ...req.body, + updated_by: updatedBy + }; - const result = await ErrorCodeService.updateErrorCodeWithFullData(brandId, errorCode, data); - const response = setResponse(result, 200, 'Error code updated successfully'); - res.status(response.statusCode).json(response); - } catch (error) { - const response = setResponse(error.message, error.statusCode || 500); - res.status(response.statusCode).json(response); - } + const result = await ErrorCodeService.updateErrorCodeWithFullData(brandId, errorCode, data); + const response = setResponse(result, 'Error code updated successfully'); + + res.status(response.statusCode).json(response); } // Soft delete error code static async delete(req, res) { - try { - const { brandId, errorCode } = req.params; - const deletedBy = req.user?.user_id || null; + const { brandId, errorCode } = req.params; + const deletedBy = req.user?.user_id || null; - const result = await ErrorCodeService.deleteErrorCode(brandId, errorCode, deletedBy); - const response = setResponse(result, 200, 'Error code deleted successfully'); - res.status(response.statusCode).json(response); - } catch (error) { - const response = setResponse(error.message, error.statusCode || 500); - res.status(response.statusCode).json(response); - } + const result = await ErrorCodeService.deleteErrorCode(brandId, errorCode, deletedBy); + const response = setResponse(result, 'Error code deleted successfully'); + + res.status(response.statusCode).json(response); } } From 867976030a1812b9ef800614384ec354f7719254 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 2 Dec 2025 15:58:06 +0700 Subject: [PATCH 07/37] repair: get error code --- db/brand_code.db.js | 7 +------ services/error_code.service.js | 37 ++-------------------------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/db/brand_code.db.js b/db/brand_code.db.js index 1a0e3e3..7220adc 100644 --- a/db/brand_code.db.js +++ b/db/brand_code.db.js @@ -108,13 +108,8 @@ const getAllErrorCodesDb = async (searchParams = {}) => { const queryText = ` SELECT COUNT(*) OVER() AS total_data, - a.*, - b.brand_name, - b.brand_type, - b.brand_manufacture, - b.brand_model + a.* FROM brand_code a - LEFT JOIN m_brands b ON a.brand_id = b.brand_id WHERE a.deleted_at IS NULL ${whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : ''} ${whereOrConditions ? whereOrConditions : ''} diff --git a/services/error_code.service.js b/services/error_code.service.js index c8f03ba..4197d32 100644 --- a/services/error_code.service.js +++ b/services/error_code.service.js @@ -25,45 +25,12 @@ const { const { getFileUploadByPathDb } = require("../db/file_uploads.db"); class ErrorCodeService { - // Get all error codes with pagination and search + // Get all error codes with pagination and search (without solutions and spareparts) static async getAllErrorCodes(param) { try { const results = await getAllErrorCodesDb(param); - // Enhance with solutions and spareparts for each error code - const errorCodesWithDetails = await Promise.all( - results.data.map(async (errorCode) => { - const solutions = await getSolutionsByErrorCodeIdDb(errorCode.error_code_id); - const spareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); - - const solutionsWithFiles = await Promise.all( - solutions.map(async (solution) => { - let fileData = null; - - if (solution.path_solution && solution.type_solution !== "text") { - fileData = await getFileUploadByPathDb(solution.path_solution); - } - - return { - ...solution, - file_upload_name: fileData?.file_upload_name || null, - path_document: fileData?.path_document || null, - }; - }) - ); - - return { - ...errorCode, - solution: solutionsWithFiles, - spareparts: spareparts, - }; - }) - ); - - return { - ...results, - data: errorCodesWithDetails, - }; + return results; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); } From 1ec69b388dd94b116658d6f4c417289a4500d77a Mon Sep 17 00:00:00 2001 From: vinix Date: Wed, 3 Dec 2025 13:27:18 +0700 Subject: [PATCH 08/37] repair: update sparepart_qty validation to allow zero --- validate/sparepart.schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validate/sparepart.schema.js b/validate/sparepart.schema.js index be204c1..967c5ff 100644 --- a/validate/sparepart.schema.js +++ b/validate/sparepart.schema.js @@ -21,7 +21,7 @@ const updateSparepartSchema = Joi.object({ sparepart_model: Joi.string().max(255).optional(), sparepart_foto: Joi.string().max(255).optional().allow(''), sparepart_item_type: Joi.string().max(255).optional(), - sparepart_qty: Joi.number().integer().min(1), + sparepart_qty: Joi.number().integer().min(0), sparepart_unit: Joi.string().max(255).optional(), sparepart_merk: Joi.string().max(255).optional(), sparepart_stok: Joi.string().max(255).optional(), From cecb88b8383fbdbd2ef2f2e17fc5fd1d3b67baf8 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 3 Dec 2025 16:15:18 +0700 Subject: [PATCH 09/37] update: delete path sollution --- controllers/file_uploads.controller.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/controllers/file_uploads.controller.js b/controllers/file_uploads.controller.js index 1c40607..f41b7cf 100644 --- a/controllers/file_uploads.controller.js +++ b/controllers/file_uploads.controller.js @@ -32,8 +32,7 @@ const uploadFile = async (req, res) => { const response = await setResponse( { file_upload_name: file.originalname, - path_document: pathDocument, - path_solution: pathDocument + path_document: pathDocument }, "File berhasil diunggah" ); @@ -45,8 +44,6 @@ const uploadFile = async (req, res) => { }; - - const getFileByPath = async (req, res) => { try { const { folder, filename } = req.params; From c8b2387e4f5f160273bf27c7948ef3ed8a11a22c Mon Sep 17 00:00:00 2001 From: vinix Date: Wed, 3 Dec 2025 16:45:24 +0700 Subject: [PATCH 10/37] izin ubah be dikit --- validate/sparepart.schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validate/sparepart.schema.js b/validate/sparepart.schema.js index 967c5ff..9f2815b 100644 --- a/validate/sparepart.schema.js +++ b/validate/sparepart.schema.js @@ -8,7 +8,7 @@ const insertSparepartSchema = Joi.object({ sparepart_model: Joi.string().max(255).optional(), sparepart_foto: Joi.string().max(255).optional().allow(""), sparepart_item_type: Joi.string().max(255).optional(), - sparepart_qty: Joi.number().integer().min(1), + sparepart_qty: Joi.number().integer().min(0), sparepart_unit: Joi.string().max(255).optional(), sparepart_merk: Joi.string().max(255).optional(), sparepart_stok: Joi.string().max(255).optional(), From 6fc21160f5336ec39cf294e28f2f27932b623575 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 4 Dec 2025 00:51:41 +0700 Subject: [PATCH 11/37] repair: get errorcodeby brand id --- controllers/error_code.controller.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/controllers/error_code.controller.js b/controllers/error_code.controller.js index 3e520ca..fbd01a4 100644 --- a/controllers/error_code.controller.js +++ b/controllers/error_code.controller.js @@ -4,13 +4,9 @@ const { setResponse, setResponsePaging } = require('../helpers/utils'); class ErrorCodeController { static async getByBrandId(req, res) { const { brandId } = req.params; - const queryParams = { - ...req.query, - brand_id: brandId - }; - const results = await ErrorCodeService.getAllErrorCodes(queryParams); - const response = await setResponsePaging(queryParams, results, 'Error codes found'); + const results = await ErrorCodeService.getErrorCodesByBrandId(brandId); + const response = setResponse(results, 'Error codes found'); res.status(response.statusCode).json(response); } From 8b4cc89be553a0c74aa2eae9147206eabd98a71a Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Thu, 4 Dec 2025 09:28:39 +0700 Subject: [PATCH 12/37] repair: change mandatory to opsional --- validate/contact.schema.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/validate/contact.schema.js b/validate/contact.schema.js index 8edcdf7..db362e6 100644 --- a/validate/contact.schema.js +++ b/validate/contact.schema.js @@ -13,14 +13,14 @@ const insertContactSchema = Joi.object({ "Phone number must be a valid Indonesian number in format +628XXXXXXXXX", }), is_active: Joi.boolean().required(), - contact_type: Joi.string().max(255).required() + contact_type: Joi.string().max(255).optional() }); const updateContactSchema = Joi.object({ - contact_name: Joi.string().min(3).max(100).required(), + contact_name: Joi.string().min(3).max(100).optional(), contact_phone: Joi.string() .pattern(/^(?:\+62|0)8\d{7,10}$/) - .required() + .optional() .messages({ "string.pattern.base": "Phone number must be a valid Indonesian number in format +628XXXXXXXXX", From 73b4abfd68a6b8f24a4e35dac4754ea3c84d84ec Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Thu, 4 Dec 2025 14:30:57 +0700 Subject: [PATCH 13/37] repair: notification error sparepart --- db/notification_error_sparepart.db.js | 55 ++++++++----- .../notification_error_sparepart.service.js | 81 ++++++++----------- 2 files changed, 69 insertions(+), 67 deletions(-) diff --git a/db/notification_error_sparepart.db.js b/db/notification_error_sparepart.db.js index 17b9288..45af6dc 100644 --- a/db/notification_error_sparepart.db.js +++ b/db/notification_error_sparepart.db.js @@ -11,10 +11,10 @@ const getAllNotificationErrorSparepartDb = async (searchParams = {}) => { const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( [ "a.brand_sparepart_id", - "a.contact_id", + "a.device_id", + "a.sparepart_id", "b.sparepart_name", - "d.contact_name", - "d.contact_type" + "d.device_name", ], searchParams.criteria, queryParams @@ -25,11 +25,10 @@ const getAllNotificationErrorSparepartDb = async (searchParams = {}) => { const { whereConditions, whereParamAnd } = pool.buildFilterQuery( [ { column: "a.brand_sparepart_id", param: searchParams.name, type: "int" }, - { column: "a.contact_id", param: searchParams.code, type: "int" }, + { column: "a.device_id", param: searchParams.code, type: "int" }, { column: "a.unit", param: searchParams.unit, type: "string" }, { column: "b.sparepart_name", param: searchParams.device, type: "string" }, - { column: "d.contact_name", param: searchParams.device, type: "string" }, - { column: "d.contact_type", param: searchParams.device, type: "string" }, + { column: "d.device_name", param: searchParams.device, type: "string" }, ], queryParams ); @@ -41,14 +40,23 @@ const getAllNotificationErrorSparepartDb = async (searchParams = {}) => { COUNT(*) OVER() AS total_data, a.*, b.sparepart_name, - b.brand_sparepart_description, - d.contact_name, - d.contact_type + b.sparepart_foto, + b.sparepart_stok, + b.sparepart_qty, + b.sparepart_description, + b.sparepart_model, + b.sparepart_merk, + b.sparepart_unit, + b.sparepart_item_type, + d.device_name + FROM notification_error_sparepart a - LEFT JOIN brand_sparepart b ON a.brand_sparepart_id = b.brand_sparepart_id + LEFT JOIN brand_sparepart c ON a.brand_sparepart_id = c.brand_sparepart_id - LEFT JOIN contact d on a.contact_id = d.contact_id + LEFT JOIN m_sparepart b ON c.sparepart_id = b.sparepart_id + + LEFT JOIN m_device d on c.device_id = d.device_id WHERE a.deleted_at IS NULL ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} @@ -69,15 +77,26 @@ const getAllNotificationErrorSparepartDb = async (searchParams = {}) => { const getNotificationErrorSparepartByIdDb = async (id) => { const queryText = ` - SELECT - a.*, + a.*, b.sparepart_name, - b.brand_sparepart_description, - d.contact_name, - d.contact_type + b.sparepart_foto, + b.sparepart_stok, + b.sparepart_qty, + b.sparepart_description, + b.sparepart_model, + b.sparepart_merk, + b.sparepart_unit, + b.sparepart_item_type, + d.device_name + FROM notification_error_sparepart a - LEFT JOIN brand_sparepart b ON a.brand_sparepart_id = b.brand_sparepart_id - LEFT JOIN contact d on a.contact_id = d.contact_id + + LEFT JOIN brand_sparepart c ON a.brand_sparepart_id = c.brand_sparepart_id + + LEFT JOIN m_sparepart b ON c.sparepart_id = b.sparepart_id + + LEFT JOIN m_device d on c.device_id = d.device_id + WHERE a.notification_error_sparepart_id = $1 AND a.deleted_at IS NULL `; diff --git a/services/notification_error_sparepart.service.js b/services/notification_error_sparepart.service.js index a5208c4..6f6620d 100644 --- a/services/notification_error_sparepart.service.js +++ b/services/notification_error_sparepart.service.js @@ -6,31 +6,23 @@ const { deleteNotificationErrorSparepartDb, } = require("../db/notification_error_sparepart.db"); -const { getContactByIdDb } = require("../db/contact.db"); const { ErrorHandler } = require("../helpers/error"); class NotificationErrorSparepartService { - static _checkAccess(contactType) { - if (contactType !== "gudang") { - throw new ErrorHandler( - 403, - "Akses ditolak. Hanya contact_type 'gudang' yang dapat getAll/create/update/delete." - ); + + static async getAll(param) { + try { + const results = await getAllNotificationErrorSparepartDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); } } - static async getAll(param, contact_id) { - const contactResult = await getContactByIdDb(contact_id); - - if (!contactResult || contactResult.length < 1) - throw new ErrorHandler(404, "Contact tidak ditemukan"); - - const contact = contactResult[0]; - - this._checkAccess(contact.contact_type); - return await getAllNotificationErrorSparepartDb(param); - } - static async getById(id) { const result = await getNotificationErrorSparepartByIdDb(id); @@ -40,46 +32,37 @@ class NotificationErrorSparepartService { return result; } - static async create(data) { - const contactResult = await getContactByIdDb(data.contact_id); + static async create(data) { + try { + if (!data || typeof data !== 'object') data = {}; - if (!contactResult || contactResult.length < 1) - throw new ErrorHandler(404, "Contact tidak ditemukan"); + const result = await createNotificationErrorSparepartDb(data); - const contact = contactResult[0]; - - this._checkAccess(contact.contact_type); - - return await createNotificationErrorSparepartDb(data); + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } } - static async update(id, data) { - const contactResult = await getContactByIdDb(data.contact_id); +static async update(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; - if (!contactResult || contactResult.length < 1) - throw new ErrorHandler(404, "Contact tidak ditemukan"); + const dataExist = await getNotificationErrorSparepartByIdDb(id); - const contact = contactResult[0]; + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Roles not found'); + } - this._checkAccess(contact.contact_type); + const result = await updateNotificationErrorSparepartDb(id, data); - const exist = await getNotificationErrorSparepartByIdDb(id); - - if (exist.length < 1) - throw new ErrorHandler(404, "Notification Error Sparepart not found"); - - return await updateNotificationErrorSparepartDb(id, data); + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } } - static async delete(id, contact_id) { - const contactResult = await getContactByIdDb(contact_id); - - if (!contactResult || contactResult.length < 1) - throw new ErrorHandler(404, "Contact tidak ditemukan"); - - const contact = contactResult[0]; - - this._checkAccess(contact.contact_type); + static async delete(id) { const exist = await getNotificationErrorSparepartByIdDb(id); From be8020605696fe26a751640c7baa6ae0c25384d4 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Thu, 4 Dec 2025 14:31:41 +0700 Subject: [PATCH 14/37] repair: notification error sparepart --- services/notification_error_sparepart.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/notification_error_sparepart.service.js b/services/notification_error_sparepart.service.js index 6f6620d..1e79b3c 100644 --- a/services/notification_error_sparepart.service.js +++ b/services/notification_error_sparepart.service.js @@ -51,7 +51,7 @@ static async update(id, data) { const dataExist = await getNotificationErrorSparepartByIdDb(id); if (dataExist.length < 1) { - throw new ErrorHandler(404, 'Roles not found'); + throw new ErrorHandler(404, 'Notification Error Sparepart not found'); } const result = await updateNotificationErrorSparepartDb(id, data); From 31daa470b70bb404c32cdc75bce8d7032b9ca761 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Thu, 4 Dec 2025 14:34:32 +0700 Subject: [PATCH 15/37] repair: notification error sparepart --- db/notification_error_sparepart.db.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/notification_error_sparepart.db.js b/db/notification_error_sparepart.db.js index 45af6dc..a20c13a 100644 --- a/db/notification_error_sparepart.db.js +++ b/db/notification_error_sparepart.db.js @@ -77,7 +77,8 @@ const getAllNotificationErrorSparepartDb = async (searchParams = {}) => { const getNotificationErrorSparepartByIdDb = async (id) => { const queryText = ` - a.*, + SELECT + a.*, b.sparepart_name, b.sparepart_foto, b.sparepart_stok, From 096fe9461def9e96b1763150f0a3dfb88a90fd73 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 4 Dec 2025 15:37:48 +0700 Subject: [PATCH 16/37] add: field listen_channel m_device --- validate/device.schema.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/validate/device.schema.js b/validate/device.schema.js index c054b02..c7182cb 100644 --- a/validate/device.schema.js +++ b/validate/device.schema.js @@ -15,7 +15,8 @@ const insertDeviceSchema = Joi.object({ .required() .messages({ 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' - }) + }), + listen_channel: Joi.string().max(100).required() }); const updateDeviceSchema = Joi.object({ @@ -28,11 +29,11 @@ const updateDeviceSchema = Joi.object({ .ip({ version: ['ipv4', 'ipv6'] }) .messages({ 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' - }) + }), + listen_channel: Joi.string().max(100) }).min(1); -// ✅ Export dengan CommonJS module.exports = { insertDeviceSchema, updateDeviceSchema }; \ No newline at end of file From a8eb785a5b8f0b8e5546fad8a36a69ec717a3c2f Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 4 Dec 2025 15:38:19 +0700 Subject: [PATCH 17/37] repair get error code by brandId --- controllers/error_code.controller.js | 5 +-- db/brand_code.db.js | 46 ++++++++++++++++++++++++++-- db/brand_sparepart.db.js | 19 +++++------- services/error_code.service.js | 20 +++++------- 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/controllers/error_code.controller.js b/controllers/error_code.controller.js index fbd01a4..d3e2404 100644 --- a/controllers/error_code.controller.js +++ b/controllers/error_code.controller.js @@ -4,9 +4,10 @@ const { setResponse, setResponsePaging } = require('../helpers/utils'); class ErrorCodeController { static async getByBrandId(req, res) { const { brandId } = req.params; + const queryParams = req.query; - const results = await ErrorCodeService.getErrorCodesByBrandId(brandId); - const response = setResponse(results, 'Error codes found'); + const results = await ErrorCodeService.getErrorCodesByBrandId(brandId, queryParams); + const response = await setResponsePaging(queryParams, results, 'Error codes found'); res.status(response.statusCode).json(response); } diff --git a/db/brand_code.db.js b/db/brand_code.db.js index 7220adc..69f263b 100644 --- a/db/brand_code.db.js +++ b/db/brand_code.db.js @@ -1,15 +1,55 @@ const pool = require("../config"); // Get error codes by brand ID -const getErrorCodesByBrandIdDb = async (brandId) => { +const getErrorCodesByBrandIdDb = async (brandId, searchParams = {}) => { + let queryParams = [brandId]; + + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [brandId, Number(searchParams.limit ?? 10), page]; + } + + // Search across multiple columns + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.error_code", "a.error_code_name", "a.error_code_description"], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ? whereParamOr : queryParams; + + // Filter conditions + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.is_active", param: searchParams.status, type: "string" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + const queryText = ` SELECT + COUNT(*) OVER() AS total_data, a.* FROM brand_code a WHERE a.brand_id = $1 AND a.deleted_at IS NULL - ORDER BY a.error_code_id + ${whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} + ORDER BY a.error_code_id DESC + ${searchParams.limit ? `OFFSET $3 * $2 ROWS FETCH NEXT $2 ROWS ONLY` : ''} `; - const result = await pool.query(queryText, [brandId]); + + const result = await pool.query(queryText, queryParams); + + // Return paginated format if limit is provided + if (searchParams.limit) { + const total = result?.recordset.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; + return { data: result.recordset, total }; + } + + // Return simple array for backward compatibility return result.recordset; }; diff --git a/db/brand_sparepart.db.js b/db/brand_sparepart.db.js index 3cc37f1..9340324 100644 --- a/db/brand_sparepart.db.js +++ b/db/brand_sparepart.db.js @@ -17,7 +17,7 @@ const getSparepartsByErrorCodeIdDb = async (errorCodeId) => { s.sparepart_stok, bs.created_at, bs.created_by - FROM brand_spareparts bs + FROM brand_sparepart bs JOIN m_sparepart s ON bs.sparepart_id = s.sparepart_id WHERE bs.error_code_id = $1 AND s.deleted_at IS NULL @@ -40,7 +40,7 @@ const getErrorCodesBySparepartIdDb = async (sparepartId) => { ec.is_active, ec.created_at, ec.updated_at - FROM brand_spareparts bs + FROM brand_sparepart bs JOIN m_error_codes ec ON bs.error_code_id = ec.error_code_id WHERE bs.sparepart_id = $1 AND ec.deleted_at IS NULL @@ -53,7 +53,7 @@ const getErrorCodesBySparepartIdDb = async (sparepartId) => { // Insert error_code-spareparts relationship const insertErrorCodeSparepartDb = async (errorCodeId, sparepartId, createdBy) => { const queryText = ` - INSERT INTO brand_spareparts (error_code_id, sparepart_id, created_by, created_at) + INSERT INTO brand_sparepart (error_code_id, sparepart_id, created_by, created_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP) `; const result = await pool.query(queryText, [errorCodeId, sparepartId, createdBy]); @@ -66,7 +66,7 @@ const insertMultipleErrorCodeSparepartsDb = async (errorCodeId, sparepartIds, cr const values = sparepartIds.map((_, index) => `($1, $${index + 2}, $${sparepartIds.length + 2}, CURRENT_TIMESTAMP)`).join(', '); const queryText = ` - INSERT INTO brand_spareparts (error_code_id, sparepart_id, created_by, created_at) + INSERT INTO brand_sparepart (error_code_id, sparepart_id, created_by, created_at) VALUES ${values} `; const params = [errorCodeId, ...sparepartIds, createdBy]; @@ -77,7 +77,7 @@ const insertMultipleErrorCodeSparepartsDb = async (errorCodeId, sparepartIds, cr // Delete specific error_code-sparepart relationship const deleteErrorCodeSparepartDb = async (errorCodeId, sparepartId) => { const queryText = ` - DELETE FROM brand_spareparts + DELETE FROM brand_sparepart WHERE error_code_id = $1 AND sparepart_id = $2 `; const result = await pool.query(queryText, [errorCodeId, sparepartId]); @@ -87,7 +87,7 @@ const deleteErrorCodeSparepartDb = async (errorCodeId, sparepartId) => { // Delete all spareparts for an error_code const deleteAllErrorCodeSparepartsDb = async (errorCodeId) => { const queryText = ` - DELETE FROM brand_spareparts + DELETE FROM brand_sparepart WHERE error_code_id = $1 `; const result = await pool.query(queryText, [errorCodeId]); @@ -107,7 +107,6 @@ const updateErrorCodeSparepartsDb = async (errorCodeId, sparepartIds, updatedBy) return true; }; -// Check if error_code-sparepart relationship exists const checkErrorCodeSparepartExistsDb = async (errorCodeId, sparepartId) => { const queryText = ` SELECT 1 @@ -118,8 +117,6 @@ const checkErrorCodeSparepartExistsDb = async (errorCodeId, sparepartId) => { return result.recordset.length > 0; }; -// Legacy functions for backward compatibility (deprecated) -// Get spareparts by brand_id (now using error_code_id mapping via error codes) const getSparepartsByBrandIdDb = async (brandId) => { const queryText = ` SELECT DISTINCT @@ -136,7 +133,7 @@ const getSparepartsByBrandIdDb = async (brandId) => { s.sparepart_stok, s.created_at, s.updated_at - FROM brand_spareparts bs + FROM brand_sparepart bs JOIN m_sparepart s ON bs.sparepart_id = s.sparepart_id JOIN m_error_codes ec ON bs.error_code_id = ec.error_code_id WHERE ec.brand_id = $1 @@ -161,7 +158,7 @@ const getBrandsBySparepartIdDb = async (sparepartId) => { b.is_active, b.created_at, b.updated_at - FROM brand_spareparts bs + FROM brand_sparepart bs JOIN m_sparepart s ON bs.sparepart_id = s.sparepart_id JOIN m_error_codes ec ON bs.error_code_id = ec.error_code_id JOIN m_brands b ON ec.brand_id = b.brand_id diff --git a/services/error_code.service.js b/services/error_code.service.js index 4197d32..6c8a53c 100644 --- a/services/error_code.service.js +++ b/services/error_code.service.js @@ -72,12 +72,16 @@ class ErrorCodeService { } // Get error codes by brand ID - static async getErrorCodesByBrandId(brandId) { + static async getErrorCodesByBrandId(brandId, queryParams = {}) { try { - const errorCodes = await getErrorCodesByBrandIdDb(brandId); + const results = await getErrorCodesByBrandIdDb(brandId, queryParams); + + if (results.data && results.total !== undefined) { + return results; + } const errorCodesWithDetails = await Promise.all( - errorCodes.map(async (errorCode) => { + results.map(async (errorCode) => { const solutions = await getSolutionsByErrorCodeIdDb(errorCode.error_code_id); const spareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); @@ -140,13 +144,10 @@ class ErrorCodeService { if (!errorId) { throw new Error("Failed to create error code"); } - - // Create sparepart relationships for this error code if (data.spareparts && Array.isArray(data.spareparts)) { await insertMultipleErrorCodeSparepartsDb(errorId, data.spareparts, data.created_by); } - // Create solutions for this error code if (data.solution && Array.isArray(data.solution)) { for (const solutionData of data.solution) { await createSolutionDb(errorId, { @@ -173,7 +174,6 @@ class ErrorCodeService { const existingErrorCode = await getErrorCodeByBrandAndCodeDb(brandId, errorCode); if (!existingErrorCode) throw new ErrorHandler(404, "Error code not found"); - // Update error code await updateErrorCodeDb(brandId, errorCode, { error_code_name: data.error_code_name, error_code_description: data.error_code_description, @@ -183,24 +183,20 @@ class ErrorCodeService { updated_by: data.updated_by, }); - // Update spareparts if provided if (data.spareparts && Array.isArray(data.spareparts)) { await updateErrorCodeSparepartsDb(existingErrorCode.error_code_id, data.spareparts, data.updated_by); } - // Update solutions if provided if (data.solution && Array.isArray(data.solution)) { const existingSolutions = await getSolutionsByErrorCodeIdDb(existingErrorCode.error_code_id); const incomingSolutionNames = data.solution.map((s) => s.solution_name); - // Update or create solutions for (const solutionData of data.solution) { const existingSolution = existingSolutions.find( (s) => s.solution_name === solutionData.solution_name ); if (existingSolution) { - // Update existing solution await updateSolutionDb( existingSolution.brand_code_solution_id, { @@ -213,7 +209,6 @@ class ErrorCodeService { } ); } else { - // Create new solution await createSolutionDb(existingErrorCode.error_code_id, { solution_name: solutionData.solution_name, type_solution: solutionData.type_solution, @@ -225,7 +220,6 @@ class ErrorCodeService { } } - // 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); From 198346ff0b6e465f76554c519dbcad145972f9d8 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 4 Dec 2025 15:44:27 +0700 Subject: [PATCH 18/37] repair: device_description not required --- validate/device.schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validate/device.schema.js b/validate/device.schema.js index c7182cb..4b3f945 100644 --- a/validate/device.schema.js +++ b/validate/device.schema.js @@ -9,7 +9,7 @@ const insertDeviceSchema = Joi.object({ is_active: Joi.boolean().required(), brand_id: Joi.number().integer().min(1), device_location: Joi.string().max(100).required(), - device_description: Joi.string().required(), + device_description: Joi.string(), ip_address: Joi.string() .ip({ version: ['ipv4', 'ipv6'] }) .required() From 1d3de9ae41302371a3101d420e2dd0470c2d9dbb Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Thu, 4 Dec 2025 16:07:04 +0700 Subject: [PATCH 19/37] repair: notification error sparepart --- db/notification_error_sparepart.db.js | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/db/notification_error_sparepart.db.js b/db/notification_error_sparepart.db.js index a20c13a..a2e519f 100644 --- a/db/notification_error_sparepart.db.js +++ b/db/notification_error_sparepart.db.js @@ -1,6 +1,34 @@ const pool = require("../config"); +const insertNotificationErrorSparepartDb = async () => { + const insertQuery = ` + INSERT INTO notification_error_sparepart ( + notification_error_id, + brand_sparepart_id + ) + SELECT + ne.notification_error_id, + bs.brand_sparepart_id + + FROM notification_error ne + + INNER JOIN brand_sparepart bs + ON ne.error_code_id = bs.error_code_id + + LEFT JOIN notification_error_sparepart nes + ON nes.notification_error_id = ne.notification_error_id + AND nes.brand_sparepart_id = bs.brand_sparepart_id + AND nes.deleted_at IS NULL + + WHERE ne.deleted_at IS NULL + AND nes.notification_error_sparepart_id IS NULL; + `; + + await pool.query(insertQuery); +}; + const getAllNotificationErrorSparepartDb = async (searchParams = {}) => { + await insertNotificationErrorSparepartDb(); let queryParams = []; if (searchParams.limit) { From 20d035a1caa2276bc452e75acef2bff27f3257a9 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Fri, 5 Dec 2025 09:13:41 +0700 Subject: [PATCH 20/37] add: sparepart in detail notification error --- services/notification_error.service.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/notification_error.service.js b/services/notification_error.service.js index 8ca77d5..3254f9c 100644 --- a/services/notification_error.service.js +++ b/services/notification_error.service.js @@ -17,6 +17,10 @@ const { getNotificationErrorLogByNotificationErrorIdDb, } = require('../db/notification_error_log.db'); +const { + getSparepartsByErrorCodeIdDb, +} = require('../db/brand_sparepart.db'); + const { getFileUploadByPathDb } = require('../db/file_uploads.db'); const { ErrorHandler } = require('../helpers/error'); @@ -71,10 +75,13 @@ class NotificationService { }) ); + const spareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); + notification.error_code = { ...errorCode, solution: solutionsWithDetails }; + notification.spareparts = spareparts; } } From dc7712a79f2bec64bc73e45ebb2b30ce5c884f8e Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Fri, 5 Dec 2025 09:21:04 +0700 Subject: [PATCH 21/37] add: sparepart in detail notification error --- db/notification_error.db.js | 2 +- services/notification_error.service.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 45d8449..a2068aa 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -18,7 +18,7 @@ const InsertNotificationErrorDb = async () => { 1 AS is_send, CONCAT( - COALESCE(b.error_code_description, '-'), + COALESCE(b.error_code_name, '-'), ' pada ', COALESCE(d.device_name, '-'), '. Pengecekan potensi kerusakan dibutuhkan' diff --git a/services/notification_error.service.js b/services/notification_error.service.js index 3254f9c..4053325 100644 --- a/services/notification_error.service.js +++ b/services/notification_error.service.js @@ -57,6 +57,8 @@ class NotificationService { // Get solutions for this error code const solutions = (await getSolutionsByErrorCodeIdDb(errorCode.error_code_id)) || []; + const spareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); + const solutionsWithDetails = await Promise.all( solutions.map(async (solution) => { let fileData = null; @@ -75,13 +77,12 @@ class NotificationService { }) ); - const spareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); notification.error_code = { ...errorCode, - solution: solutionsWithDetails + solution: solutionsWithDetails, + spareparts: spareparts }; - notification.spareparts = spareparts; } } From 555a68e90c933a3e9b0bd9e114a2afdf0a23cef1 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 5 Dec 2025 18:18:50 +0700 Subject: [PATCH 22/37] repair: brand + errorcode --- controllers/brand.controller.js | 20 +-- controllers/error_code.controller.js | 40 +++-- routes/error_code.route.js | 2 +- services/brand.service.js | 249 ++------------------------- services/error_code.service.js | 47 +++-- validate/brand.schema.js | 81 +-------- validate/error_code.schema.js | 71 ++++++++ 7 files changed, 151 insertions(+), 359 deletions(-) create mode 100644 validate/error_code.schema.js diff --git a/controllers/brand.controller.js b/controllers/brand.controller.js index c84a8bd..7eb1437 100644 --- a/controllers/brand.controller.js +++ b/controllers/brand.controller.js @@ -29,7 +29,7 @@ class BrandController { res.status(response.statusCode).json(response); } - // Create brand with nested error codes and solutions + // Create brand static async create(req, res) { const { error, value } = await checkValidate(insertBrandSchema, req); @@ -39,7 +39,7 @@ class BrandController { value.created_by = req.user?.user_id || null; - const results = await BrandService.createBrandWithFullData(value); + const results = await BrandService.createBrand(value); const response = await setResponse(results, 'Brand created successfully'); return res.status(response.statusCode).json(response); @@ -49,29 +49,15 @@ class BrandController { static async update(req, res) { const { id } = req.params; - // Debug logging untuk lihat request body - console.log('🔍 BE Raw Request Body:', req.body); - console.log('🔍 BE Request Headers:', req.headers); - console.log('🔍 BE Request Method:', req.method); - const { error, value } = await checkValidate(updateBrandSchema, req); if (error) { - console.log('❌ BE Validation Error:', { - error, - details: error.details?.map(d => ({ - field: d.path.join('.'), - message: d.message, - value: d.context?.value - })), - requestBody: req.body - }); return res.status(400).json(setResponse(error, 'Validation failed', 400)); } value.updated_by = req.user?.user_id || null; - const results = await BrandService.updateBrandWithFullData(id, value); + const results = await BrandService.updateBrand(id, value); const response = await setResponse(results, 'Brand updated successfully'); res.status(response.statusCode).json(response); diff --git a/controllers/error_code.controller.js b/controllers/error_code.controller.js index d3e2404..a263397 100644 --- a/controllers/error_code.controller.js +++ b/controllers/error_code.controller.js @@ -1,5 +1,9 @@ const ErrorCodeService = require('../services/error_code.service'); -const { setResponse, setResponsePaging } = require('../helpers/utils'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { + insertErrorCodeSchema, + updateErrorCodeSchema, +} = require('../validate/error_code.schema'); class ErrorCodeController { static async getByBrandId(req, res) { @@ -23,15 +27,16 @@ class ErrorCodeController { // Create error code with solutions and spareparts static async create(req, res) { + const { error, value } = await checkValidate(insertErrorCodeSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + const { brandId } = req.params; - const createdBy = req.user?.user_id || null; + value.created_by = req.user?.user_id || null; - const data = { - ...req.body, - created_by: createdBy - }; - - const result = await ErrorCodeService.createErrorCodeWithFullData(brandId, data); + const result = await ErrorCodeService.createErrorCodeWithFullData(brandId, value); const response = setResponse(result, 'Error code created successfully'); res.status(response.statusCode).json(response); @@ -39,15 +44,16 @@ class ErrorCodeController { // Update error code with solutions and spareparts static async update(req, res) { - const { brandId, errorCode } = req.params; - const updatedBy = req.user?.user_id || null; + const { error, value } = await checkValidate(updateErrorCodeSchema, req); - const data = { - ...req.body, - updated_by: updatedBy - }; + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } - const result = await ErrorCodeService.updateErrorCodeWithFullData(brandId, errorCode, data); + const { brandId, errorCodeId } = req.params; + value.updated_by = req.user?.user_id || null; + + const result = await ErrorCodeService.updateErrorCodeWithFullData(brandId, errorCodeId, value); const response = setResponse(result, 'Error code updated successfully'); res.status(response.statusCode).json(response); @@ -55,10 +61,10 @@ class ErrorCodeController { // Soft delete error code static async delete(req, res) { - const { brandId, errorCode } = req.params; + const { brandId, errorCodeId } = req.params; const deletedBy = req.user?.user_id || null; - const result = await ErrorCodeService.deleteErrorCode(brandId, errorCode, deletedBy); + const result = await ErrorCodeService.deleteErrorCode(brandId, errorCodeId, deletedBy); const response = setResponse(result, 'Error code deleted successfully'); res.status(response.statusCode).json(response); diff --git a/routes/error_code.route.js b/routes/error_code.route.js index c7ba80c..c65860b 100644 --- a/routes/error_code.route.js +++ b/routes/error_code.route.js @@ -9,7 +9,7 @@ router.route('/brand/:brandId') .get(verifyToken.verifyAccessToken, ErrorCodeController.getByBrandId) .post(verifyToken.verifyAccessToken, verifyAccess(), ErrorCodeController.create); -router.route('/brand/:brandId/:errorCode') +router.route('/brand/:brandId/:errorCodeId') .put(verifyToken.verifyAccessToken, verifyAccess(), ErrorCodeController.update) .delete(verifyToken.verifyAccessToken, verifyAccess(), ErrorCodeController.delete); diff --git a/services/brand.service.js b/services/brand.service.js index 256c676..a50b8b4 100644 --- a/services/brand.service.js +++ b/services/brand.service.js @@ -7,32 +7,6 @@ const { deleteBrandDb, checkBrandNameExistsDb, } = require("../db/brand.db"); - -const { - insertMultipleErrorCodeSparepartsDb, - updateErrorCodeSparepartsDb, - getSparepartsByErrorCodeIdDb, -} = require("../db/brand_sparepart.db"); - -// Error code operations -const { - getErrorCodesByBrandIdDb, - createErrorCodeDb, - updateErrorCodeDb, - deleteErrorCodeDb, -} = require("../db/brand_code.db"); - -// Sparepart operations -const { getSparepartsByIdsDb } = require("../db/sparepart.db"); - -// Solution operations -const { - getSolutionsByErrorCodeIdDb, - createSolutionDb, - updateSolutionDb, - deleteSolutionDb, -} = require("../db/brand_code_solution.db"); -const { getFileUploadByPathDb } = require("../db/file_uploads.db"); const { ErrorHandler } = require("../helpers/error"); class BrandService { @@ -41,7 +15,6 @@ class BrandService { try { const results = await getAllBrandsDb(param); - // Return brands data - spareparts are now associated with error codes, not brands return { ...results, data: results.data @@ -65,10 +38,8 @@ class BrandService { // Create brand - static async createBrandWithFullData(data) { + static async createBrand(data) { try { - if (!data || typeof data !== "object") data = {}; - if (data.brand_name) { const brandExists = await checkBrandNameExistsDb(data.brand_name); if (brandExists) { @@ -76,85 +47,23 @@ class BrandService { } } - if ( - !data.error_code || - !Array.isArray(data.error_code) || - data.error_code.length === 0 - ) { - throw new ErrorHandler( - 400, - "Brand must have at least 1 error code with solution" - ); - } - - for (const errorCode of data.error_code) { - if ( - !errorCode.solution || - !Array.isArray(errorCode.solution) || - errorCode.solution.length === 0 - ) { - throw new ErrorHandler( - 400, - `Error code ${errorCode.error_code} must have at least 1 solution` - ); - } - } - const brandData = { brand_name: data.brand_name, brand_type: data.brand_type, brand_manufacture: data.brand_manufacture, brand_model: data.brand_model, - is_active: data.is_active, + is_active: data.is_active !== undefined ? data.is_active : true, created_by: data.created_by, }; const createdBrand = await createBrandDb(brandData); if (!createdBrand) { - throw new Error("Failed to create brand"); + throw new ErrorHandler(500, "Failed to create brand"); } - const brandId = createdBrand.brand_id; - - for (const errorCodeData of data.error_code) { - const errorId = await createErrorCodeDb(brandId, { - error_code: errorCodeData.error_code, - error_code_name: errorCodeData.error_code_name, - error_code_description: errorCodeData.error_code_description, - error_code_color: errorCodeData.error_code_color, - path_icon: errorCodeData.path_icon, - is_active: errorCodeData.is_active, - created_by: data.created_by, - }); - - if (!errorId) { - throw new Error("Failed to create error code"); - } - - // Create sparepart relationships for this error code - if (errorCodeData.spareparts && Array.isArray(errorCodeData.spareparts)) { - await insertMultipleErrorCodeSparepartsDb(errorId, errorCodeData.spareparts, data.created_by); - } - - // Create solutions for this error code - if (errorCodeData.solution && Array.isArray(errorCodeData.solution)) { - for (const solutionData of errorCodeData.solution) { - await createSolutionDb(errorId, { - 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.created_by, - }); - } - } - } - - const createdBrandWithSpareparts = await this.getBrandById(brandId); - return createdBrandWithSpareparts; + return createdBrand; } catch (error) { - throw new ErrorHandler(500, `Bulk insert failed: ${error.message}`); + throw new ErrorHandler(error.statusCode || 500, error.message); } } @@ -177,12 +86,11 @@ class BrandService { // Update brand - static async updateBrandWithFullData(id, data) { + static async updateBrand(id, data) { try { const existingBrand = await getBrandByIdDb(id); if (!existingBrand) throw new ErrorHandler(404, "Brand not found"); - if (data.brand_name && data.brand_name !== existingBrand.brand_name) { const brandExists = await checkBrandNameExistsDb(data.brand_name, id); if (brandExists) { @@ -191,147 +99,22 @@ class BrandService { } const brandData = { - brand_name: data.brand_name, - brand_type: data.brand_type, - brand_manufacture: data.brand_manufacture, - brand_model: data.brand_model, - is_active: data.is_active, + brand_name: data.brand_name || existingBrand.brand_name, + brand_type: data.brand_type !== undefined ? data.brand_type : existingBrand.brand_type, + brand_manufacture: data.brand_manufacture !== undefined ? data.brand_manufacture : existingBrand.brand_manufacture, + brand_model: data.brand_model !== undefined ? data.brand_model : existingBrand.brand_model, + is_active: data.is_active !== undefined ? data.is_active : existingBrand.is_active, updated_by: data.updated_by, }; - await updateBrandDb(existingBrand.brand_name, brandData); - - 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) { - // Check if error code already exists - const existingEC = existingErrorCodes.find( - (ec) => ec.error_code === errorCodeData.error_code - ); - - if (existingEC) { - // Update existing error code using separate db function - await updateErrorCodeDb( - existingEC.brand_id, - existingEC.error_code, - { - error_code_name: errorCodeData.error_code_name, - error_code_description: errorCodeData.error_code_description, - error_code_color: errorCodeData.error_code_color, - path_icon: errorCodeData.path_icon, - is_active: errorCodeData.is_active, - updated_by: data.updated_by, - } - ); - - if (errorCodeData.spareparts && Array.isArray(errorCodeData.spareparts)) { - await updateErrorCodeSparepartsDb(existingEC.error_code_id, errorCodeData.spareparts, data.updated_by); - } - - 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) { - 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 { - const errorId = await createErrorCodeDb(id, { - error_code: errorCodeData.error_code, - error_code_name: errorCodeData.error_code_name, - error_code_description: errorCodeData.error_code_description, - error_code_color: errorCodeData.error_code_color, - path_icon: errorCodeData.path_icon, - is_active: errorCodeData.is_active, - created_by: data.updated_by, - }); - - if (errorCodeData.spareparts && Array.isArray(errorCodeData.spareparts)) { - await insertMultipleErrorCodeSparepartsDb(errorId, errorCodeData.spareparts, data.updated_by); - } - - if ( - errorCodeData.solution && - Array.isArray(errorCodeData.solution) - ) { - for (const solutionData of errorCodeData.solution) { - await createSolutionDb(errorId, { - 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, - }); - } - } - } - } - - for (const existingEC of existingErrorCodes) { - if (!incomingErrorCodes.includes(existingEC.error_code)) { - await deleteErrorCodeDb(id, existingEC.error_code, data.updated_by); - } - } + const updatedBrand = await updateBrandDb(existingBrand.brand_name, brandData); + if (!updatedBrand) { + throw new ErrorHandler(500, "Failed to update brand"); } - const updatedBrandWithSpareparts = await this.getBrandById(id); - return updatedBrandWithSpareparts; + return updatedBrand; } catch (error) { - throw new ErrorHandler(500, `Update failed: ${error.message}`); + throw new ErrorHandler(error.statusCode || 500, error.message); } } } diff --git a/services/error_code.service.js b/services/error_code.service.js index 6c8a53c..2576c38 100644 --- a/services/error_code.service.js +++ b/services/error_code.service.js @@ -25,7 +25,6 @@ const { const { getFileUploadByPathDb } = require("../db/file_uploads.db"); class ErrorCodeService { - // Get all error codes with pagination and search (without solutions and spareparts) static async getAllErrorCodes(param) { try { const results = await getAllErrorCodesDb(param); @@ -169,19 +168,34 @@ class ErrorCodeService { } // Update error code with solutions and spareparts - static async updateErrorCodeWithFullData(brandId, errorCode, data) { + static async updateErrorCodeWithFullData(brandId, errorCodeId, data) { try { - const existingErrorCode = await getErrorCodeByBrandAndCodeDb(brandId, errorCode); + const existingErrorCode = await getErrorCodeByIdDb(errorCodeId); if (!existingErrorCode) throw new ErrorHandler(404, "Error code not found"); - await updateErrorCodeDb(brandId, errorCode, { - error_code_name: data.error_code_name, - error_code_description: data.error_code_description, - error_code_color: data.error_code_color, - path_icon: data.path_icon, - is_active: data.is_active, - updated_by: data.updated_by, - }); + // Verify the error code belongs to the specified brand + if (existingErrorCode.brand_id !== parseInt(brandId)) { + throw new ErrorHandler(403, "Error code does not belong to specified brand"); + } + + // Check if there are any error code fields to update + const hasMainFieldUpdate = + data.error_code_name !== undefined || + data.error_code_description !== undefined || + data.error_code_color !== undefined || + data.path_icon !== undefined || + data.is_active !== undefined; + + if (hasMainFieldUpdate) { + await updateErrorCodeDb(brandId, existingErrorCode.error_code, { + error_code_name: data.error_code_name, + error_code_description: data.error_code_description, + error_code_color: data.error_code_color, + path_icon: data.path_icon, + is_active: data.is_active, + updated_by: data.updated_by, + }); + } if (data.spareparts && Array.isArray(data.spareparts)) { await updateErrorCodeSparepartsDb(existingErrorCode.error_code_id, data.spareparts, data.updated_by); @@ -235,15 +249,20 @@ class ErrorCodeService { } // Soft delete error code - static async deleteErrorCode(brandId, errorCode, deletedBy) { + static async deleteErrorCode(brandId, errorCodeId, deletedBy) { try { - const errorCodeExist = await getErrorCodeByBrandAndCodeDb(brandId, errorCode); + const errorCodeExist = await getErrorCodeByIdDb(errorCodeId); if (!errorCodeExist) { throw new ErrorHandler(404, "Error code not found"); } - const result = await deleteErrorCodeDb(brandId, errorCode, deletedBy); + // Verify the error code belongs to the specified brand + if (errorCodeExist.brand_id !== parseInt(brandId)) { + throw new ErrorHandler(403, "Error code does not belong to specified brand"); + } + + const result = await deleteErrorCodeDb(brandId, errorCodeExist.error_code, deletedBy); return result; } catch (error) { diff --git a/validate/brand.schema.js b/validate/brand.schema.js index 57fe595..954cf19 100644 --- a/validate/brand.schema.js +++ b/validate/brand.schema.js @@ -8,91 +8,18 @@ const insertBrandSchema = Joi.object({ 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(), + is_active: Joi.boolean().optional().default(true), 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().allow(""), - error_code_color: Joi.string().optional().allow(""), - path_icon: Joi.string().optional().allow(""), - is_active: Joi.boolean().required(), - what_action_to_take: Joi.string().optional().allow(""), - spareparts: Joi.array().items(Joi.number().integer()).optional(), - solution: Joi.array() - .items( - Joi.object({ - solution_name: Joi.string().max(100).required(), - type_solution: Joi.string() - .valid("text", "pdf", "image", "video", "link") - .required(), - text_solution: Joi.when("type_solution", { - is: "text", - then: Joi.string().required(), - otherwise: Joi.string().optional().allow(""), - }), - path_solution: Joi.when("type_solution", { - is: "text", - then: Joi.string().optional().allow(""), - otherwise: Joi.string().required(), - }), - is_active: Joi.boolean().required(), - }) - ) - .min(1) - .required(), - }) - ) - .min(1) - .required(), }); // Update Brand Validation const updateBrandSchema = Joi.object({ - brand_name: Joi.string().max(100).required(), + brand_name: Joi.string().max(100).optional(), brand_type: Joi.string().max(50).optional().allow(""), - brand_manufacture: Joi.string().max(100).required(), + brand_manufacture: Joi.string().max(100).optional(), brand_model: Joi.string().max(100).optional().allow(""), - is_active: Joi.boolean().required(), + is_active: Joi.boolean().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().allow(""), - error_code_color: Joi.string().optional().allow(""), - path_icon: Joi.string().optional().allow(""), - is_active: Joi.boolean().required(), - what_action_to_take: Joi.string().optional().allow(""), - spareparts: Joi.array().items(Joi.number().integer()).optional(), - solution: Joi.array() - .items( - Joi.object({ - solution_name: Joi.string().max(100).required(), - type_solution: Joi.string() - .valid("text", "pdf", "image", "video", "link") - .required(), - text_solution: Joi.when("type_solution", { - is: "text", - then: Joi.string().required(), - otherwise: Joi.string().optional().allow(""), - }), - path_solution: Joi.when("type_solution", { - is: "text", - then: Joi.string().optional().allow(""), - otherwise: Joi.string().required(), - }), - is_active: Joi.boolean().optional(), - }) - ) - .min(1) - .required(), - }) - ) - .optional(), }).min(1); module.exports = { diff --git a/validate/error_code.schema.js b/validate/error_code.schema.js new file mode 100644 index 0000000..957b718 --- /dev/null +++ b/validate/error_code.schema.js @@ -0,0 +1,71 @@ +const Joi = require("joi"); + +// ======================== +// Error Code Validation +// ======================== + +const solutionSchema = Joi.object({ + solution_name: Joi.string().max(100).required(), + type_solution: Joi.string() + .valid("text", "pdf", "image", "video", "link") + .required(), + text_solution: Joi.when("type_solution", { + is: "text", + then: Joi.string().required(), + otherwise: Joi.string().optional().allow(""), + }), + path_solution: Joi.when("type_solution", { + is: "text", + then: Joi.string().optional().allow(""), + otherwise: Joi.string().required(), + }), + is_active: Joi.boolean().default(true), +}); + +const insertErrorCodeSchema = Joi.object({ + error_code: Joi.string().max(100).required(), + error_code_name: Joi.string().max(100).required(), + error_code_description: Joi.string().optional().allow(""), + error_code_color: Joi.string().optional().allow(""), + path_icon: Joi.string().optional().allow(""), + is_active: Joi.boolean().default(true), + solution: Joi.array() + .items(solutionSchema) + .min(1) + .required() + .messages({ + "array.min": "Error code must have at least 1 solution", + }), + spareparts: Joi.array() + .items(Joi.number().integer()) + .optional(), +}).messages({ + "object.unknown": "{{#child}} is not allowed", +}); + +const updateErrorCodeSchema = Joi.object({ + error_code_name: Joi.string().max(100).optional(), + error_code_description: Joi.string().optional().allow(""), + error_code_color: Joi.string().optional().allow(""), + path_icon: Joi.string().optional().allow(""), + is_active: Joi.boolean().optional(), + solution: Joi.array() + .items(solutionSchema) + .min(1) + .optional() + .messages({ + "array.min": "Error code must have at least 1 solution", + }), + spareparts: Joi.array() + .items(Joi.number().integer()) + .optional(), +}).min(1).messages({ + "object.min": "At least one field must be provided for update", + "object.unknown": "{{#child}} is not allowed", +}); + +module.exports = { + insertErrorCodeSchema, + updateErrorCodeSchema, + solutionSchema, +}; \ No newline at end of file From d063478fc2196fb30b8711e4896b16341096afa7 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Mon, 8 Dec 2025 11:29:16 +0700 Subject: [PATCH 23/37] add: path icon & error code color in all notif error --- db/notification_error.db.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/db/notification_error.db.js b/db/notification_error.db.js index a2068aa..721e36d 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -111,6 +111,8 @@ const getAllNotificationDb = async (searchParams = {}) => { b.error_code, b.error_code_name, + b.error_code_color, + b.path_icon, b.created_at, c.solution_name, @@ -122,13 +124,16 @@ const getAllNotificationDb = async (searchParams = {}) => { COALESCE(d.device_name, '') + ' - ' + COALESCE(b.error_code_name, '') AS device_name_error - FROM notification_error a - LEFT JOIN brand_code b - ON a.error_code_id = b.error_code_id AND b.deleted_at IS NULL - LEFT JOIN brand_code_solution c - ON b.error_code_id = c.error_code_id AND c.deleted_at IS NULL - LEFT JOIN m_device d - ON b.brand_id = d.brand_id AND d.deleted_at IS NULL + FROM notification_error a + + LEFT JOIN brand_code b + ON a.error_code_id = b.error_code_id AND b.deleted_at IS NULL + + LEFT JOIN brand_code_solution c + ON b.error_code_id = c.error_code_id AND c.deleted_at IS NULL + + LEFT JOIN m_device d + ON b.brand_id = d.brand_id AND d.deleted_at IS NULL WHERE a.deleted_at IS NULL ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} From fb3061e0d1dfec78a80ca22ad8f999a43587cb97 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Tue, 9 Dec 2025 16:04:32 +0700 Subject: [PATCH 24/37] add: add notification --- controllers/notification_error.controller.js | 45 +++++++++++++++--- db/notification_error.db.js | 49 ++++---------------- routes/notification_error.route.js | 8 +++- services/notification_error.service.js | 13 ++++++ validate/notification.schema.js | 2 + 5 files changed, 69 insertions(+), 48 deletions(-) diff --git a/controllers/notification_error.controller.js b/controllers/notification_error.controller.js index d37a36c..a710ab4 100644 --- a/controllers/notification_error.controller.js +++ b/controllers/notification_error.controller.js @@ -1,12 +1,26 @@ -const NotificationErrorService = require('../services/notification_error.service'); -const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const NotificationErrorService = require("../services/notification_error.service"); +const { + setResponse, + setResponsePaging, + checkValidate, +} = require("../helpers/utils"); +const { + insertNotificationSchema, + updateNotificationSchema, +} = require("../validate/notification.schema"); class NotificationErrorController { static async getAll(req, res) { const queryParams = req.query; - const results = await NotificationErrorService.getAllNotification(queryParams); - const response = await setResponsePaging(queryParams, results, 'Notification found') + const results = await NotificationErrorService.getAllNotification( + queryParams + ); + const response = await setResponsePaging( + queryParams, + results, + "Notification found" + ); res.status(response.statusCode).json(response); } @@ -15,11 +29,30 @@ class NotificationErrorController { const { id } = req.params; const results = await NotificationErrorService.getNotificationById(id); - const response = await setResponse(results, 'Notification retrieved successfully'); + const response = await setResponse( + results, + "Notification retrieved successfully" + ); return res.status(response.statusCode).json(response); } + static async create(req, res) { + const { error, value } = await checkValidate(insertNotificationSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, "Validation failed", 400)); + } + + value.userId = req.user.user_id; + + const results = await NotificationErrorService.createNotificationError( + value + ); + const response = await setResponse(results, "Notification created successfully"); + + return res.status(response.statusCode).json(response); + } } -module.exports = NotificationErrorController; \ No newline at end of file +module.exports = NotificationErrorController; diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 721e36d..d7b9220 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -1,44 +1,14 @@ const pool = require("../config"); -const InsertNotificationErrorDb = async () => { - const insertQuery = ` - INSERT INTO notification_error ( - error_code_id, - is_active, - is_delivered, - is_read, - is_send, - message_error_issue - ) - SELECT - b.error_code_id, - 1 AS is_active, - 1 AS is_delivered, - 0 AS is_read, - 1 AS is_send, +const InsertNotificationErrorDb = async (store) => { + const { query: queryText, values } = pool.buildDynamicInsert( + "notification_error", + store + ); + const result = await pool.query(queryText, values); + const insertedId = result.recordset?.[0]?.inserted_id; - CONCAT( - COALESCE(b.error_code_name, '-'), - ' pada ', - COALESCE(d.device_name, '-'), - '. Pengecekan potensi kerusakan dibutuhkan' - ) AS message_error_issue - - FROM brand_code b - - LEFT JOIN notification_error a - ON a.error_code_id = b.error_code_id - AND a.deleted_at IS NULL - - LEFT JOIN m_device d - ON b.brand_id = d.brand_id - AND d.deleted_at IS NULL - - WHERE b.deleted_at IS NULL - AND a.notification_error_id IS NULL; - `; - - await pool.query(insertQuery); + return insertedId ? await getNotificationByIdDb(insertedId) : null; }; const getNotificationByIdDb = async (id) => { @@ -55,8 +25,6 @@ const getNotificationByIdDb = async (id) => { const getAllNotificationDb = async (searchParams = {}) => { let queryParams = []; - await InsertNotificationErrorDb(); - const boolFields = ["is_send", "is_delivered", "is_read", "is_active"]; boolFields.forEach((f) => { @@ -158,4 +126,5 @@ const getAllNotificationDb = async (searchParams = {}) => { module.exports = { getNotificationByIdDb, getAllNotificationDb, + InsertNotificationErrorDb }; diff --git a/routes/notification_error.route.js b/routes/notification_error.route.js index a0e8f83..20336cd 100644 --- a/routes/notification_error.route.js +++ b/routes/notification_error.route.js @@ -7,10 +7,14 @@ const router = express.Router(); router .route('/') - .get(verifyToken.verifyAccessToken, NotificationErrorController.getAll) + .get(verifyToken.verifyAccessToken,verifyAccess(), NotificationErrorController.getAll) + + router + .route('/') + .post(verifyToken.verifyAccessToken,verifyAccess(), NotificationErrorController.create) router .route('/:id') - .get(verifyToken.verifyAccessToken, NotificationErrorController.getById) + .get(verifyToken.verifyAccessToken, verifyAccess(), NotificationErrorController.getById) module.exports = router; diff --git a/services/notification_error.service.js b/services/notification_error.service.js index 4053325..0c22909 100644 --- a/services/notification_error.service.js +++ b/services/notification_error.service.js @@ -1,6 +1,7 @@ const { getAllNotificationDb, getNotificationByIdDb, + InsertNotificationErrorDb, } = require('../db/notification_error.db'); const { @@ -40,6 +41,18 @@ class NotificationService { } } + static async createNotificationError(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await InsertNotificationErrorDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + // Get notification by ID static async getNotificationById(id) { try { diff --git a/validate/notification.schema.js b/validate/notification.schema.js index 38b55fb..683033f 100644 --- a/validate/notification.schema.js +++ b/validate/notification.schema.js @@ -13,6 +13,8 @@ const insertNotificationSchema = Joi.object({ "number.base": "error_code_id must be a number", }), + message_error_issue: Joi.string().max(255).optional(), + is_send: Joi.boolean().required().messages({ "any.required": "is_send is required", "boolean.base": "is_send must be a boolean", From 2b93baa64886af8786c41178915a5ee35694ccc4 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Tue, 9 Dec 2025 16:30:19 +0700 Subject: [PATCH 25/37] add: crud notif error user --- .../notification_error_user.controller.js | 71 ++++++++++++ db/notification_error_user.db.js | 105 ++++++++++++++++++ routes/index.js | 2 + routes/notification_error_user.route.js | 17 +++ services/notification_error_user.service.js | 88 +++++++++++++++ validate/notification_error_user.schema.js | 44 ++++++++ 6 files changed, 327 insertions(+) create mode 100644 controllers/notification_error_user.controller.js create mode 100644 db/notification_error_user.db.js create mode 100644 routes/notification_error_user.route.js create mode 100644 services/notification_error_user.service.js create mode 100644 validate/notification_error_user.schema.js diff --git a/controllers/notification_error_user.controller.js b/controllers/notification_error_user.controller.js new file mode 100644 index 0000000..6855991 --- /dev/null +++ b/controllers/notification_error_user.controller.js @@ -0,0 +1,71 @@ +const NotificationErrorUserService = require('../services/notification_error_user.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertNotificationErrorUserSchema, updateNotificationErrorUserSchema } = require('../validate/notification_error_user.schema'); + +class NotificationErrorUserController { + // Get all NotificationErrorUser + static async getAll(req, res) { + const queryParams = req.query; + + const results = await NotificationErrorUserService.getAllNotificationErrorUser(queryParams); + const response = await setResponsePaging(queryParams, results, 'Notification Error User found') + + res.status(response.statusCode).json(response); + } + + // Get NotificationErrorUser by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await NotificationErrorUserService.getNotificationErrorUserById(id); + const response = await setResponse(results, 'Notification Error User found') + + res.status(response.statusCode).json(response); + } + + // Create NotificationErrorUser + static async create(req, res) { + const { error, value } = await checkValidate(insertNotificationErrorUserSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await NotificationErrorUserService.createNotificationErrorUser(value); + const response = await setResponse(results, 'Notification Error User created successfully') + + return res.status(response.statusCode).json(response); + } + + // Update NotificationErrorUser + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateNotificationErrorUserSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await NotificationErrorUserService.updateNotificationErrorUser(id, value); + const response = await setResponse(results, 'Contact updated successfully') + + res.status(response.statusCode).json(response); + } + + // Soft delete contact + static async delete(req, res) { + const { id } = req.params; + + const results = await NotificationErrorUserService.deleteNotificationErrorUser(id, req.user.user_id); + const response = await setResponse(results, 'Contact deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = NotificationErrorUserController; diff --git a/db/notification_error_user.db.js b/db/notification_error_user.db.js new file mode 100644 index 0000000..5893d1c --- /dev/null +++ b/db/notification_error_user.db.js @@ -0,0 +1,105 @@ +const pool = require("../config"); + +// Get all Notification +const getAllNotificationErrorUserDb = 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( + [ + "a.notification_error_id", + "a.contact_id", + ], + searchParams.criteria, + queryParams + ); + + if (whereParamOr) queryParams = whereParamOr; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.notification_error_id", param: searchParams.name, type: "int" }, + { column: "a.contact_id", param: searchParams.code, type: "int" }, + ], + queryParams + ); + + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.* + FROM notification_error_user a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ORDER BY a.notification_error_user_id ASC + ${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 getNotificationErrorUserByIdDb = async (id) => { + const queryText = ` + SELECT + a.* + FROM notification_error_user a + WHERE a.notification_error_user_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +const createNotificationErrorUserDb = async (store) => { + const { query: queryText, values } = pool.buildDynamicInsert("notification_error_user", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset?.[0]?.inserted_id; + + return insertedId ? await getNotificationErrorUserByIdDb(insertedId) : null; +}; + +const updateNotificationErrorUserDb = async (id, data) => { + const store = { ...data }; + const whereData = { notification_error_user_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "notification_error_user", + store, + whereData + ); + + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getNotificationErrorUserByIdDb(id); +}; + +// Soft delete tag +const deleteNotificationErrorUserDb = async (id, deletedBy) => { + const queryText = ` + UPDATE notification_error_user + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE notification_error_user_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllNotificationErrorUserDb, + getNotificationErrorUserByIdDb, + createNotificationErrorUserDb, + updateNotificationErrorUserDb, + deleteNotificationErrorUserDb, +}; diff --git a/routes/index.js b/routes/index.js index e6121bf..fabe125 100644 --- a/routes/index.js +++ b/routes/index.js @@ -18,6 +18,7 @@ const notificationError = require("./notification_error.route") const notificationErrorSparepart = require("./notification_error_sparepart.route") const sparepart = require("./sparepart.route") const notificationErrorLog = require("./notification_error_log.route") +const notificationErrorUser = require("./notification_error_user.route") const errorCode = require("./error_code.route") router.use("/auth", auth); @@ -39,6 +40,7 @@ router.use("/notification", notificationError) router.use("/notification-sparepart", notificationErrorSparepart) router.use("/sparepart", sparepart) router.use("/notification-log", notificationErrorLog) +router.use("/notification-user", notificationErrorUser) router.use("/error-code", errorCode) module.exports = router; diff --git a/routes/notification_error_user.route.js b/routes/notification_error_user.route.js new file mode 100644 index 0000000..473a0a7 --- /dev/null +++ b/routes/notification_error_user.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const NotificationErrorUserController = require('../controllers/notification_error_user.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyAccess = require("../middleware/verifyAccess") + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, NotificationErrorUserController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), NotificationErrorUserController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, NotificationErrorUserController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), NotificationErrorUserController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), NotificationErrorUserController.delete); + +module.exports = router; \ No newline at end of file diff --git a/services/notification_error_user.service.js b/services/notification_error_user.service.js new file mode 100644 index 0000000..f589a7a --- /dev/null +++ b/services/notification_error_user.service.js @@ -0,0 +1,88 @@ +const { + getAllNotificationErrorUserDb, + getNotificationErrorUserByIdDb, + createNotificationErrorUserDb, + updateNotificationErrorUserDb, + deleteNotificationErrorUserDb +} = require('../db/notification_error_user.db'); +const { ErrorHandler } = require('../helpers/error'); + +class NotificationErrorUserService { + // Get all Contact + static async getAllNotificationErrorUser(param) { + try { + const results = await getAllNotificationErrorUserDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get NotificationErrorUser by ID + static async getNotificationErrorUserById(id) { + try { + const result = await getNotificationErrorUserByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'NotificationErrorUser not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create NotificationErrorUser + static async createNotificationErrorUser(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await createNotificationErrorUserDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update NotificationErrorUser + static async updateNotificationErrorUser(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getNotificationErrorUserByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'NotificationErrorUser not found'); + } + + const result = await updateNotificationErrorUserDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete NotificationErrorUser + static async deleteNotificationErrorUser(id, userId) { + try { + const dataExist = await getNotificationErrorUserByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'NotificationErrorUser not found'); + } + + const result = await deleteNotificationErrorUserDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = NotificationErrorUserService; diff --git a/validate/notification_error_user.schema.js b/validate/notification_error_user.schema.js new file mode 100644 index 0000000..5aae2ce --- /dev/null +++ b/validate/notification_error_user.schema.js @@ -0,0 +1,44 @@ +const Joi = require("joi"); + +// ======================== +// Insert Notification Error Schema +// ======================== +const insertNotificationErrorUserSchema = Joi.object({ + notification_error_id: Joi.number().required().messages({ + "any.required": "notification_error_id is required", + "number.base": "notification_error_id must be a number", + }), + + contact_id: Joi.number().required().messages({ + "any.required": "contact_id is required", + "number.base": "contact_id must be a number", + }), + + is_send: Joi.boolean().required().messages({ + "any.required": "is_send is required", + "boolean.base": "is_send must be a boolean", + }), +}); + +// ======================== +// Update Notification Error Schema +// ======================== +const updateNotificationErrorUserSchema = Joi.object({ + notification_error_id: Joi.number().optional().messages({ + "number.base": "notification_error_id must be a number", + }), + + contact_id: Joi.number().required().messages({ + "any.required": "contact_id is required", + "number.base": "contact_id must be a number", + }), + + is_send: Joi.boolean().optional().messages({ + "boolean.base": "is_send must be a boolean", + }), +}); + +module.exports = { + insertNotificationErrorUserSchema, + updateNotificationErrorUserSchema, +}; From 907f5767c1eb93be6ac443b4d52db57d8c4546dc Mon Sep 17 00:00:00 2001 From: Fachba Date: Thu, 18 Dec 2025 10:05:50 +0700 Subject: [PATCH 26/37] notification wa by mqtt broker --- app.js | 9 ++- config/index.js | 33 ++++++++++ controllers/auth.controller.js | 38 +++++++++++ db/contact.db.js | 1 + db/history_value.db.js | 2 +- db/notification_wa.db.js | 59 +++++++++++++++++ routes/auth.route.js | 1 + services/notifikasi-wa.service.js | 105 ++++++++++++++++++++++++++++++ validate/contact.schema.js | 4 +- 9 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 db/notification_wa.db.js create mode 100644 services/notifikasi-wa.service.js diff --git a/app.js b/app.js index c83b7ff..67ee5fe 100644 --- a/app.js +++ b/app.js @@ -8,7 +8,8 @@ const helmet = require("helmet"); const compression = require("compression"); const unknownEndpoint = require("./middleware/unKnownEndpoint"); const { handleError } = require("./helpers/error"); -const { checkConnection } = require("./config"); +const { checkConnection, mqttClient } = require("./config"); +const { onNotification } = require("./services/notifikasi-wa.service"); const app = express(); @@ -47,4 +48,10 @@ app.get("/check-db", async (req, res) => { app.use(unknownEndpoint); app.use(handleError); +// Saat pesan diterima +mqttClient.on('message', (topic, message) => { + console.log(`Received message on topic "${topic}":`, message.toString()); + onNotification(topic, message); +}); + module.exports = app; diff --git a/config/index.js b/config/index.js index 15c4e5a..116d59e 100644 --- a/config/index.js +++ b/config/index.js @@ -1,8 +1,11 @@ require("dotenv").config(); +const { default: mqtt } = require("mqtt"); const sql = require("mssql"); const isProduction = process.env.NODE_ENV === "production"; +const endPointWhatsapp = process.env.ENDPOINT_WHATSAPP; + // Config SQL Server const config = { user: process.env.SQL_USERNAME, @@ -284,6 +287,34 @@ async function generateKode(prefix, tableName, columnName) { return prefix + String(nextNumber).padStart(3, "0"); } +// Koneksi ke broker MQTT +const mqttOptions = { + clientId: 'express_mqtt_client_' + Math.random().toString(16).substr(2, 8), + clean: true, + connectTimeout: 4000, + username: '', // jika ada + password: '', // jika ada +}; + +const mqttUrl = 'ws://localhost:1884'; // Ganti dengan broker kamu +const topic = 'morek'; + +const mqttClient = mqtt.connect(mqttUrl, mqttOptions); + +// Saat terkoneksi +mqttClient.on('connect', () => { + console.log('MQTT connected'); + + // Subscribe ke topik tertentu + mqttClient.subscribe(topic, (err) => { + if (!err) { + console.log(`Subscribed to topic "${topic}"`); + } else { + console.error('Subscribe error:', err); + } + }); +}); + module.exports = { checkConnection, query, @@ -293,4 +324,6 @@ module.exports = { buildDynamicInsert, buildDynamicUpdate, generateKode, + endPointWhatsapp, + mqttClient }; diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index de1a9dd..1c75f84 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -2,6 +2,9 @@ const AuthService = require('../services/auth.service'); const { setResponse, checkValidate } = require('../helpers/utils'); const { registerSchema, loginSchema } = require('../validate/auth.schema'); const { createCaptcha } = require('../utils/captcha'); +const JWTService = require('../utils/jwt'); + +const CryptoJS = require('crypto-js'); class AuthController { // Register @@ -94,6 +97,41 @@ class AuthController { const response = await setResponse({ svg, text }, 'Captcha generated'); res.status(response.statusCode).json(response); } + + static async verifyTokenRedirect(req, res) { + const { tokenRedirect } = req.body; + + const bytes = CryptoJS.AES.decrypt(tokenRedirect, process.env.VITE_KEY_SESSION); + const decrypted = JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); + + const userPhone = decrypted?.user_phone + const userName = decrypted?.user_name + const idData = decrypted?.id + + const payload = { + user_id: userPhone, + user_fullname: userName, + }; + + const tokens = JWTService.generateTokenPair(payload); + + // Simpan refresh token di cookie + res.cookie('refreshToken', tokens.refreshToken, { + httpOnly: true, + secure: false, + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000 + }); + + const response = await setResponse( + { + accessToken: tokens.accessToken + }, + 'Verify successful' + ); + + res.status(response.statusCode).json(response); + } } module.exports = AuthController; diff --git a/db/contact.db.js b/db/contact.db.js index f8c3b65..2b1cff5 100644 --- a/db/contact.db.js +++ b/db/contact.db.js @@ -24,6 +24,7 @@ const getAllContactDb = async (searchParams = {}) => { [ { column: "a.contact_name", param: searchParams.name, type: "string" }, { column: "a.contact_type", param: searchParams.code, type: "string" }, + { column: "a.is_active", param: searchParams.active, type: "boolean" }, ], queryParams ); diff --git a/db/history_value.db.js b/db/history_value.db.js index 41032b5..aea197d 100644 --- a/db/history_value.db.js +++ b/db/history_value.db.js @@ -349,7 +349,7 @@ const getHistoryValueTrendingPivotDb = async (tableName, searchParams = {}) => { const tagList = Object.keys(rows[0]).filter(k => k !== timeKey); const nivoData = tagList.map(tag => ({ - id: tag, + name: tag, data: rows.map(row => ({ x: row[timeKey], y: row[tag] !== null ? Number(row[tag]) : null diff --git a/db/notification_wa.db.js b/db/notification_wa.db.js new file mode 100644 index 0000000..aa2d368 --- /dev/null +++ b/db/notification_wa.db.js @@ -0,0 +1,59 @@ +const { default: axios } = require('axios'); +const CryptoJS = require('crypto-js'); + +const generateTokenRedirect = async (userPhone, userName, id) => { + + const plain = { + user_phone: userPhone, + user_name: userName, + id + } + + const tokenCrypt = CryptoJS.AES.encrypt(JSON.stringify(plain), process.env.VITE_KEY_SESSION).toString(); + return tokenCrypt +} + +const shortUrltiny = async (encodedToken) => { + const url = `${process.env.ENDPOINT_FE}/redirect?token=${encodedToken}` + + const encodedUrl = encodeURIComponent(url); // ⬅️ Encode dulu! + + const response = await axios.get(`https://tinyurl.com/api-create.php?url=${encodedUrl}`); + + let shortUrl = response.data; + if (!shortUrl.startsWith('http')) { + shortUrl = 'https://' + shortUrl; + } + + return shortUrl +} + +const sendNotifikasi = async (phone, message) => { + const payload = { + phone: phone, + message: message + }; + + // console.log('payload', payload); + + const endPointWhatsapp = process.env.ENDPOINT_WHATSAPP; + + const response = await axios.post(endPointWhatsapp, payload); + // console.log('response', response); + + try { + const response = await axios.post(endPointWhatsapp, payload); + // console.log(response.data); + return response?.data + } catch (error) { + // console.error(error.response?.data || error.message); + return error.response?.data || error.message + } + +}; + +module.exports = { + generateTokenRedirect, + shortUrltiny, + sendNotifikasi, +}; diff --git a/routes/auth.route.js b/routes/auth.route.js index 166d68d..e84b420 100644 --- a/routes/auth.route.js +++ b/routes/auth.route.js @@ -7,5 +7,6 @@ router.post('/login', AuthController.login); router.post('/register', AuthController.register); router.get('/generate-captcha', AuthController.generateCaptcha); router.post('/refresh-token', AuthController.refreshToken); +router.post('/verify-redirect', AuthController.verifyTokenRedirect); module.exports = router; \ No newline at end of file diff --git a/services/notifikasi-wa.service.js b/services/notifikasi-wa.service.js new file mode 100644 index 0000000..210b454 --- /dev/null +++ b/services/notifikasi-wa.service.js @@ -0,0 +1,105 @@ +const { getAllContactDb } = require('../db/contact.db'); +const { InsertNotificationErrorDb } = require('../db/notification_error.db'); +const { createNotificationErrorUserDb, updateNotificationErrorUserDb } = require('../db/notification_error_user.db'); +const { generateTokenRedirect, shortUrltiny, sendNotifikasi } = require('../db/notification_wa.db'); + +class NotifikasiWaService { + + async onNotification(topic, message) { + + try { + const paramDb = { + limit: 100, + page: 1, + criteria: '', + active: 1 + } + + // const chanel = { + // "time": "2025-12-11 11:10:58", + // "c_4501": 4, + // "c_5501": 3, + // "c_6501": 0 + // } + + if (topic === 'morek') { + + const dataMqtt = JSON.parse(message); + + const resultChanel = []; + + Object.entries(dataMqtt).forEach(([key, value]) => { + if (key.startsWith('c_')) { + resultChanel.push({ + chanel_id: Number(key.slice(2)), + value + }); + } + }); + + const results = await getAllContactDb(paramDb); + + const bodyMessage = `Hai Operator\n` + + `Terjadi peringatan pada device, silahkan cek detail pada link berikut :\n`; + + const dataUsers = results.data; + + for (const chanel of resultChanel) { + const data = { + "error_code_id": chanel.value, + "error_chanel": chanel.chanel_id, + "message_error_issue": bodyMessage, + "is_send": false, + "is_delivered": false, + "is_read": false, + "is_active": true + } + + const resultNotificationError = await InsertNotificationErrorDb(data) + + for (const dataUser of dataUsers) { + if (dataUser.is_active) { + + const param = { + idData: resultNotificationError.notification_error_id, + userPhone: dataUser.contact_phone, + userName: dataUser.contact_name, + bodyMessage: bodyMessage, + } + + const tokenRedirect = await generateTokenRedirect(param.userPhone, param.userName, param.idData) + + const encodedToken = encodeURIComponent(tokenRedirect); + + const shortUrl = await shortUrltiny(encodedToken) + + let bodyWithUrl = `${param.bodyMessage}\n🔗 ${shortUrl}`; + + param.bodyMessage = bodyWithUrl + + const resultNotificationErrorUser = await createNotificationErrorUserDb({ + notification_error_id: resultNotificationError.notification_error_id, + contact_phone: param.userPhone, + contact_name: param.userName, + is_send: false, + }); + + const resultSend = await sendNotifikasi(param.userPhone, param.bodyMessage); + + await updateNotificationErrorUserDb(resultNotificationErrorUser[0].notification_error_user_id, { + is_send: resultSend?.error ? false : true, + }); + } + } + } + } + + } catch (error) { + // throw new ErrorHandler(error.statusCode, error.message); + return error + } + } + +} + +module.exports = new NotifikasiWaService(); diff --git a/validate/contact.schema.js b/validate/contact.schema.js index db362e6..04aaa0a 100644 --- a/validate/contact.schema.js +++ b/validate/contact.schema.js @@ -13,7 +13,7 @@ const insertContactSchema = Joi.object({ "Phone number must be a valid Indonesian number in format +628XXXXXXXXX", }), is_active: Joi.boolean().required(), - contact_type: Joi.string().max(255).optional() + contact_type: Joi.string().max(255).optional().allow(null) }); const updateContactSchema = Joi.object({ @@ -26,7 +26,7 @@ const updateContactSchema = Joi.object({ "Phone number must be a valid Indonesian number in format +628XXXXXXXXX", }), is_active: Joi.boolean().optional(), - contact_type: Joi.string().max(255).optional() + contact_type: Joi.string().max(255).optional().allow(null) }); module.exports = { From f2c8c3818db4b61e22600643e02a73584fa73240 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 18 Dec 2025 10:51:19 +0700 Subject: [PATCH 27/37] repair: ErrorCode brand-device --- services/error_code.service.js | 2 ++ validate/error_code.schema.js | 1 + 2 files changed, 3 insertions(+) diff --git a/services/error_code.service.js b/services/error_code.service.js index 2576c38..1c2c68f 100644 --- a/services/error_code.service.js +++ b/services/error_code.service.js @@ -180,6 +180,7 @@ class ErrorCodeService { // Check if there are any error code fields to update const hasMainFieldUpdate = + data.error_code !== undefined || data.error_code_name !== undefined || data.error_code_description !== undefined || data.error_code_color !== undefined || @@ -188,6 +189,7 @@ class ErrorCodeService { if (hasMainFieldUpdate) { await updateErrorCodeDb(brandId, existingErrorCode.error_code, { + error_code: data.error_code, error_code_name: data.error_code_name, error_code_description: data.error_code_description, error_code_color: data.error_code_color, diff --git a/validate/error_code.schema.js b/validate/error_code.schema.js index 957b718..0b53de1 100644 --- a/validate/error_code.schema.js +++ b/validate/error_code.schema.js @@ -44,6 +44,7 @@ const insertErrorCodeSchema = Joi.object({ }); const updateErrorCodeSchema = Joi.object({ + error_code: Joi.string().max(100).optional(), error_code_name: Joi.string().max(100).optional(), error_code_description: Joi.string().optional().allow(""), error_code_color: Joi.string().optional().allow(""), From 1aa7b1bc08beaaded082be3c19ba9fa6398202d0 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Thu, 18 Dec 2025 11:39:24 +0700 Subject: [PATCH 28/37] add: reader in notification detail & update notification --- controllers/notification_error.controller.js | 17 ++++++++ .../notification_error_user.controller.js | 6 +-- db/notification_error.db.js | 42 ++++++++++++++++++- routes/notification_error.route.js | 1 + services/notification_error.service.js | 28 ++++++++++++- 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/controllers/notification_error.controller.js b/controllers/notification_error.controller.js index a710ab4..548d8af 100644 --- a/controllers/notification_error.controller.js +++ b/controllers/notification_error.controller.js @@ -53,6 +53,23 @@ class NotificationErrorController { return res.status(response.statusCode).json(response); } + + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateNotificationSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await NotificationErrorService.updateNotificationError(id, value); + const response = await setResponse(results, 'Notification Error User updated successfully') + + res.status(response.statusCode).json(response); + } } module.exports = NotificationErrorController; diff --git a/controllers/notification_error_user.controller.js b/controllers/notification_error_user.controller.js index 6855991..c2b4342 100644 --- a/controllers/notification_error_user.controller.js +++ b/controllers/notification_error_user.controller.js @@ -52,17 +52,17 @@ class NotificationErrorUserController { value.userId = req.user.user_id const results = await NotificationErrorUserService.updateNotificationErrorUser(id, value); - const response = await setResponse(results, 'Contact updated successfully') + const response = await setResponse(results, 'Notification Error User updated successfully') res.status(response.statusCode).json(response); } - // Soft delete contact + // Soft delete Notification Error User static async delete(req, res) { const { id } = req.params; const results = await NotificationErrorUserService.deleteNotificationErrorUser(id, req.user.user_id); - const response = await setResponse(results, 'Contact deleted successfully') + const response = await setResponse(results, 'Notification Error User deleted successfully') res.status(response.statusCode).json(response); } diff --git a/db/notification_error.db.js b/db/notification_error.db.js index d7b9220..cdb98fc 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -22,6 +22,7 @@ const getNotificationByIdDb = async (id) => { return result.recordset[0]; }; + const getAllNotificationDb = async (searchParams = {}) => { let queryParams = []; @@ -122,9 +123,48 @@ const getAllNotificationDb = async (searchParams = {}) => { return { data: result.recordset, total }; }; +const updateNotificationErrorDb = async (id, data) => { + const store = { ...data }; + const whereData = { notification_error_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "notification_error", + store, + whereData + ); + + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getNotificationByIdDb(id); +}; + +const getReaderNotificationErrorDb = async (id) => { + const queryText = ` + SELECT + a.notification_error_user_id, + a.contact_phone, + a.contact_name, + a.is_send, + b.notification_error_id, + b.error_code_id + + FROM notification_error_user a + + LEFT JOIN notification_error b ON a.notification_error_id = b.notification_error_id + + WHERE a.notification_error_id = $1 + AND a.is_send = 1 + AND a.deleted_at IS NULL + `; + + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + module.exports = { getNotificationByIdDb, getAllNotificationDb, - InsertNotificationErrorDb + InsertNotificationErrorDb, + updateNotificationErrorDb, + getReaderNotificationErrorDb }; diff --git a/routes/notification_error.route.js b/routes/notification_error.route.js index 20336cd..d8ab014 100644 --- a/routes/notification_error.route.js +++ b/routes/notification_error.route.js @@ -16,5 +16,6 @@ router router .route('/:id') .get(verifyToken.verifyAccessToken, verifyAccess(), NotificationErrorController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), NotificationErrorController.update) module.exports = router; diff --git a/services/notification_error.service.js b/services/notification_error.service.js index 0c22909..61554a5 100644 --- a/services/notification_error.service.js +++ b/services/notification_error.service.js @@ -2,6 +2,8 @@ const { getAllNotificationDb, getNotificationByIdDb, InsertNotificationErrorDb, + getReaderNotificationErrorDb, + updateNotificationErrorDb, } = require('../db/notification_error.db'); const { @@ -62,6 +64,8 @@ class NotificationService { throw new ErrorHandler(404, 'Notification not found'); } + const readerNotification = (await getReaderNotificationErrorDb(id))|| []; + // Get error code details if error_code_id exists if (notification.error_code_id) { const errorCode = await getErrorCodeByIdDb(notification.error_code_id); @@ -70,7 +74,7 @@ class NotificationService { // Get solutions for this error code const solutions = (await getSolutionsByErrorCodeIdDb(errorCode.error_code_id)) || []; - const spareparts = await getSparepartsByErrorCodeIdDb(errorCode.error_code_id); + const spareparts = (await getSparepartsByErrorCodeIdDb(errorCode.error_code_id)) || []; const solutionsWithDetails = await Promise.all( solutions.map(async (solution) => { @@ -94,7 +98,7 @@ class NotificationService { notification.error_code = { ...errorCode, solution: solutionsWithDetails, - spareparts: spareparts + spareparts: spareparts, }; } } @@ -102,6 +106,8 @@ class NotificationService { // Get activity logs for this notification const notificationLogs = (await getNotificationErrorLogByNotificationErrorIdDb(id)) || []; + notification.reader = readerNotification; + notification.activity_logs = notificationLogs; return notification; @@ -109,6 +115,24 @@ class NotificationService { throw new ErrorHandler(error.statusCode, error.message); } } + + static async updateNotificationError(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getNotificationByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'NotificationErrorUser not found'); + } + + const result = await updateNotificationErrorDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } } module.exports = NotificationService; From dd5e1cc71375f96eae6111b79ab0b435469ef145 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Thu, 18 Dec 2025 12:43:44 +0700 Subject: [PATCH 29/37] add: api is read & not read in notification --- controllers/notification_error.controller.js | 30 +++ db/notification_error.db.js | 190 ++++++++++++++++++- routes/notification_error.route.js | 8 + services/notification_error.service.js | 28 +++ validate/notification.schema.js | 4 - 5 files changed, 253 insertions(+), 7 deletions(-) diff --git a/controllers/notification_error.controller.js b/controllers/notification_error.controller.js index 548d8af..472b81b 100644 --- a/controllers/notification_error.controller.js +++ b/controllers/notification_error.controller.js @@ -25,6 +25,36 @@ class NotificationErrorController { res.status(response.statusCode).json(response); } + static async getAllIsRead(req, res) { + const queryParams = req.query; + + const results = await NotificationErrorService.getAllNotificationIsRead( + queryParams + ); + const response = await setResponsePaging( + queryParams, + results, + "Notification found" + ); + + res.status(response.statusCode).json(response); + } + + static async getAllIsNotRead(req, res) { + const queryParams = req.query; + + const results = await NotificationErrorService.getAllNotificationIsNotRead( + queryParams + ); + const response = await setResponsePaging( + queryParams, + results, + "Notification found" + ); + + res.status(response.statusCode).json(response); + } + static async getById(req, res) { const { id } = req.params; diff --git a/db/notification_error.db.js b/db/notification_error.db.js index cdb98fc..9e15c37 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -144,15 +144,13 @@ const getReaderNotificationErrorDb = async (id) => { a.contact_phone, a.contact_name, a.is_send, - b.notification_error_id, - b.error_code_id + b.message_error_issue FROM notification_error_user a LEFT JOIN notification_error b ON a.notification_error_id = b.notification_error_id WHERE a.notification_error_id = $1 - AND a.is_send = 1 AND a.deleted_at IS NULL `; @@ -160,11 +158,197 @@ const getReaderNotificationErrorDb = async (id) => { return result.recordset; }; +const getAllNotificationIsReadDb = 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.error_code", + "b.error_code_name", + "c.solution_name", + "COALESCE(a.is_send, 0)", + "COALESCE(a.is_delivered, 0)", + "COALESCE(a.is_read, 0)", + "COALESCE(a.is_active, 0)", + ], + searchParams.criteria, + queryParams + ); + if (whereParamOr) queryParams = whereParamOr; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "COALESCE(a.is_send, 0)", param: searchParams.is_send, type: "number" }, + { column: "COALESCE(a.is_delivered, 0)", param: searchParams.is_delivered, type: "number" }, + { column: "COALESCE(a.is_read, 0)", param: searchParams.is_read, type: "number" }, + { column: "COALESCE(a.is_active, 0)", param: searchParams.is_active, type: "number" }, + ], + queryParams + ); + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + + a.notification_error_id, + a.error_code_id, + a.message_error_issue, + a.is_send, + a.is_delivered, + a.is_read, + a.is_active, + + b.error_code, + b.error_code_name, + b.error_code_color, + b.path_icon, + b.created_at, + + c.solution_name, + c.type_solution, + c.path_solution, + + d.device_name, + d.device_location, + + COALESCE(d.device_name, '') + ' - ' + COALESCE(b.error_code_name, '') AS device_name_error + + FROM notification_error a + + LEFT JOIN brand_code b + ON a.error_code_id = b.error_code_id AND b.deleted_at IS NULL + + LEFT JOIN brand_code_solution c + ON b.error_code_id = c.error_code_id AND c.deleted_at IS NULL + + LEFT JOIN m_device d + ON b.brand_id = d.brand_id AND d.deleted_at IS NULL + + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + AND a.is_read = 1 + + ORDER BY a.notification_error_id 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, 5) + : 0; + + return { data: result.recordset, total }; +}; + +const getAllNotificationIsNotReadDb = 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.error_code", + "b.error_code_name", + "c.solution_name", + "COALESCE(a.is_send, 0)", + "COALESCE(a.is_delivered, 0)", + "COALESCE(a.is_read, 0)", + "COALESCE(a.is_active, 0)", + ], + searchParams.criteria, + queryParams + ); + if (whereParamOr) queryParams = whereParamOr; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "COALESCE(a.is_send, 0)", param: searchParams.is_send, type: "number" }, + { column: "COALESCE(a.is_delivered, 0)", param: searchParams.is_delivered, type: "number" }, + { column: "COALESCE(a.is_read, 0)", param: searchParams.is_read, type: "number" }, + { column: "COALESCE(a.is_active, 0)", param: searchParams.is_active, type: "number" }, + ], + queryParams + ); + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + + a.notification_error_id, + a.error_code_id, + a.message_error_issue, + a.is_send, + a.is_delivered, + a.is_read, + a.is_active, + + b.error_code, + b.error_code_name, + b.error_code_color, + b.path_icon, + b.created_at, + + c.solution_name, + c.type_solution, + c.path_solution, + + d.device_name, + d.device_location, + + COALESCE(d.device_name, '') + ' - ' + COALESCE(b.error_code_name, '') AS device_name_error + + FROM notification_error a + + LEFT JOIN brand_code b + ON a.error_code_id = b.error_code_id AND b.deleted_at IS NULL + + LEFT JOIN brand_code_solution c + ON b.error_code_id = c.error_code_id AND c.deleted_at IS NULL + + LEFT JOIN m_device d + ON b.brand_id = d.brand_id AND d.deleted_at IS NULL + + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + AND a.is_read = 0 + + ORDER BY a.notification_error_id 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, 5) + : 0; + + return { data: result.recordset, total }; +}; module.exports = { getNotificationByIdDb, getAllNotificationDb, + getAllNotificationIsReadDb, + getAllNotificationIsNotReadDb, InsertNotificationErrorDb, updateNotificationErrorDb, getReaderNotificationErrorDb + }; diff --git a/routes/notification_error.route.js b/routes/notification_error.route.js index d8ab014..8f3854f 100644 --- a/routes/notification_error.route.js +++ b/routes/notification_error.route.js @@ -9,6 +9,14 @@ router .route('/') .get(verifyToken.verifyAccessToken,verifyAccess(), NotificationErrorController.getAll) +router + .route('/read') + .get(verifyToken.verifyAccessToken,verifyAccess(), NotificationErrorController.getAllIsRead) + +router + .route('/not-ready') + .get(verifyToken.verifyAccessToken,verifyAccess(), NotificationErrorController.getAllIsNotRead) + router .route('/') .post(verifyToken.verifyAccessToken,verifyAccess(), NotificationErrorController.create) diff --git a/services/notification_error.service.js b/services/notification_error.service.js index 61554a5..6230f6c 100644 --- a/services/notification_error.service.js +++ b/services/notification_error.service.js @@ -1,5 +1,7 @@ const { getAllNotificationDb, + getAllNotificationIsReadDb, + getAllNotificationIsNotReadDb, getNotificationByIdDb, InsertNotificationErrorDb, getReaderNotificationErrorDb, @@ -43,6 +45,32 @@ class NotificationService { } } + static async getAllNotificationIsRead(param) { + try { + const results = await getAllNotificationIsReadDb(param); + + results.data.map(element => { + }); + + return results; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + static async getAllNotificationIsNotRead(param) { + try { + const results = await getAllNotificationIsNotReadDb(param); + + results.data.map(element => { + }); + + return results; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + static async createNotificationError(data) { try { if (!data || typeof data !== 'object') data = {}; diff --git a/validate/notification.schema.js b/validate/notification.schema.js index 683033f..e4796fb 100644 --- a/validate/notification.schema.js +++ b/validate/notification.schema.js @@ -40,10 +40,6 @@ const insertNotificationSchema = Joi.object({ // Update Notification Schema // ======================== const updateNotificationSchema = Joi.object({ - error_code_id: Joi.number().optional().messages({ - "number.base": "error_code_id must be a number", - }), - is_send: Joi.boolean().optional().messages({ "boolean.base": "is_send must be a boolean", }), From 5e7de6d144cf00c5be0948640d107af65c485d36 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Thu, 18 Dec 2025 13:04:57 +0700 Subject: [PATCH 30/37] delete: api is ready & not read --- controllers/notification_error.controller.js | 29 --- db/notification_error.db.js | 216 ++----------------- routes/notification_error.route.js | 8 - services/notification_error.service.js | 28 --- 4 files changed, 12 insertions(+), 269 deletions(-) diff --git a/controllers/notification_error.controller.js b/controllers/notification_error.controller.js index 472b81b..deb6d08 100644 --- a/controllers/notification_error.controller.js +++ b/controllers/notification_error.controller.js @@ -25,35 +25,6 @@ class NotificationErrorController { res.status(response.statusCode).json(response); } - static async getAllIsRead(req, res) { - const queryParams = req.query; - - const results = await NotificationErrorService.getAllNotificationIsRead( - queryParams - ); - const response = await setResponsePaging( - queryParams, - results, - "Notification found" - ); - - res.status(response.statusCode).json(response); - } - - static async getAllIsNotRead(req, res) { - const queryParams = req.query; - - const results = await NotificationErrorService.getAllNotificationIsNotRead( - queryParams - ); - const response = await setResponsePaging( - queryParams, - results, - "Notification found" - ); - - res.status(response.statusCode).json(response); - } static async getById(req, res) { const { id } = req.params; diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 9e15c37..03b85d5 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -26,15 +26,6 @@ const getNotificationByIdDb = async (id) => { const getAllNotificationDb = async (searchParams = {}) => { let queryParams = []; - const boolFields = ["is_send", "is_delivered", "is_read", "is_active"]; - - boolFields.forEach((f) => { - if (searchParams[f] !== undefined && searchParams[f] !== null && searchParams[f] !== "") { - const v = searchParams[f]; - searchParams[f] = v == "1" ? 1 : v == "0" ? 0 : null; - } - }); - if (searchParams.limit) { const page = Number(searchParams.page ?? 1) - 1; queryParams = [Number(searchParams.limit ?? 10), page]; @@ -42,13 +33,13 @@ const getAllNotificationDb = async (searchParams = {}) => { const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( [ + "a.message_error_issue", + "a.is_send", + "a.is_delivered", + "a.is_read", + "a.is_active", "b.error_code", "b.error_code_name", - "c.solution_name", - "COALESCE(a.is_send, 0)", - "COALESCE(a.is_delivered, 0)", - "COALESCE(a.is_read, 0)", - "COALESCE(a.is_active, 0)", ], searchParams.criteria, queryParams @@ -57,10 +48,13 @@ const getAllNotificationDb = async (searchParams = {}) => { const { whereConditions, whereParamAnd } = pool.buildFilterQuery( [ - { column: "COALESCE(a.is_send, 0)", param: searchParams.is_send, type: "number" }, - { column: "COALESCE(a.is_delivered, 0)", param: searchParams.is_delivered, type: "number" }, - { column: "COALESCE(a.is_read, 0)", param: searchParams.is_read, type: "number" }, - { column: "COALESCE(a.is_active, 0)", param: searchParams.is_active, type: "number" }, + { column: "a.message_error_issue", param: searchParams.message_error_issue, type: "string" }, + { column: "a.is_send", param: searchParams.is_send, type: "number" }, + { column: "a.is_delivered", param: searchParams.is_delivered, type: "number" }, + { column: "a.is_read", param: searchParams.is_read, type: "number" }, + { column: "a.is_active", param: searchParams.is_active, type: "number" }, + { column: "b.error_code", param: searchParams.error_code, type: "string" }, + { column: "b.error_code_name", param: searchParams.error_code_name, type: "string" }, ], queryParams ); @@ -158,195 +152,9 @@ const getReaderNotificationErrorDb = async (id) => { return result.recordset; }; -const getAllNotificationIsReadDb = 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.error_code", - "b.error_code_name", - "c.solution_name", - "COALESCE(a.is_send, 0)", - "COALESCE(a.is_delivered, 0)", - "COALESCE(a.is_read, 0)", - "COALESCE(a.is_active, 0)", - ], - searchParams.criteria, - queryParams - ); - if (whereParamOr) queryParams = whereParamOr; - - const { whereConditions, whereParamAnd } = pool.buildFilterQuery( - [ - { column: "COALESCE(a.is_send, 0)", param: searchParams.is_send, type: "number" }, - { column: "COALESCE(a.is_delivered, 0)", param: searchParams.is_delivered, type: "number" }, - { column: "COALESCE(a.is_read, 0)", param: searchParams.is_read, type: "number" }, - { column: "COALESCE(a.is_active, 0)", param: searchParams.is_active, type: "number" }, - ], - queryParams - ); - if (whereParamAnd) queryParams = whereParamAnd; - - const queryText = ` - SELECT - COUNT(*) OVER() AS total_data, - - a.notification_error_id, - a.error_code_id, - a.message_error_issue, - a.is_send, - a.is_delivered, - a.is_read, - a.is_active, - - b.error_code, - b.error_code_name, - b.error_code_color, - b.path_icon, - b.created_at, - - c.solution_name, - c.type_solution, - c.path_solution, - - d.device_name, - d.device_location, - - COALESCE(d.device_name, '') + ' - ' + COALESCE(b.error_code_name, '') AS device_name_error - - FROM notification_error a - - LEFT JOIN brand_code b - ON a.error_code_id = b.error_code_id AND b.deleted_at IS NULL - - LEFT JOIN brand_code_solution c - ON b.error_code_id = c.error_code_id AND c.deleted_at IS NULL - - LEFT JOIN m_device d - ON b.brand_id = d.brand_id AND d.deleted_at IS NULL - - WHERE a.deleted_at IS NULL - ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} - ${whereOrConditions ? ` ${whereOrConditions}` : ""} - AND a.is_read = 1 - - ORDER BY a.notification_error_id 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, 5) - : 0; - - return { data: result.recordset, total }; -}; - -const getAllNotificationIsNotReadDb = 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.error_code", - "b.error_code_name", - "c.solution_name", - "COALESCE(a.is_send, 0)", - "COALESCE(a.is_delivered, 0)", - "COALESCE(a.is_read, 0)", - "COALESCE(a.is_active, 0)", - ], - searchParams.criteria, - queryParams - ); - if (whereParamOr) queryParams = whereParamOr; - - const { whereConditions, whereParamAnd } = pool.buildFilterQuery( - [ - { column: "COALESCE(a.is_send, 0)", param: searchParams.is_send, type: "number" }, - { column: "COALESCE(a.is_delivered, 0)", param: searchParams.is_delivered, type: "number" }, - { column: "COALESCE(a.is_read, 0)", param: searchParams.is_read, type: "number" }, - { column: "COALESCE(a.is_active, 0)", param: searchParams.is_active, type: "number" }, - ], - queryParams - ); - if (whereParamAnd) queryParams = whereParamAnd; - - const queryText = ` - SELECT - COUNT(*) OVER() AS total_data, - - a.notification_error_id, - a.error_code_id, - a.message_error_issue, - a.is_send, - a.is_delivered, - a.is_read, - a.is_active, - - b.error_code, - b.error_code_name, - b.error_code_color, - b.path_icon, - b.created_at, - - c.solution_name, - c.type_solution, - c.path_solution, - - d.device_name, - d.device_location, - - COALESCE(d.device_name, '') + ' - ' + COALESCE(b.error_code_name, '') AS device_name_error - - FROM notification_error a - - LEFT JOIN brand_code b - ON a.error_code_id = b.error_code_id AND b.deleted_at IS NULL - - LEFT JOIN brand_code_solution c - ON b.error_code_id = c.error_code_id AND c.deleted_at IS NULL - - LEFT JOIN m_device d - ON b.brand_id = d.brand_id AND d.deleted_at IS NULL - - WHERE a.deleted_at IS NULL - ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} - ${whereOrConditions ? ` ${whereOrConditions}` : ""} - AND a.is_read = 0 - - ORDER BY a.notification_error_id 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, 5) - : 0; - - return { data: result.recordset, total }; -}; - module.exports = { getNotificationByIdDb, getAllNotificationDb, - getAllNotificationIsReadDb, - getAllNotificationIsNotReadDb, InsertNotificationErrorDb, updateNotificationErrorDb, getReaderNotificationErrorDb diff --git a/routes/notification_error.route.js b/routes/notification_error.route.js index 8f3854f..d8ab014 100644 --- a/routes/notification_error.route.js +++ b/routes/notification_error.route.js @@ -9,14 +9,6 @@ router .route('/') .get(verifyToken.verifyAccessToken,verifyAccess(), NotificationErrorController.getAll) -router - .route('/read') - .get(verifyToken.verifyAccessToken,verifyAccess(), NotificationErrorController.getAllIsRead) - -router - .route('/not-ready') - .get(verifyToken.verifyAccessToken,verifyAccess(), NotificationErrorController.getAllIsNotRead) - router .route('/') .post(verifyToken.verifyAccessToken,verifyAccess(), NotificationErrorController.create) diff --git a/services/notification_error.service.js b/services/notification_error.service.js index 6230f6c..61554a5 100644 --- a/services/notification_error.service.js +++ b/services/notification_error.service.js @@ -1,7 +1,5 @@ const { getAllNotificationDb, - getAllNotificationIsReadDb, - getAllNotificationIsNotReadDb, getNotificationByIdDb, InsertNotificationErrorDb, getReaderNotificationErrorDb, @@ -45,32 +43,6 @@ class NotificationService { } } - static async getAllNotificationIsRead(param) { - try { - const results = await getAllNotificationIsReadDb(param); - - results.data.map(element => { - }); - - return results; - } catch (error) { - throw new ErrorHandler(error.statusCode, error.message); - } - } - - static async getAllNotificationIsNotRead(param) { - try { - const results = await getAllNotificationIsNotReadDb(param); - - results.data.map(element => { - }); - - return results; - } catch (error) { - throw new ErrorHandler(error.statusCode, error.message); - } - } - static async createNotificationError(data) { try { if (!data || typeof data !== 'object') data = {}; From c112ff165afd83328da2b55e2c16f98ebd91c87c Mon Sep 17 00:00:00 2001 From: Albani Rajata Malik Date: Thu, 18 Dec 2025 13:26:48 +0700 Subject: [PATCH 31/37] update value report --- controllers/history_value.controller.js | 101 +++-- db/history_value.db.js | 479 +++++++++++++++++------- services/history_value.service.js | 141 ++++--- 3 files changed, 506 insertions(+), 215 deletions(-) diff --git a/controllers/history_value.controller.js b/controllers/history_value.controller.js index e4f3665..d4299f5 100644 --- a/controllers/history_value.controller.js +++ b/controllers/history_value.controller.js @@ -4,53 +4,102 @@ const { setResponsePaging } = require('../helpers/utils'); class HistoryValueController { static async getAllHistoryAlarm(req, res) { - const queryParams = req.query; + try { + const queryParams = req.query; - const results = await HistoryValue.getAllHistoryAlarm(queryParams); - const response = await setResponsePaging(queryParams, results, 'Data found'); + const results = await HistoryValue.getAllHistoryAlarm(queryParams); + const response = await setResponsePaging(queryParams, results, 'Data found'); - res.status(response.statusCode).json(response); + res.status(response.statusCode).json(response); + } catch (error) { + const statusCode = error.statusCode || 500; + res.status(statusCode).json({ + success: false, + statusCode, + message: error.message || 'Internal server error' + }); + } } static async getAllHistoryEvent(req, res) { - const queryParams = req.query; + try { + const queryParams = req.query; - const results = await HistoryValue.getAllHistoryEvent(queryParams); - const response = await setResponsePaging(queryParams, results, 'Data found'); + const results = await HistoryValue.getAllHistoryEvent(queryParams); + const response = await setResponsePaging(queryParams, results, 'Data found'); - res.status(response.statusCode).json(response); + res.status(response.statusCode).json(response); + } catch (error) { + const statusCode = error.statusCode || 500; + res.status(statusCode).json({ + success: false, + statusCode, + message: error.message || 'Internal server error' + }); + } } static async getHistoryValueReport(req, res) { - const queryParams = req.query; + try { + const queryParams = req.query; - const results = await HistoryValue.getHistoryValueReport(queryParams); - const response = await setResponsePaging(queryParams, results, 'Data found'); + const results = await HistoryValue.getHistoryValueReport(queryParams); + const response = await setResponsePaging(queryParams, results, 'Data found'); - res.status(response.statusCode).json(response); + res.status(response.statusCode).json(response); + } catch (error) { + const statusCode = error.statusCode || 500; + res.status(statusCode).json({ + success: false, + statusCode, + message: error.message || 'Internal server error' + }); + } } - static async getHistoryValueReportPivot(req, res) { +static async getHistoryValueReportPivot(req, res) { + try { 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 + if (results.column) { + response.columns = results.column; + } res.status(response.statusCode).json(response); + } catch (error) { + const statusCode = error.statusCode || 500; + res.status(statusCode).json({ + success: false, + statusCode, + message: error.message || 'Internal server error' + }); } } -module.exports = HistoryValueController; + static async getHistoryValueTrendingPivot(req, res) { + try { + const queryParams = req.query; + + const results = await HistoryValue.getHistoryValueTrendingPivot(queryParams); + const response = await setResponsePaging(queryParams, results, 'Data found'); + + if (results.column) { + response.columns = results.column; + } + + res.status(response.statusCode).json(response); + } catch (error) { + const statusCode = error.statusCode || 500; + res.status(statusCode).json({ + success: false, + statusCode, + message: error.message || 'Internal server error' + }); + } + } +} + +module.exports = HistoryValueController; \ No newline at end of file diff --git a/db/history_value.db.js b/db/history_value.db.js index aea197d..ef55234 100644 --- a/db/history_value.db.js +++ b/db/history_value.db.js @@ -115,160 +115,324 @@ const getHistoryEventDb = async (searchParams = {}) => { }; 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; + try { + if (!tableName || !/^[a-zA-Z0-9_]+$/.test(tableName)) { + throw new Error('Invalid table name format'); + } + + const queryText = ` + SELECT TABLE_NAME, TABLE_SCHEMA, TABLE_TYPE + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_NAME = $1 + `; + + const result = await pool.query(queryText, [tableName]); + return result.recordset; + } catch (error) { + console.error('Error in checkTableNamedDb:', error); + throw error; + } }; const getHistoryValueReportDb = async (tableName, searchParams = {}) => { - let queryParams = []; + try { + if (!tableName || !/^[a-zA-Z0-9_]+$/.test(tableName)) { + throw new Error('Invalid table name format'); + } - if (searchParams.limit) { - const page = Number(searchParams.page ?? 1) - 1; - queryParams = [Number(searchParams.limit ?? 10), page]; - } + 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 - ); + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["b.tag_name", "CAST(a.tagnum AS VARCHAR)"], + searchParams.criteria, + queryParams + ); - if (whereParamOr) queryParams = whereParamOr; + 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 - ); + 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; + 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 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 result = await pool.query(queryText, queryParams); - const total = - result?.recordset?.length > 0 + const total = result.recordset?.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; - return { data: result.recordset, total }; + return { data: result.recordset, total }; + } catch (error) { + console.error('Error in getHistoryValueReportDb:', error); + throw error; + } }; 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); + try { + if (!tableName || !/^[a-zA-Z0-9_]+$/.test(tableName)) { + throw new Error('Invalid table name format'); + } - // --- Normalisasi tanggal - if (from.length === 10) from += ' 00:00:00'; - if (to.length === 10) to += ' 23:59:59'; + let from = searchParams.from || ''; + let to = searchParams.to || ''; + const interval = Math.max(1, Math.min(1440, Number(searchParams.interval ?? 10))); - // --- 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 (from.length === 10 && /^\d{4}-\d{2}-\d{2}$/.test(from)) { + from += ' 00:00:00'; + } + if (to.length === 10 && /^\d{4}-\d{2}-\d{2}$/.test(to)) { + to += ' 23:59:59'; + } - if (tags.recordset.length === 0) { - return { data: [], total: 0 }; + console.log('Table:', tableName); + console.log('From:', from, '| To:', to, '| Interval:', interval); + console.log('Filters:', searchParams); + + const dateRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; + if (!dateRegex.test(from) || !dateRegex.test(to)) { + throw new Error('Invalid date format. Expected: YYYY-MM-DD or YYYY-MM-DD HH:MM:SS'); + } + + const fromDate = new Date(from); + const toDate = new Date(to); + const daysDiff = (toDate - fromDate) / (1000 * 60 * 60 * 24); + + if (daysDiff > 365) { + throw new Error('Date range cannot exceed 1 year'); + } + if (daysDiff < 0) { + throw new Error('From date must be before to date'); + } + + let tagQueryParams = []; + let tagWhereConditions = []; + + if (searchParams.plant_sub_section_id) { + tagWhereConditions.push(`plant_sub_section_id = $${tagQueryParams.length + 1}`); + tagQueryParams.push(searchParams.plant_sub_section_id); + } + + if (searchParams.plant_section_id) { + tagWhereConditions.push(`plant_section_id = $${tagQueryParams.length + 1}`); + tagQueryParams.push(searchParams.plant_section_id); + } + + if (searchParams.name) { + const nameFilter = `(tag_name LIKE $${tagQueryParams.length + 1} OR CAST(tag_number AS VARCHAR) LIKE $${tagQueryParams.length + 2})`; + tagWhereConditions.push(nameFilter); + tagQueryParams.push(`%${searchParams.name}%`, `%${searchParams.name}%`); + } + + if (searchParams.criteria) { + const criteriaFilter = `(tag_name LIKE $${tagQueryParams.length + 1} OR CAST(tag_number AS VARCHAR) LIKE $${tagQueryParams.length + 2})`; + tagWhereConditions.push(criteriaFilter); + tagQueryParams.push(`%${searchParams.criteria}%`, `%${searchParams.criteria}%`); + } + + const tagWhereClause = tagWhereConditions.length > 0 + ? ` AND ${tagWhereConditions.join(" AND ")}` + : ''; + + const tagsQuery = ` + SELECT tag_name, tag_number + FROM m_tags + WHERE is_report = 1 AND deleted_at IS NULL + ${tagWhereClause} + ORDER BY tag_name + `; + + console.log('Tags Query:', tagsQuery); + console.log('Tags Query Params:', tagQueryParams); + + const tagsResult = await pool.query(tagsQuery, tagQueryParams); + + console.log('Tags found:', tagsResult.recordset.length); + + if (tagsResult.recordset.length === 0) { + return { data: [], column: '' }; + } + + const tagNames = tagsResult.recordset.map(r => `[${r.tag_name}]`).join(', '); + const tagNamesColumn = tagsResult.recordset.map(r => r.tag_name).join(', '); + const tagNumbers = tagsResult.recordset.map(r => r.tag_number); + + console.log('Filtered tag numbers:', tagNumbers); + console.log('Filtered tag names:', tagNamesColumn); + + const tagNumbersFilter = tagNumbers.length > 0 + ? ` AND a.tagnum IN (${tagNumbers.join(',')})` + : ''; + + const queryText = ` + DECLARE + @fromParam DATETIME = $1, + @toParam DATETIME = $2, + @intervalParam INT = $3; + + SELECT TOP 10 + 'DEBUG_AVERAGING' as info, + b.tag_name, + DATEADD(MINUTE, + (DATEDIFF(MINUTE, @fromParam, CAST(a.datetime AS DATETIME)) / @intervalParam) * @intervalParam, + @fromParam + ) AS waktu_group, + COUNT(*) as data_points, + AVG(CAST(a.val AS FLOAT)) as avg_val, + MIN(CAST(a.val AS FLOAT)) as min_val, + MAX(CAST(a.val AS FLOAT)) as max_val + FROM ${tableName} a + INNER JOIN m_tags b ON a.tagnum = b.tag_number + AND b.deleted_at IS NULL + AND b.is_report = 1 + WHERE CAST(a.datetime AS DATETIME) BETWEEN @fromParam AND @toParam + AND a.val IS NOT NULL + ${tagNumbersFilter} + GROUP BY + b.tag_name, + DATEADD(MINUTE, + (DATEDIFF(MINUTE, @fromParam, CAST(a.datetime AS DATETIME)) / @intervalParam) * @intervalParam, + @fromParam + ) + ORDER BY b.tag_name, waktu_group; + + ;WITH TimeSeries AS ( + SELECT @fromParam AS waktu + UNION ALL + SELECT DATEADD(MINUTE, @intervalParam, waktu) + FROM TimeSeries + WHERE DATEADD(MINUTE, @intervalParam, waktu) <= @toParam + ), + CleanData AS ( + SELECT + CAST(a.datetime AS DATETIME) as datetime_clean, + a.tagnum, + CAST(a.val AS FLOAT) as val, + b.tag_name + FROM ${tableName} a + INNER JOIN m_tags b ON a.tagnum = b.tag_number + AND b.deleted_at IS NULL + AND b.is_report = 1 + WHERE ISDATE(a.datetime) = 1 + AND a.val IS NOT NULL + AND CAST(a.datetime AS DATETIME) BETWEEN @fromParam AND @toParam + ${tagNumbersFilter} + ), + Averaged AS ( + SELECT + DATEADD(MINUTE, + (DATEDIFF(MINUTE, @fromParam, datetime_clean) / @intervalParam) * @intervalParam, + @fromParam + ) AS waktu_group, + tag_name, + AVG(val) AS avg_val + FROM CleanData + GROUP BY + DATEADD(MINUTE, + (DATEDIFF(MINUTE, @fromParam, datetime_clean) / @intervalParam) * @intervalParam, + @fromParam + ), + tag_name + ), + Pivoted AS ( + SELECT + waktu_group, + ${tagNames} + FROM Averaged + PIVOT ( + MAX(avg_val) + FOR tag_name IN (${tagNames}) + ) AS p + ) + SELECT + CONVERT(VARCHAR(19), 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 request = pool.request(); + request.timeout = 60000; // 60 detik + + const result = await request.query(queryText, [from, to, interval]); + + if (result.recordsets && result.recordsets.length >= 2) { + console.log('Sample averaging data:'); + result.recordsets[0].slice(0, 10).forEach(row => { + console.log(`${row.tag_name} @ ${row.waktu_group}: avg=${row.avg_val}, min=${row.min_val}, max=${row.max_val}, points=${row.data_points}`); + }); + + console.log('\nPivot result sample:'); + if (result.recordsets[1] && result.recordsets[1].length > 0) { + result.recordsets[1].slice(0, 5).forEach(row => { + console.log(JSON.stringify(row, null, 2)); + }); + } + } + + const rows = result.recordsets?.[1] || result.recordset; + + if (!rows || rows.length === 0) { + console.log('No pivot data'); + return { data: [], column: tagNamesColumn }; + } + + 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 && row[tag] !== undefined ? Number(row[tag]) : null + })) + })); + + nivoData.forEach(series => { + const nonNull = series.data.filter(d => d.y !== null && d.y !== 0); + const sampleVals = nonNull.slice(0, 3).map(d => d.y); + console.log(`${series.id}: ${nonNull.length} non-zero values, sample: [${sampleVals.join(', ')}]`); + }); + + return { data: nivoData, column: tagNamesColumn }; + } catch (error) { + console.error('Error in getHistoryValueReportPivotDb:', error); + throw error; } - - 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, - ROUND(AVG(a.val), 4) 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 = {}) => { @@ -282,18 +446,55 @@ const getHistoryValueTrendingPivotDb = async (tableName, searchParams = {}) => { 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 + let tagQueryParams = []; + let tagWhereConditions = []; + + if (searchParams.plant_sub_section_id) { + tagWhereConditions.push(`plant_sub_section_id = $${tagQueryParams.length + 1}`); + tagQueryParams.push(searchParams.plant_sub_section_id); + } + + if (searchParams.plant_section_id) { + tagWhereConditions.push(`plant_section_id = $${tagQueryParams.length + 1}`); + tagQueryParams.push(searchParams.plant_section_id); + } + + if (searchParams.name) { + const nameFilter = `(tag_name LIKE $${tagQueryParams.length + 1} OR CAST(tag_number AS VARCHAR) LIKE $${tagQueryParams.length + 2})`; + tagWhereConditions.push(nameFilter); + tagQueryParams.push(`%${searchParams.name}%`, `%${searchParams.name}%`); + } + + if (searchParams.criteria) { + const criteriaFilter = `(tag_name LIKE $${tagQueryParams.length + 1} OR CAST(tag_number AS VARCHAR) LIKE $${tagQueryParams.length + 2})`; + tagWhereConditions.push(criteriaFilter); + tagQueryParams.push(`%${searchParams.criteria}%`, `%${searchParams.criteria}%`); + } + + const tagWhereClause = tagWhereConditions.length > 0 + ? ` AND ${tagWhereConditions.join(" AND ")}` + : ''; + + const tagsQuery = ` + SELECT tag_name, tag_number FROM m_tags WHERE is_report = 1 AND deleted_at IS NULL - `); + ${tagWhereClause} + ORDER BY tag_name + `; + + const tags = await pool.query(tagsQuery, tagQueryParams); if (tags.recordset.length === 0) { return { data: [] }; } const tagNames = tags.recordset.map(r => `[${r.tag_name}]`).join(', '); + const tagNumbers = tags.recordset.map(r => r.tag_number); + + const tagNumbersFilter = tagNumbers.length > 0 + ? ` AND a.tagnum IN (${tagNumbers.join(',')})` + : ''; const queryText = ` DECLARE @@ -316,6 +517,7 @@ const getHistoryValueTrendingPivotDb = async (tableName, searchParams = {}) => { 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 + ${tagNumbersFilter} GROUP BY DATEADD(MINUTE, DATEDIFF(MINUTE, 0, CAST(a.datetime AS DATETIME)) / @intervalParam * @intervalParam, 0), b.tag_name @@ -359,7 +561,6 @@ const getHistoryValueTrendingPivotDb = async (tableName, searchParams = {}) => { return { data: nivoData }; }; - module.exports = { getHistoryAlarmDb, getHistoryEventDb, @@ -367,4 +568,4 @@ module.exports = { getHistoryValueReportDb, getHistoryValueReportPivotDb, getHistoryValueTrendingPivotDb -}; +}; \ No newline at end of file diff --git a/services/history_value.service.js b/services/history_value.service.js index d3f71d9..1913f41 100644 --- a/services/history_value.service.js +++ b/services/history_value.service.js @@ -1,4 +1,11 @@ -const { getHistoryAlarmDb, getHistoryEventDb, checkTableNamedDb, getHistoryValueReportDb, getHistoryValueReportPivotDb, getHistoryValueTrendingPivotDb } = 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'); @@ -7,94 +14,128 @@ class HistoryValue { static async getAllHistoryAlarm(param) { try { const results = await getHistoryAlarmDb(param); - - results.data.map(element => { - }); - - return results + return results; } catch (error) { - throw new ErrorHandler(error.statusCode, error.message); + throw new ErrorHandler(error.statusCode || 500, error.message || 'Error fetching alarm history'); } } static async getAllHistoryEvent(param) { try { const results = await getHistoryEventDb(param); - - results.data.map(element => { - }); - - return results + return results; } catch (error) { - throw new ErrorHandler(error.statusCode, error.message); + throw new ErrorHandler(error.statusCode || 500, error.message || 'Error fetching event history'); } } static async getHistoryValueReport(param) { try { + if (!param.plant_sub_section_id) { + throw new ErrorHandler(400, 'plant_sub_section_id is required'); + } const plantSubSection = await getSubSectionByIdDb(param.plant_sub_section_id); - if (plantSubSection.length < 1) throw new ErrorHandler(404, 'Plant sub section not found'); + if (!plantSubSection || plantSubSection.length < 1) { + throw new ErrorHandler(404, 'Plant sub section not found'); + } - const tabelExist = await checkTableNamedDb(plantSubSection[0]?.table_name_value); + const tableNameValue = plantSubSection[0]?.table_name_value; + + if (!tableNameValue) { + throw new ErrorHandler(404, 'Table name not configured for this sub section'); + } - if (tabelExist.length < 1) throw new ErrorHandler(404, 'Value not found'); + const tableExist = await checkTableNamedDb(tableNameValue); - const results = await getHistoryValueReportDb(tabelExist[0]?.TABLE_NAME, param); + if (!tableExist || tableExist.length < 1) { + throw new ErrorHandler(404, `Value table '${tableNameValue}' not found`); + } - results.data.map(element => { - }); - - return results + const results = await getHistoryValueReportDb(tableExist[0].TABLE_NAME, param); + return results; } catch (error) { - throw new ErrorHandler(error.statusCode, error.message); + throw new ErrorHandler( + error.statusCode || 500, + error.message || 'Error fetching history value report' + ); } } 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); + try { + if (!param.plant_sub_section_id) { + throw new ErrorHandler(400, 'plant_sub_section_id is required'); } + if (!param.from || !param.to) { + throw new ErrorHandler(400, 'from and to date parameters are required'); + } + + const plantSubSection = await getSubSectionByIdDb(param.plant_sub_section_id); + + if (!plantSubSection || plantSubSection.length < 1) { + throw new ErrorHandler(404, 'Plant sub section not found'); + } + + const tableNameValue = plantSubSection[0]?.table_name_value; + + if (!tableNameValue) { + throw new ErrorHandler(404, 'Table name not configured for this sub section'); + } + + const tableExist = await checkTableNamedDb(tableNameValue); + + if (!tableExist || tableExist.length < 1) { + throw new ErrorHandler(404, `Value table '${tableNameValue}' not found`); + } + + const results = await getHistoryValueReportPivotDb(tableExist[0].TABLE_NAME, param); + return results; + } catch (error) { + throw new ErrorHandler( + error.statusCode || 500, + error.message || 'Error fetching history value report pivot' + ); } +} static async getHistoryValueTrendingPivot(param) { try { + if (!param.plant_sub_section_id) { + throw new ErrorHandler(400, 'plant_sub_section_id is required'); + } + if (!param.from || !param.to) { + throw new ErrorHandler(400, 'from and to date parameters are required'); + } const plantSubSection = await getSubSectionByIdDb(param.plant_sub_section_id); - if (plantSubSection.length < 1) throw new ErrorHandler(404, 'Plant sub section not found'); + if (!plantSubSection || plantSubSection.length < 1) { + throw new ErrorHandler(404, 'Plant sub section not found'); + } - const tabelExist = await checkTableNamedDb(plantSubSection[0]?.table_name_value); + const tableNameValue = plantSubSection[0]?.table_name_value; + + if (!tableNameValue) { + throw new ErrorHandler(404, 'Table name not configured for this sub section'); + } - if (tabelExist.length < 1) throw new ErrorHandler(404, 'Value not found'); + const tableExist = await checkTableNamedDb(tableNameValue); - const results = await getHistoryValueTrendingPivotDb(tabelExist[0]?.TABLE_NAME, param); + if (!tableExist || tableExist.length < 1) { + throw new ErrorHandler(404, `Value table '${tableNameValue}' not found`); + } - results.data.map(element => { - }); - - return results + const results = await getHistoryValueTrendingPivotDb(tableExist[0].TABLE_NAME, param); + return results; } catch (error) { - throw new ErrorHandler(error.statusCode, error.message); + throw new ErrorHandler( + error.statusCode || 500, + error.message || 'Error fetching history value trending pivot' + ); } } } -module.exports = HistoryValue; +module.exports = HistoryValue; \ No newline at end of file From 1496b80fdf5fcd00fa1b344176ae57c0f4dc2db4 Mon Sep 17 00:00:00 2001 From: Athif Date: Thu, 18 Dec 2025 13:37:47 +0700 Subject: [PATCH 32/37] Update Value Report. --- controllers/history_value.controller.js | 34 ++++++------- db/history_value.db.js | 7 +-- services/history_value.service.js | 66 ++++++++++++------------- 3 files changed, 52 insertions(+), 55 deletions(-) diff --git a/controllers/history_value.controller.js b/controllers/history_value.controller.js index d4299f5..ee68c69 100644 --- a/controllers/history_value.controller.js +++ b/controllers/history_value.controller.js @@ -57,27 +57,27 @@ class HistoryValueController { } } -static async getHistoryValueReportPivot(req, res) { - try { - const queryParams = req.query; + static async getHistoryValueReportPivot(req, res) { + try { + const queryParams = req.query; - const results = await HistoryValue.getHistoryValueReportPivot(queryParams); - const response = await setResponsePaging(queryParams, results, 'Data found'); + const results = await HistoryValue.getHistoryValueReportPivot(queryParams); + const response = await setResponsePaging(queryParams, results, 'Data found'); - if (results.column) { - response.columns = results.column; + if (results.column) { + response.columns = results.column; + } + + res.status(response.statusCode).json(response); + } catch (error) { + const statusCode = error.statusCode || 500; + res.status(statusCode).json({ + success: false, + statusCode, + message: error.message || 'Internal server error' + }); } - - res.status(response.statusCode).json(response); - } catch (error) { - const statusCode = error.statusCode || 500; - res.status(statusCode).json({ - success: false, - statusCode, - message: error.message || 'Internal server error' - }); } -} static async getHistoryValueTrendingPivot(req, res) { try { diff --git a/db/history_value.db.js b/db/history_value.db.js index ef55234..b610fb0 100644 --- a/db/history_value.db.js +++ b/db/history_value.db.js @@ -385,10 +385,7 @@ const getHistoryValueReportPivotDb = async (tableName, searchParams = {}) => { OPTION (MAXRECURSION 0); `; - const request = pool.request(); - request.timeout = 60000; // 60 detik - - const result = await request.query(queryText, [from, to, interval]); + const result = await pool.query(queryText, [from, to, interval]); if (result.recordsets && result.recordsets.length >= 2) { console.log('Sample averaging data:'); @@ -551,7 +548,7 @@ const getHistoryValueTrendingPivotDb = async (tableName, searchParams = {}) => { const tagList = Object.keys(rows[0]).filter(k => k !== timeKey); const nivoData = tagList.map(tag => ({ - name: tag, + id: tag, data: rows.map(row => ({ x: row[timeKey], y: row[tag] !== null ? Number(row[tag]) : null diff --git a/services/history_value.service.js b/services/history_value.service.js index 1913f41..2c13de2 100644 --- a/services/history_value.service.js +++ b/services/history_value.service.js @@ -64,41 +64,41 @@ class HistoryValue { } static async getHistoryValueReportPivot(param) { - try { - if (!param.plant_sub_section_id) { - throw new ErrorHandler(400, 'plant_sub_section_id is required'); + try { + if (!param.plant_sub_section_id) { + throw new ErrorHandler(400, 'plant_sub_section_id is required'); + } + if (!param.from || !param.to) { + throw new ErrorHandler(400, 'from and to date parameters are required'); + } + + const plantSubSection = await getSubSectionByIdDb(param.plant_sub_section_id); + + if (!plantSubSection || plantSubSection.length < 1) { + throw new ErrorHandler(404, 'Plant sub section not found'); + } + + const tableNameValue = plantSubSection[0]?.table_name_value; + + if (!tableNameValue) { + throw new ErrorHandler(404, 'Table name not configured for this sub section'); + } + + const tableExist = await checkTableNamedDb(tableNameValue); + + if (!tableExist || tableExist.length < 1) { + throw new ErrorHandler(404, `Value table '${tableNameValue}' not found`); + } + + const results = await getHistoryValueReportPivotDb(tableExist[0].TABLE_NAME, param); + return results; + } catch (error) { + throw new ErrorHandler( + error.statusCode || 500, + error.message || 'Error fetching history value report pivot' + ); } - if (!param.from || !param.to) { - throw new ErrorHandler(400, 'from and to date parameters are required'); - } - - const plantSubSection = await getSubSectionByIdDb(param.plant_sub_section_id); - - if (!plantSubSection || plantSubSection.length < 1) { - throw new ErrorHandler(404, 'Plant sub section not found'); - } - - const tableNameValue = plantSubSection[0]?.table_name_value; - - if (!tableNameValue) { - throw new ErrorHandler(404, 'Table name not configured for this sub section'); - } - - const tableExist = await checkTableNamedDb(tableNameValue); - - if (!tableExist || tableExist.length < 1) { - throw new ErrorHandler(404, `Value table '${tableNameValue}' not found`); - } - - const results = await getHistoryValueReportPivotDb(tableExist[0].TABLE_NAME, param); - return results; - } catch (error) { - throw new ErrorHandler( - error.statusCode || 500, - error.message || 'Error fetching history value report pivot' - ); } -} static async getHistoryValueTrendingPivot(param) { try { From 85750b397b5b74a907a4ddaa18816106eec7eef7 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Thu, 18 Dec 2025 14:37:47 +0700 Subject: [PATCH 33/37] repair: change readers to users in detail notification --- db/notification_error.db.js | 8 ++++---- services/notification_error.service.js | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 03b85d5..2ec1500 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -131,14 +131,14 @@ const updateNotificationErrorDb = async (id, data) => { return getNotificationByIdDb(id); }; -const getReaderNotificationErrorDb = async (id) => { +const getUsersNotificationErrorDb = async (id) => { const queryText = ` SELECT + b.notification_error_id, a.notification_error_user_id, a.contact_phone, a.contact_name, - a.is_send, - b.message_error_issue + a.is_send FROM notification_error_user a @@ -157,6 +157,6 @@ module.exports = { getAllNotificationDb, InsertNotificationErrorDb, updateNotificationErrorDb, - getReaderNotificationErrorDb + getUsersNotificationErrorDb }; diff --git a/services/notification_error.service.js b/services/notification_error.service.js index 61554a5..7b348b8 100644 --- a/services/notification_error.service.js +++ b/services/notification_error.service.js @@ -2,7 +2,7 @@ const { getAllNotificationDb, getNotificationByIdDb, InsertNotificationErrorDb, - getReaderNotificationErrorDb, + getUsersNotificationErrorDb, updateNotificationErrorDb, } = require('../db/notification_error.db'); @@ -64,7 +64,7 @@ class NotificationService { throw new ErrorHandler(404, 'Notification not found'); } - const readerNotification = (await getReaderNotificationErrorDb(id))|| []; + const usersNotification = (await getUsersNotificationErrorDb(id))|| []; // Get error code details if error_code_id exists if (notification.error_code_id) { @@ -106,7 +106,7 @@ class NotificationService { // Get activity logs for this notification const notificationLogs = (await getNotificationErrorLogByNotificationErrorIdDb(id)) || []; - notification.reader = readerNotification; + notification.users = usersNotification; notification.activity_logs = notificationLogs; From 9f7a73e1491e60fca356099e7ffe33872c0cecd5 Mon Sep 17 00:00:00 2001 From: mhmmdafif Date: Thu, 18 Dec 2025 15:18:20 +0700 Subject: [PATCH 34/37] add: plant sub section name, device, brand name in detail notification --- db/notification_error.db.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 2ec1500..7648683 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -14,8 +14,28 @@ const InsertNotificationErrorDb = async (store) => { const getNotificationByIdDb = async (id) => { const queryText = ` SELECT - a.* + a.*, + b.plant_sub_section_id, + c.plant_sub_section_name, + d.device_code, + d.device_name, + d.device_location, + e.brand_name + FROM notification_error a + + LEFT JOIN m_tags b + ON a.error_chanel = b.tag_number + + LEFT JOIN m_plant_sub_section c + ON b.plant_sub_section_id = c.plant_sub_section_id + + LEFT JOIN m_device d + ON b.device_id = d.device_id AND d.deleted_at IS NULL + + LEFT JOIN m_brands e + ON d.brand_id = e.brand_id AND d.deleted_at IS NULL + WHERE a.notification_error_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); From 518d6ff42747a870bb509636a69da324d3540f19 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 18 Dec 2025 16:19:46 +0700 Subject: [PATCH 35/37] repair: detail notif get with error_channel --- db/notification_error.db.js | 9 +++++++-- db/notification_error_log.db.js | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 7648683..8f1aa38 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -31,7 +31,7 @@ const getNotificationByIdDb = async (id) => { ON b.plant_sub_section_id = c.plant_sub_section_id LEFT JOIN m_device d - ON b.device_id = d.device_id AND d.deleted_at IS NULL + ON a.error_chanel = d.listen_channel AND d.deleted_at IS NULL LEFT JOIN m_brands e ON d.brand_id = e.brand_id AND d.deleted_at IS NULL @@ -102,8 +102,10 @@ const getAllNotificationDb = async (searchParams = {}) => { c.type_solution, c.path_solution, + d.device_code, d.device_name, d.device_location, + e.brand_name, COALESCE(d.device_name, '') + ' - ' + COALESCE(b.error_code_name, '') AS device_name_error @@ -116,7 +118,10 @@ const getAllNotificationDb = async (searchParams = {}) => { ON b.error_code_id = c.error_code_id AND c.deleted_at IS NULL LEFT JOIN m_device d - ON b.brand_id = d.brand_id AND d.deleted_at IS NULL + ON a.error_chanel = d.listen_channel AND d.deleted_at IS NULL + + LEFT JOIN m_brands e + ON d.brand_id = e.brand_id AND d.deleted_at IS NULL WHERE a.deleted_at IS NULL ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} diff --git a/db/notification_error_log.db.js b/db/notification_error_log.db.js index 1a8a0cf..8c8a4d9 100644 --- a/db/notification_error_log.db.js +++ b/db/notification_error_log.db.js @@ -7,7 +7,7 @@ const getAllNotificationErrorLogDb = async () => { b.contact_name, b.contact_type FROM notification_error_log a - LEFT JOIN contact b ON a.contact_id = b.contact_id + LEFT JOIN contact b ON a.contact_phone = b.contact_phone WHERE a.deleted_at IS NULL ORDER BY a.notification_error_log_id DESC `; @@ -22,7 +22,7 @@ const getNotificationErrorLogByIdDb = async (id) => { b.contact_name, b.contact_type FROM notification_error_log a - LEFT JOIN contact b ON a.contact_id = b.contact_id + LEFT JOIN contact b ON a.contact_phone = b.contact_phone WHERE a.notification_error_log_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); @@ -36,7 +36,7 @@ const getNotificationErrorLogByNotificationErrorIdDb = async (notificationErrorI b.contact_name, b.contact_type FROM notification_error_log a - LEFT JOIN contact b ON a.contact_id = b.contact_id + LEFT JOIN contact b ON a.contact_phone = b.contact_phone WHERE a.notification_error_id = $1 AND a.deleted_at IS NULL ORDER BY a.created_at DESC `; From 706fd401f47cefff04f72bd4f63dc5677f22b2c4 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 18 Dec 2025 16:36:48 +0700 Subject: [PATCH 36/37] rapair: info device notification error --- db/notification_error.db.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 8f1aa38..75524d9 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -20,6 +20,7 @@ const getNotificationByIdDb = async (id) => { d.device_code, d.device_name, d.device_location, + d.listen_channel, e.brand_name FROM notification_error a @@ -105,6 +106,7 @@ const getAllNotificationDb = async (searchParams = {}) => { d.device_code, d.device_name, d.device_location, + d.listen_channel, e.brand_name, COALESCE(d.device_name, '') + ' - ' + COALESCE(b.error_code_name, '') AS device_name_error From c9dba276bb76f2daa2524e38f288c47d799eaa12 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 18 Dec 2025 17:32:21 +0700 Subject: [PATCH 37/37] repair: notification_log --- controllers/notification_error_log.controller.js | 13 ++++++++++++- services/notification_error_log.service.js | 2 +- validate/notification_error_log.schema.js | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/controllers/notification_error_log.controller.js b/controllers/notification_error_log.controller.js index 64b6670..d928a77 100644 --- a/controllers/notification_error_log.controller.js +++ b/controllers/notification_error_log.controller.js @@ -55,7 +55,18 @@ class NotificationErrorLogController { return res.status(400).json(setResponse(error, 'Validation failed', 400)); } - value.created_by = req.user.user_id; + let createdBy, contactPhone; + + if (!isNaN(req.user.userId) && Number(req.user.userId) > 0) { + createdBy = Number(req.user.userId); + contactPhone = value.contact_phone || null; + } else { + createdBy = null; + contactPhone = req.user.userId; + } + + value.created_by = createdBy; + value.contact_phone = contactPhone; const results = await NotificationErrorLogService.createNotificationErrorLog(value); const response = await setResponse(results, 'Notification Error Log created successfully') diff --git a/services/notification_error_log.service.js b/services/notification_error_log.service.js index 45d9f58..b36a5b8 100644 --- a/services/notification_error_log.service.js +++ b/services/notification_error_log.service.js @@ -46,7 +46,7 @@ class NotificationErrorLogService { const store = { notification_error_id: data.notification_error_id, - contact_id: data.contact_id, + contact_phone: data.contact_phone, notification_error_log_description: data.notification_error_log_description, created_by: data.created_by }; diff --git a/validate/notification_error_log.schema.js b/validate/notification_error_log.schema.js index ed740b4..1083e9a 100644 --- a/validate/notification_error_log.schema.js +++ b/validate/notification_error_log.schema.js @@ -2,7 +2,7 @@ const Joi = require("joi"); const insertNotificationErrorLogSchema = Joi.object({ notification_error_id: Joi.number().integer().required(), - contact_id: Joi.number().integer().required(), + contact_phone: Joi.string().optional(), notification_error_log_description: Joi.string().required() });