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 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/controllers/brand.controller.js b/controllers/brand.controller.js index 4820f71..7eb1437 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, @@ -30,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); @@ -40,61 +39,28 @@ 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); } - // Update brand + // Update brand static async update(req, res) { const { id } = req.params; const { error, value } = await checkValidate(updateBrandSchema, req); + 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.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.updateBrand(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/controllers/error_code.controller.js b/controllers/error_code.controller.js new file mode 100644 index 0000000..a263397 --- /dev/null +++ b/controllers/error_code.controller.js @@ -0,0 +1,74 @@ +const ErrorCodeService = require('../services/error_code.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { + insertErrorCodeSchema, + updateErrorCodeSchema, +} = require('../validate/error_code.schema'); + +class ErrorCodeController { + static async getByBrandId(req, res) { + const { brandId } = req.params; + const queryParams = req.query; + + const results = await ErrorCodeService.getErrorCodesByBrandId(brandId, 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) { + const { id } = req.params; + const result = await ErrorCodeService.getErrorCodeById(id); + const response = setResponse(result, 'Error code found'); + + res.status(response.statusCode).json(response); + } + + // 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; + value.created_by = req.user?.user_id || null; + + const result = await ErrorCodeService.createErrorCodeWithFullData(brandId, value); + 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) { + const { error, value } = await checkValidate(updateErrorCodeSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + 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); + } + + // Soft delete error code + static async delete(req, res) { + const { brandId, errorCodeId } = req.params; + const deletedBy = req.user?.user_id || null; + + const result = await ErrorCodeService.deleteErrorCode(brandId, errorCodeId, deletedBy); + const response = setResponse(result, 'Error code deleted successfully'); + + res.status(response.statusCode).json(response); + } +} + +module.exports = ErrorCodeController; \ No newline at end of file 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; diff --git a/controllers/history_value.controller.js b/controllers/history_value.controller.js index e4f3665..ee68c69 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) { - const queryParams = req.query; + 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'); - response.columns = results.column + if (results.column) { + response.columns = results.column; + } - 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 getHistoryValueTrendingPivot(req, res) { - const queryParams = req.query; + try { + const queryParams = req.query; - const results = await HistoryValue.getHistoryValueTrendingPivot(queryParams); - const response = await setResponsePaging(queryParams, results, 'Data found'); + 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); + 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; +module.exports = HistoryValueController; \ No newline at end of file diff --git a/controllers/notification_error.controller.js b/controllers/notification_error.controller.js index d37a36c..deb6d08 100644 --- a/controllers/notification_error.controller.js +++ b/controllers/notification_error.controller.js @@ -1,25 +1,76 @@ -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); } + static async getById(req, res) { 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); + } + + 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; \ No newline at end of file +module.exports = NotificationErrorController; 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/controllers/notification_error_user.controller.js b/controllers/notification_error_user.controller.js new file mode 100644 index 0000000..c2b4342 --- /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, 'Notification Error User updated successfully') + + res.status(response.statusCode).json(response); + } + + // 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, 'Notification Error User deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = NotificationErrorUserController; 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/db/brand_code.db.js b/db/brand_code.db.js index 4d8925a..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; }; @@ -64,10 +104,71 @@ 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.* + FROM brand_code a + 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/db/brand_sparepart.db.js b/db/brand_sparepart.db.js index 850879d..9340324 100644 --- a/db/brand_sparepart.db.js +++ b/db/brand_sparepart.db.js @@ -1,9 +1,125 @@ 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_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 + 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_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 + 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_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]); + 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_sparepart (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_sparepart + 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_sparepart + 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; +}; + +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; +}; + +const getSparepartsByBrandIdDb = async (brandId) => { + const queryText = ` + SELECT DISTINCT s.sparepart_id, s.sparepart_name, s.sparepart_code, @@ -17,20 +133,22 @@ 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 - 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, @@ -40,9 +158,13 @@ const getBrandsBySparepartIdDb = async (sparepartId) => { b.is_active, b.created_at, b.updated_at - FROM brand_spareparts bs - JOIN m_brands b ON bs.brand_id = b.brand_id + 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 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 +172,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/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..b610fb0 100644 --- a/db/history_value.db.js +++ b/db/history_value.db.js @@ -115,160 +115,321 @@ 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 result = await pool.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 +443,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 +514,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 +558,6 @@ const getHistoryValueTrendingPivotDb = async (tableName, searchParams = {}) => { return { data: nivoData }; }; - module.exports = { getHistoryAlarmDb, getHistoryEventDb, @@ -367,4 +565,4 @@ module.exports = { getHistoryValueReportDb, getHistoryValueReportPivotDb, getHistoryValueTrendingPivotDb -}; +}; \ No newline at end of file diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 45d8449..75524d9 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -1,71 +1,52 @@ 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_description, '-'), - ' 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) => { const queryText = ` SELECT - a.* + a.*, + b.plant_sub_section_id, + c.plant_sub_section_name, + d.device_code, + d.device_name, + d.device_location, + d.listen_channel, + 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 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.notification_error_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); return result.recordset[0]; }; + const getAllNotificationDb = async (searchParams = {}) => { let queryParams = []; - await InsertNotificationErrorDb(); - - 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]; @@ -73,13 +54,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 @@ -88,10 +69,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 ); @@ -111,24 +95,35 @@ const getAllNotificationDb = async (searchParams = {}) => { 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_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 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 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 ")}` : ""} @@ -149,8 +144,46 @@ 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 getUsersNotificationErrorDb = async (id) => { + const queryText = ` + SELECT + b.notification_error_id, + a.notification_error_user_id, + a.contact_phone, + a.contact_name, + a.is_send + + 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.deleted_at IS NULL + `; + + const result = await pool.query(queryText, [id]); + return result.recordset; +}; module.exports = { getNotificationByIdDb, getAllNotificationDb, + InsertNotificationErrorDb, + updateNotificationErrorDb, + getUsersNotificationErrorDb + }; 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 `; diff --git a/db/notification_error_sparepart.db.js b/db/notification_error_sparepart.db.js index 17b9288..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) { @@ -11,10 +39,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 +53,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 +68,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 +105,27 @@ const getAllNotificationErrorSparepartDb = async (searchParams = {}) => { const getNotificationErrorSparepartByIdDb = async (id) => { const queryText = ` - SELECT + SELECT 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/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/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/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/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/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/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/routes/error_code.route.js b/routes/error_code.route.js new file mode 100644 index 0000000..c65860b --- /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/:errorCodeId') + .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..fabe125 100644 --- a/routes/index.js +++ b/routes/index.js @@ -18,6 +18,8 @@ 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); router.use("/user", users); @@ -38,5 +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.route.js b/routes/notification_error.route.js index a0e8f83..d8ab014 100644 --- a/routes/notification_error.route.js +++ b/routes/notification_error.route.js @@ -7,10 +7,15 @@ 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) + .put(verifyToken.verifyAccessToken, verifyAccess(), NotificationErrorController.update) 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/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 diff --git a/services/brand.service.js b/services/brand.service.js index 883a62d..a50b8b4 100644 --- a/services/brand.service.js +++ b/services/brand.service.js @@ -2,36 +2,11 @@ const { getAllBrandsDb, getBrandByIdDb, - getBrandByNameDb, createBrandDb, updateBrandDb, deleteBrandDb, checkBrandNameExistsDb, } = require("../db/brand.db"); - -const { - insertMultipleBrandSparepartsDb, - updateBrandSparepartsDb, - deleteAllBrandSparepartsDb, - getSparepartsByBrandIdDb, -} = require("../db/brand_sparepart.db"); - -// Error code operations -const { - getErrorCodesByBrandIdDb, - createErrorCodeDb, - updateErrorCodeDb, - deleteErrorCodeDb, -} = require("../db/brand_code.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 { @@ -40,86 +15,22 @@ 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 { ...results, - data: brandsWithSpareparts + data: results.data }; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); } } - // 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"); - // Get spareparts for this brand - const spareparts = await getSparepartsByBrandIdDb(brand.brand_id); - - const errorCodes = await getErrorCodesByBrandIdDb(brand.brand_id); - - const errorCodesWithSolutions = await Promise.all( - errorCodes.map(async (errorCode) => { - const solutions = await getSolutionsByErrorCodeIdDb( - 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, - }; - }) - ); - - return { - ...brand, - spareparts: spareparts, - error_code: errorCodesWithSolutions, - }; + return brand; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); } @@ -127,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) { @@ -138,84 +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; - - 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, - 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 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); } } @@ -236,8 +84,9 @@ 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"); @@ -250,143 +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.spareparts !== undefined) { - await updateBrandSparepartsDb(existingBrand.brand_id, data.spareparts || [], data.updated_by); + const updatedBrand = await updateBrandDb(existingBrand.brand_name, brandData); + if (!updatedBrand) { + throw new ErrorHandler(500, "Failed to update brand"); } - 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.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.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 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 new file mode 100644 index 0000000..1c2c68f --- /dev/null +++ b/services/error_code.service.js @@ -0,0 +1,276 @@ +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 { + static async getAllErrorCodes(param) { + try { + const results = await getAllErrorCodesDb(param); + + return results; + } 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, queryParams = {}) { + try { + const results = await getErrorCodesByBrandIdDb(brandId, queryParams); + + if (results.data && results.total !== undefined) { + return results; + } + + const errorCodesWithDetails = await Promise.all( + results.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"); + } + if (data.spareparts && Array.isArray(data.spareparts)) { + await insertMultipleErrorCodeSparepartsDb(errorId, data.spareparts, data.created_by); + } + + 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, errorCodeId, data) { + try { + const existingErrorCode = await getErrorCodeByIdDb(errorCodeId); + if (!existingErrorCode) throw new ErrorHandler(404, "Error code not found"); + + // 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 !== undefined || + 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: 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, + updated_by: data.updated_by, + }); + } + + if (data.spareparts && Array.isArray(data.spareparts)) { + await updateErrorCodeSparepartsDb(existingErrorCode.error_code_id, data.spareparts, data.updated_by); + } + + if (data.solution && Array.isArray(data.solution)) { + const existingSolutions = await getSolutionsByErrorCodeIdDb(existingErrorCode.error_code_id); + const incomingSolutionNames = data.solution.map((s) => s.solution_name); + + for (const solutionData of data.solution) { + const existingSolution = existingSolutions.find( + (s) => s.solution_name === solutionData.solution_name + ); + + if (existingSolution) { + 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 { + 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, + }); + } + } + + 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, errorCodeId, deletedBy) { + try { + const errorCodeExist = await getErrorCodeByIdDb(errorCodeId); + + if (!errorCodeExist) { + throw new ErrorHandler(404, "Error code not found"); + } + + // 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) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = ErrorCodeService; \ No newline at end of file diff --git a/services/history_value.service.js b/services/history_value.service.js index d3f71d9..2c13de2 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 { + 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 getHistoryValueReportPivotDb(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 getHistoryValueReportPivotDb(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 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 diff --git a/services/notification_error.service.js b/services/notification_error.service.js index 8ca77d5..7b348b8 100644 --- a/services/notification_error.service.js +++ b/services/notification_error.service.js @@ -1,6 +1,9 @@ const { getAllNotificationDb, getNotificationByIdDb, + InsertNotificationErrorDb, + getUsersNotificationErrorDb, + updateNotificationErrorDb, } = require('../db/notification_error.db'); const { @@ -17,6 +20,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'); @@ -36,6 +43,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 { @@ -45,6 +64,8 @@ class NotificationService { throw new ErrorHandler(404, 'Notification not found'); } + const usersNotification = (await getUsersNotificationErrorDb(id))|| []; + // Get error code details if error_code_id exists if (notification.error_code_id) { const errorCode = await getErrorCodeByIdDb(notification.error_code_id); @@ -53,6 +74,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; @@ -71,9 +94,11 @@ class NotificationService { }) ); + notification.error_code = { ...errorCode, - solution: solutionsWithDetails + solution: solutionsWithDetails, + spareparts: spareparts, }; } } @@ -81,6 +106,8 @@ class NotificationService { // Get activity logs for this notification const notificationLogs = (await getNotificationErrorLogByNotificationErrorIdDb(id)) || []; + notification.users = usersNotification; + notification.activity_logs = notificationLogs; return notification; @@ -88,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; 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/services/notification_error_sparepart.service.js b/services/notification_error_sparepart.service.js index a5208c4..1e79b3c 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, 'Notification Error Sparepart 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); 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/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/brand.schema.js b/validate/brand.schema.js index 5d9d30f..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(""), - spareparts: Joi.array().items(Joi.number().integer()).optional(), // Array of sparepart_id - 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(""), - 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(""), - spareparts: Joi.array().items(Joi.number().integer()).optional(), // Array of sparepart_id - 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(""), - 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/contact.schema.js b/validate/contact.schema.js index 8edcdf7..04aaa0a 100644 --- a/validate/contact.schema.js +++ b/validate/contact.schema.js @@ -13,20 +13,20 @@ 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().allow(null) }); 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", }), is_active: Joi.boolean().optional(), - contact_type: Joi.string().max(255).optional() + contact_type: Joi.string().max(255).optional().allow(null) }); module.exports = { diff --git a/validate/device.schema.js b/validate/device.schema.js index c054b02..4b3f945 100644 --- a/validate/device.schema.js +++ b/validate/device.schema.js @@ -9,13 +9,14 @@ 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() .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 diff --git a/validate/error_code.schema.js b/validate/error_code.schema.js new file mode 100644 index 0000000..0b53de1 --- /dev/null +++ b/validate/error_code.schema.js @@ -0,0 +1,72 @@ +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: 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(""), + 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 diff --git a/validate/notification.schema.js b/validate/notification.schema.js index 38b55fb..e4796fb 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", @@ -38,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", }), 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() }); 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, +}; diff --git a/validate/sparepart.schema.js b/validate/sparepart.schema.js index be204c1..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(), @@ -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(),