Compare commits

...

50 Commits

Author SHA1 Message Date
436ea1cf89 replace created at in notif error 2026-01-09 14:30:07 +07:00
8e0dc4bb88 fixing token redirect 2026-01-09 11:06:05 +07:00
b680a249d5 connect mqtt ws colo 2026-01-08 15:07:47 +07:00
b1cf4ff624 optional solution error code 2026-01-08 14:32:43 +07:00
747a96ac30 fixing verify token redirect 2026-01-08 14:17:11 +07:00
026a88a9a9 remove validation at least 1 solution 2026-01-08 13:16:13 +07:00
4d2c18edfb fixing verify access 2026-01-08 12:16:16 +07:00
a4d8d55dbf add log in resend notif wa 2026-01-08 11:30:41 +07:00
a6075174f5 Test push 2026-01-08 10:47:39 +07:00
3d163f4507 repair resend notif wa 2026-01-08 09:35:52 +07:00
bc265ccc33 repair resend notif wa 2026-01-08 09:35:18 +07:00
cecdc09719 fixing notification 2026-01-07 16:09:53 +07:00
2a25339478 repair: resend wa 2026-01-07 12:16:53 +07:00
3faa3656c1 repair: resend notif wa 2026-01-07 11:59:27 +07:00
aa7cd2fbaa repair: service & controller notif and notif user 2026-01-07 09:34:59 +07:00
f3abaeac39 repair: change error update is_read true to success 2026-01-06 16:28:37 +07:00
b025c5ea82 repair: body message wa in notif 2026-01-06 14:37:52 +07:00
6f4d171537 add: self-signed certificate 2026-01-06 11:39:33 +07:00
71c5e94f42 repair: change message wa notification 2026-01-05 16:10:29 +07:00
0c16b2a3fd add: is_send & updated_at in notif error user at notif error 2026-01-05 11:31:43 +07:00
402c1c0705 add: is_read & updated_at in notif error user at notif error 2026-01-05 11:25:25 +07:00
adaa9fda9a repair: update is read in notification error 2026-01-05 10:08:53 +07:00
3eb403c557 repair: resend chat all user & by user 2026-01-04 19:20:43 +07:00
bedc948a74 add: params contact_phone and validation resend chat wa in notif error user 2026-01-01 18:31:09 +07:00
889aa04808 update is_read in notification error 2025-12-31 15:38:47 +07:00
25ba80ab7e add: resend chat wa notif error user 2025-12-31 15:11:34 +07:00
a704eb3235 repair: notification error log created_by 2025-12-23 17:54:14 +07:00
10b7ac5e68 add: list user berdasarkan notifcation_error_id 2025-12-23 11:53:12 +07:00
37185a9fbc fixing redirect token wa 2025-12-22 17:23:22 +07:00
c9dba276bb repair: notification_log 2025-12-18 17:32:21 +07:00
706fd401f4 rapair: info device notification error 2025-12-18 16:36:48 +07:00
518d6ff427 repair: detail notif get with error_channel 2025-12-18 16:19:46 +07:00
9f7a73e149 add: plant sub section name, device, brand name in detail notification 2025-12-18 15:18:20 +07:00
bba50177e9 repair: change readers to users 2025-12-18 14:38:58 +07:00
85750b397b repair: change readers to users in detail notification 2025-12-18 14:37:47 +07:00
Athif
1496b80fdf Update Value Report. 2025-12-18 13:37:47 +07:00
Albani Rajata Malik
c112ff165a update value report 2025-12-18 13:26:48 +07:00
5e7de6d144 delete: api is ready & not read 2025-12-18 13:04:57 +07:00
dd5e1cc713 add: api is read & not read in notification 2025-12-18 12:43:44 +07:00
1aa7b1bc08 add: reader in notification detail & update notification 2025-12-18 11:39:24 +07:00
f2c8c3818d repair: ErrorCode brand-device 2025-12-18 10:51:19 +07:00
907f5767c1 notification wa by mqtt broker 2025-12-18 10:05:50 +07:00
2b93baa648 add: crud notif error user 2025-12-09 16:30:19 +07:00
fb3061e0d1 add: add notification 2025-12-09 16:04:32 +07:00
d063478fc2 add: path icon & error code color in all notif error 2025-12-08 11:29:16 +07:00
555a68e90c repair: brand + errorcode 2025-12-05 18:18:50 +07:00
dc7712a79f add: sparepart in detail notification error 2025-12-05 09:21:04 +07:00
20d035a1ca add: sparepart in detail notification error 2025-12-05 09:13:41 +07:00
1d3de9ae41 repair: notification error sparepart 2025-12-04 16:07:04 +07:00
198346ff0b repair: device_description not required 2025-12-04 15:44:27 +07:00
37 changed files with 2007 additions and 723 deletions

9
app.js
View File

@@ -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;

View File

@@ -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: 'morekmorekmorek', // jika ada
password: 'morek888', // jika ada
};
const mqttUrl = 'ws://117.102.231.130:7001'; // Ganti dengan broker kamu
const topic = process.env.TOPIC_COD ?? '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
};

View File

@@ -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,44 @@ 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));
console.log("decrypted: ", decrypted);
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,
idData
},
'Verify successful'
);
res.status(response.statusCode).json(response);
}
}
module.exports = AuthController;

View File

@@ -29,7 +29,7 @@ class BrandController {
res.status(response.statusCode).json(response);
}
// Create brand with nested error codes and solutions
// Create brand
static async create(req, res) {
const { error, value } = await checkValidate(insertBrandSchema, req);
@@ -39,7 +39,7 @@ class BrandController {
value.created_by = req.user?.user_id || null;
const results = await BrandService.createBrandWithFullData(value);
const results = await BrandService.createBrand(value);
const response = await setResponse(results, 'Brand created successfully');
return res.status(response.statusCode).json(response);
@@ -49,29 +49,15 @@ class BrandController {
static async update(req, res) {
const { id } = req.params;
// Debug logging untuk lihat request body
console.log('🔍 BE Raw Request Body:', req.body);
console.log('🔍 BE Request Headers:', req.headers);
console.log('🔍 BE Request Method:', req.method);
const { error, value } = await checkValidate(updateBrandSchema, req);
if (error) {
console.log('❌ BE Validation Error:', {
error,
details: error.details?.map(d => ({
field: d.path.join('.'),
message: d.message,
value: d.context?.value
})),
requestBody: req.body
});
return res.status(400).json(setResponse(error, 'Validation failed', 400));
}
value.updated_by = req.user?.user_id || null;
const results = await BrandService.updateBrandWithFullData(id, value);
const results = await BrandService.updateBrand(id, value);
const response = await setResponse(results, 'Brand updated successfully');
res.status(response.statusCode).json(response);

View File

@@ -1,5 +1,9 @@
const ErrorCodeService = require('../services/error_code.service');
const { setResponse, setResponsePaging } = require('../helpers/utils');
const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils');
const {
insertErrorCodeSchema,
updateErrorCodeSchema,
} = require('../validate/error_code.schema');
class ErrorCodeController {
static async getByBrandId(req, res) {
@@ -23,15 +27,16 @@ class ErrorCodeController {
// Create error code with solutions and spareparts
static async create(req, res) {
const { error, value } = await checkValidate(insertErrorCodeSchema, req);
if (error) {
return res.status(400).json(setResponse(error, 'Validation failed', 400));
}
const { brandId } = req.params;
const createdBy = req.user?.user_id || null;
value.created_by = req.user?.user_id || null;
const data = {
...req.body,
created_by: createdBy
};
const result = await ErrorCodeService.createErrorCodeWithFullData(brandId, data);
const result = await ErrorCodeService.createErrorCodeWithFullData(brandId, value);
const response = setResponse(result, 'Error code created successfully');
res.status(response.statusCode).json(response);
@@ -39,15 +44,16 @@ class ErrorCodeController {
// Update error code with solutions and spareparts
static async update(req, res) {
const { brandId, errorCode } = req.params;
const updatedBy = req.user?.user_id || null;
const { error, value } = await checkValidate(updateErrorCodeSchema, req);
const data = {
...req.body,
updated_by: updatedBy
};
if (error) {
return res.status(400).json(setResponse(error, 'Validation failed', 400));
}
const result = await ErrorCodeService.updateErrorCodeWithFullData(brandId, errorCode, data);
const { brandId, errorCodeId } = req.params;
value.updated_by = req.user?.user_id || null;
const result = await ErrorCodeService.updateErrorCodeWithFullData(brandId, errorCodeId, value);
const response = setResponse(result, 'Error code updated successfully');
res.status(response.statusCode).json(response);
@@ -55,10 +61,10 @@ class ErrorCodeController {
// Soft delete error code
static async delete(req, res) {
const { brandId, errorCode } = req.params;
const { brandId, errorCodeId } = req.params;
const deletedBy = req.user?.user_id || null;
const result = await ErrorCodeService.deleteErrorCode(brandId, errorCode, deletedBy);
const result = await ErrorCodeService.deleteErrorCode(brandId, errorCodeId, deletedBy);
const response = setResponse(result, 'Error code deleted successfully');
res.status(response.statusCode).json(response);

View File

@@ -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;

View File

@@ -1,12 +1,26 @@
const NotificationErrorService = require('../services/notification_error.service');
const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils');
const NotificationErrorService = require("../services/notification_error.service");
const {
setResponse,
setResponsePaging,
checkValidate,
} = require("../helpers/utils");
const {
insertNotificationSchema,
updateNotificationSchema,
} = require("../validate/notification.schema");
class NotificationErrorController {
static async getAll(req, res) {
const queryParams = req.query;
const results = await NotificationErrorService.getAllNotification(queryParams);
const response = await setResponsePaging(queryParams, results, 'Notification found')
const results = await NotificationErrorService.getAllNotification(
queryParams
);
const response = await setResponsePaging(
queryParams,
results,
"Notification found"
);
res.status(response.statusCode).json(response);
}
@@ -15,11 +29,60 @@ class NotificationErrorController {
const { id } = req.params;
const results = await NotificationErrorService.getNotificationById(id);
const response = await setResponse(results, 'Notification retrieved successfully');
const response = await setResponse(
results,
"Notification retrieved successfully"
);
return res.status(response.statusCode).json(response);
}
static async create(req, res) {
const { error, value } = await checkValidate(insertNotificationSchema, req);
if (error) {
return res.status(400).json(setResponse(error, "Validation failed", 400));
}
value.userId = req.user.user_id;
const results = await NotificationErrorService.createNotificationError(
value
);
const response = await setResponse(
results,
"Notification created successfully"
);
return res.status(response.statusCode).json(response);
}
static async update(req, res) {
const { id } = req.params;
const userId = req.user.user_id;
const results = await NotificationErrorService.updateNotificationError(
id,
userId
);
const response = await setResponse(
results,
"Notification Error updated successfully"
);
res.status(response.statusCode).json(response);
}
static async resend(req, res) {
const { id } = req.params;
const results = await NotificationErrorService.resendNotification(id);
const response = await setResponse(
results,
"Notification Error resend successfully"
);
res.status(response.statusCode).json(response);
}
}
module.exports = NotificationErrorController;
module.exports = NotificationErrorController;

View File

@@ -55,9 +55,7 @@ class NotificationErrorLogController {
return res.status(400).json(setResponse(error, 'Validation failed', 400));
}
value.created_by = req.user.user_id;
const results = await NotificationErrorLogService.createNotificationErrorLog(value);
const results = await NotificationErrorLogService.createNotificationErrorLog(value, req.user.user_id);
const response = await setResponse(results, 'Notification Error Log created successfully')
return res.status(response.statusCode).json(response);

View File

@@ -0,0 +1,123 @@
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);
}
static async resendByUser(req, res) {
const { id, contact_phone } = req.params;
const results = await NotificationErrorUserService.resendNotificationByUser(
id,
contact_phone
);
const response = await setResponse(
results,
"Notification Error By User resend successfully"
);
res.status(response.statusCode).json(response);
}
}
module.exports = NotificationErrorUserController;

View File

@@ -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
);

View File

@@ -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
};
};

View File

@@ -1,71 +1,68 @@
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 getDeviceNotificationByIdDb = async (chanel_id) => {
const queryText = `
SELECT
device_code,
device_name,
device_location,
listen_channel
FROM m_device
WHERE listen_channel = $1
AND deleted_at IS NULL
`;
const result = await pool.query(queryText, [chanel_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 +70,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,14 +85,18 @@ 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
);
if (whereParamAnd) queryParams = whereParamAnd;
queryParams = whereParamAnd ? whereParamAnd : queryParams;
const queryText = `
SELECT
@@ -108,27 +109,38 @@ const getAllNotificationDb = async (searchParams = {}) => {
a.is_delivered,
a.is_read,
a.is_active,
a.created_at,
b.error_code,
b.error_code_name,
b.created_at,
b.error_code_color,
b.path_icon,
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 +161,51 @@ 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,
a.updated_at,
c.is_active
FROM notification_error_user a
LEFT JOIN notification_error b ON a.notification_error_id = b.notification_error_id
LEFT JOIN contact c ON a.contact_phone = c.contact_phone
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,
getDeviceNotificationByIdDb,
getAllNotificationDb,
InsertNotificationErrorDb,
updateNotificationErrorDb,
getUsersNotificationErrorDb
};

View File

@@ -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]);
@@ -32,11 +32,15 @@ const getNotificationErrorLogByIdDb = async (id) => {
const getNotificationErrorLogByNotificationErrorIdDb = async (notificationErrorId) => {
const queryText = `
SELECT
a.*,
b.contact_name,
b.contact_type
a.notification_error_log_description,
a.created_at,
b.contact_type,
c.user_fullname as created_by_name,
case when a.created_by is not null then c.user_fullname else b.contact_name end as contact_name,
case when a.created_by is not null then c.user_phone else a.contact_phone end as contact_phone
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
LEFT JOIN m_users c ON a.created_by = c.user_id
WHERE a.notification_error_id = $1 AND a.deleted_at IS NULL
ORDER BY a.created_at DESC
`;
@@ -45,7 +49,27 @@ const getNotificationErrorLogByNotificationErrorIdDb = async (notificationErrorI
};
const createNotificationErrorLogDb = async (store) => {
const { query: queryText, values } = pool.buildDynamicInsert("notification_error_log", store);
const queryText = `
INSERT INTO notification_error_log (
notification_error_id,
contact_phone,
notification_error_log_description,
created_by,
updated_by,
created_at,
updated_at
)
VALUES ($1, $2, $3, $4, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
SELECT SCOPE_IDENTITY() as inserted_id;
`;
const values = [
store.notification_error_id,
store.contact_phone,
store.notification_error_log_description,
store.created_by
];
const result = await pool.query(queryText, values);
const insertedId = result.recordset[0]?.inserted_id;
return insertedId ? await getNotificationErrorLogByIdDb(insertedId) : null;

View File

@@ -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) {

View File

@@ -0,0 +1,147 @@
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.*,
b. is_active as contact_is_active,
d.error_code,
d.error_code_name,
e.device_name
FROM notification_error_user a
LEFT JOIN contact b ON a.contact_phone = b.contact_phone
LEFT JOIN notification_error c ON a.notification_error_id = c.notification_error_id
LEFT JOIN brand_code d ON d.error_code_id = c.error_code_id
LEFT JOIN m_device e ON c.error_chanel = e.listen_channel
WHERE a.notification_error_user_id = $1 AND a.deleted_at IS NULL
`;
const result = await pool.query(queryText, [id]);
return result.recordset;
};
const getNotificationErrorByIdDb = async (notification_error_id) => {
const queryText = `
SELECT
a.*,
b.is_active as contact_is_active,
c.is_read
FROM notification_error_user a
LEFT JOIN contact b ON a.contact_phone = b.contact_phone
LEFT JOIN notification_error c ON a.notification_error_id = c.notification_error_id
WHERE a.notification_error_id = $1 AND a.deleted_at IS NULL
`;
const result = await pool.query(queryText, [notification_error_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,
getNotificationErrorByIdDb,
createNotificationErrorUserDb,
updateNotificationErrorUserDb,
deleteNotificationErrorUserDb,
};

62
db/notification_wa.db.js Normal file
View File

@@ -0,0 +1,62 @@
// db/notification_wa.db.js
const { default: axios } = require('axios');
const CryptoJS = require('crypto-js');
const https = require('https');
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
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}`,{httpsAgent}) ;
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;
try {
const response = await axios.post(endPointWhatsapp, payload,{httpsAgent} );
// 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,
};

View File

@@ -1,6 +1,10 @@
const { ErrorHandler } = require("../helpers/error");
const { getUserByIdDb } = require("../db/user.db");
function isPhoneNumberID(phone) {
return /^(?:\+62|62|0)8[1-9][0-9]{6,10}$/.test(phone);
}
const verifyAccess = (minLevel = 1, allowUnapprovedReadOnly = false) => {
return async (req, res, next) => {
try {
@@ -11,21 +15,31 @@ const verifyAccess = (minLevel = 1, allowUnapprovedReadOnly = false) => {
// Super Admin bypass semua
if (user.is_sa) return next();
const fullUser = await getUserByIdDb(user.user_id);
if (!fullUser) throw new ErrorHandler(403, "Forbidden: User not found");
if (!fullUser.is_approve) {
if (req.method !== "GET") {
throw new ErrorHandler(403, "Account not approved — read-only access");
if (!isPhoneNumberID(user.user_id) && user.user_id) {
const fullUser = await getUserByIdDb(user.user_id);
if (!fullUser) throw new ErrorHandler(403, "Forbidden: User not found");
if (!fullUser.is_approve) {
if (req.method !== "GET") {
throw new ErrorHandler(403, "Account not approved — read-only access");
}
if (allowUnapprovedReadOnly) return next();
throw new ErrorHandler(403, "Account not approved");
}
if (allowUnapprovedReadOnly) return next();
if (!fullUser.role_level || fullUser.role_level < minLevel) {
throw new ErrorHandler(403, "Forbidden: Insufficient role level");
}
} else {
if (req.method !== 'GET' && req.baseUrl !== '/api/notification-log') {
if (req.baseUrl !== '/api/notification') {
throw new ErrorHandler(403, "Forbidden: Insufficient Access");
}
}
throw new ErrorHandler(403, "Account not approved");
}
if (!fullUser.role_level || fullUser.role_level < minLevel) {
throw new ErrorHandler(403, "Forbidden: Insufficient role level");
}
next();

View File

@@ -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;

View File

@@ -9,7 +9,7 @@ router.route('/brand/:brandId')
.get(verifyToken.verifyAccessToken, ErrorCodeController.getByBrandId)
.post(verifyToken.verifyAccessToken, verifyAccess(), ErrorCodeController.create);
router.route('/brand/:brandId/:errorCode')
router.route('/brand/:brandId/:errorCodeId')
.put(verifyToken.verifyAccessToken, verifyAccess(), ErrorCodeController.update)
.delete(verifyToken.verifyAccessToken, verifyAccess(), ErrorCodeController.delete);

View File

@@ -18,6 +18,7 @@ const notificationError = require("./notification_error.route")
const notificationErrorSparepart = require("./notification_error_sparepart.route")
const sparepart = require("./sparepart.route")
const notificationErrorLog = require("./notification_error_log.route")
const notificationErrorUser = require("./notification_error_user.route")
const errorCode = require("./error_code.route")
router.use("/auth", auth);
@@ -39,6 +40,7 @@ router.use("/notification", notificationError)
router.use("/notification-sparepart", notificationErrorSparepart)
router.use("/sparepart", sparepart)
router.use("/notification-log", notificationErrorLog)
router.use("/notification-user", notificationErrorUser)
router.use("/error-code", errorCode)
module.exports = router;

View File

@@ -1,16 +1,40 @@
const express = require('express');
const NotificationErrorController = require('../controllers/notification_error.controller');
const verifyToken = require('../middleware/verifyToken');
const verifyAccess = require('../middleware/verifyAccess');
const express = require("express");
const NotificationErrorController = require("../controllers/notification_error.controller");
const verifyToken = require("../middleware/verifyToken");
const verifyAccess = require("../middleware/verifyAccess");
const router = express.Router();
router
.route('/')
.get(verifyToken.verifyAccessToken, NotificationErrorController.getAll)
.route("/")
.get(
verifyToken.verifyAccessToken,
verifyAccess(),
NotificationErrorController.getAll
);
router
.route('/:id')
.route("/")
.post(
verifyToken.verifyAccessToken,
verifyAccess(),
NotificationErrorController.create
);
router
.route("/:id")
.get(verifyToken.verifyAccessToken, NotificationErrorController.getById)
.put(
verifyToken.verifyAccessToken,
verifyAccess(),
NotificationErrorController.update
);
router.post(
"/resend/:id",
verifyToken.verifyAccessToken,
verifyAccess(),
NotificationErrorController.resend
);
module.exports = router;

View File

@@ -7,7 +7,10 @@ const router = express.Router();
router.route("/")
.get(verifyToken.verifyAccessToken, NotificationErrorLogController.getAll)
.post(verifyToken.verifyAccessToken, verifyAccess(), NotificationErrorLogController.create);
.post(
verifyToken.verifyAccessToken,
verifyAccess(),
NotificationErrorLogController.create);
router.route("/:id")
.get(verifyToken.verifyAccessToken, NotificationErrorLogController.getById);

View File

@@ -0,0 +1,38 @@
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
);
router.post(
"/resend/:id/:contact_phone",
verifyToken.verifyAccessToken,
verifyAccess(),
NotificationErrorUserController.resendByUser
);
module.exports = router;

View File

@@ -7,32 +7,6 @@ const {
deleteBrandDb,
checkBrandNameExistsDb,
} = require("../db/brand.db");
const {
insertMultipleErrorCodeSparepartsDb,
updateErrorCodeSparepartsDb,
getSparepartsByErrorCodeIdDb,
} = require("../db/brand_sparepart.db");
// Error code operations
const {
getErrorCodesByBrandIdDb,
createErrorCodeDb,
updateErrorCodeDb,
deleteErrorCodeDb,
} = require("../db/brand_code.db");
// Sparepart operations
const { getSparepartsByIdsDb } = require("../db/sparepart.db");
// Solution operations
const {
getSolutionsByErrorCodeIdDb,
createSolutionDb,
updateSolutionDb,
deleteSolutionDb,
} = require("../db/brand_code_solution.db");
const { getFileUploadByPathDb } = require("../db/file_uploads.db");
const { ErrorHandler } = require("../helpers/error");
class BrandService {
@@ -41,7 +15,6 @@ class BrandService {
try {
const results = await getAllBrandsDb(param);
// Return brands data - spareparts are now associated with error codes, not brands
return {
...results,
data: results.data
@@ -65,10 +38,8 @@ class BrandService {
// Create brand
static async createBrandWithFullData(data) {
static async createBrand(data) {
try {
if (!data || typeof data !== "object") data = {};
if (data.brand_name) {
const brandExists = await checkBrandNameExistsDb(data.brand_name);
if (brandExists) {
@@ -76,85 +47,23 @@ class BrandService {
}
}
if (
!data.error_code ||
!Array.isArray(data.error_code) ||
data.error_code.length === 0
) {
throw new ErrorHandler(
400,
"Brand must have at least 1 error code with solution"
);
}
for (const errorCode of data.error_code) {
if (
!errorCode.solution ||
!Array.isArray(errorCode.solution) ||
errorCode.solution.length === 0
) {
throw new ErrorHandler(
400,
`Error code ${errorCode.error_code} must have at least 1 solution`
);
}
}
const brandData = {
brand_name: data.brand_name,
brand_type: data.brand_type,
brand_manufacture: data.brand_manufacture,
brand_model: data.brand_model,
is_active: data.is_active,
is_active: data.is_active !== undefined ? data.is_active : true,
created_by: data.created_by,
};
const createdBrand = await createBrandDb(brandData);
if (!createdBrand) {
throw new Error("Failed to create brand");
throw new ErrorHandler(500, "Failed to create brand");
}
const brandId = createdBrand.brand_id;
for (const errorCodeData of data.error_code) {
const errorId = await createErrorCodeDb(brandId, {
error_code: errorCodeData.error_code,
error_code_name: errorCodeData.error_code_name,
error_code_description: errorCodeData.error_code_description,
error_code_color: errorCodeData.error_code_color,
path_icon: errorCodeData.path_icon,
is_active: errorCodeData.is_active,
created_by: data.created_by,
});
if (!errorId) {
throw new Error("Failed to create error code");
}
// Create sparepart relationships for this error code
if (errorCodeData.spareparts && Array.isArray(errorCodeData.spareparts)) {
await insertMultipleErrorCodeSparepartsDb(errorId, errorCodeData.spareparts, data.created_by);
}
// Create solutions for this error code
if (errorCodeData.solution && Array.isArray(errorCodeData.solution)) {
for (const solutionData of errorCodeData.solution) {
await createSolutionDb(errorId, {
solution_name: solutionData.solution_name,
type_solution: solutionData.type_solution,
text_solution: solutionData.text_solution || null,
path_solution: solutionData.path_solution || null,
is_active: solutionData.is_active,
created_by: data.created_by,
});
}
}
}
const createdBrandWithSpareparts = await this.getBrandById(brandId);
return createdBrandWithSpareparts;
return createdBrand;
} catch (error) {
throw new ErrorHandler(500, `Bulk insert failed: ${error.message}`);
throw new ErrorHandler(error.statusCode || 500, error.message);
}
}
@@ -177,12 +86,11 @@ class BrandService {
// Update brand
static async updateBrandWithFullData(id, data) {
static async updateBrand(id, data) {
try {
const existingBrand = await getBrandByIdDb(id);
if (!existingBrand) throw new ErrorHandler(404, "Brand not found");
if (data.brand_name && data.brand_name !== existingBrand.brand_name) {
const brandExists = await checkBrandNameExistsDb(data.brand_name, id);
if (brandExists) {
@@ -191,147 +99,22 @@ class BrandService {
}
const brandData = {
brand_name: data.brand_name,
brand_type: data.brand_type,
brand_manufacture: data.brand_manufacture,
brand_model: data.brand_model,
is_active: data.is_active,
brand_name: data.brand_name || existingBrand.brand_name,
brand_type: data.brand_type !== undefined ? data.brand_type : existingBrand.brand_type,
brand_manufacture: data.brand_manufacture !== undefined ? data.brand_manufacture : existingBrand.brand_manufacture,
brand_model: data.brand_model !== undefined ? data.brand_model : existingBrand.brand_model,
is_active: data.is_active !== undefined ? data.is_active : existingBrand.is_active,
updated_by: data.updated_by,
};
await updateBrandDb(existingBrand.brand_name, brandData);
if (data.error_code && Array.isArray(data.error_code)) {
const existingErrorCodes = await getErrorCodesByBrandIdDb(id);
const incomingErrorCodes = data.error_code.map((ec) => ec.error_code);
// Create/update/delete error codes
for (const errorCodeData of data.error_code) {
// Check if error code already exists
const existingEC = existingErrorCodes.find(
(ec) => ec.error_code === errorCodeData.error_code
);
if (existingEC) {
// Update existing error code using separate db function
await updateErrorCodeDb(
existingEC.brand_id,
existingEC.error_code,
{
error_code_name: errorCodeData.error_code_name,
error_code_description: errorCodeData.error_code_description,
error_code_color: errorCodeData.error_code_color,
path_icon: errorCodeData.path_icon,
is_active: errorCodeData.is_active,
updated_by: data.updated_by,
}
);
if (errorCodeData.spareparts && Array.isArray(errorCodeData.spareparts)) {
await updateErrorCodeSparepartsDb(existingEC.error_code_id, errorCodeData.spareparts, data.updated_by);
}
if (
errorCodeData.solution &&
Array.isArray(errorCodeData.solution)
) {
const existingSolutions = await getSolutionsByErrorCodeIdDb(
existingEC.error_code_id
);
const incomingSolutionNames = errorCodeData.solution.map(
(s) => s.solution_name
);
// Update or create solutions
for (const solutionData of errorCodeData.solution) {
const existingSolution = existingSolutions.find(
(s) => s.solution_name === solutionData.solution_name
);
if (existingSolution) {
// Update existing solution
await updateSolutionDb(
existingSolution.brand_code_solution_id,
{
solution_name: solutionData.solution_name,
type_solution: solutionData.type_solution,
text_solution: solutionData.text_solution || null,
path_solution: solutionData.path_solution || null,
is_active: solutionData.is_active,
updated_by: data.updated_by,
}
);
} else {
// Create new solution
await createSolutionDb(existingEC.error_code_id, {
solution_name: solutionData.solution_name,
type_solution: solutionData.type_solution,
text_solution: solutionData.text_solution || null,
path_solution: solutionData.path_solution || null,
is_active: solutionData.is_active,
created_by: data.updated_by,
});
}
}
// Delete solutions that are not in the incoming request
for (const existingSolution of existingSolutions) {
if (
!incomingSolutionNames.includes(
existingSolution.solution_name
)
) {
await deleteSolutionDb(
existingSolution.brand_code_solution_id,
data.updated_by
);
}
}
}
} else {
const errorId = await createErrorCodeDb(id, {
error_code: errorCodeData.error_code,
error_code_name: errorCodeData.error_code_name,
error_code_description: errorCodeData.error_code_description,
error_code_color: errorCodeData.error_code_color,
path_icon: errorCodeData.path_icon,
is_active: errorCodeData.is_active,
created_by: data.updated_by,
});
if (errorCodeData.spareparts && Array.isArray(errorCodeData.spareparts)) {
await insertMultipleErrorCodeSparepartsDb(errorId, errorCodeData.spareparts, data.updated_by);
}
if (
errorCodeData.solution &&
Array.isArray(errorCodeData.solution)
) {
for (const solutionData of errorCodeData.solution) {
await createSolutionDb(errorId, {
solution_name: solutionData.solution_name,
type_solution: solutionData.type_solution,
text_solution: solutionData.text_solution || null,
path_solution: solutionData.path_solution || null,
is_active: solutionData.is_active,
created_by: data.updated_by,
});
}
}
}
}
for (const existingEC of existingErrorCodes) {
if (!incomingErrorCodes.includes(existingEC.error_code)) {
await deleteErrorCodeDb(id, existingEC.error_code, data.updated_by);
}
}
const updatedBrand = await updateBrandDb(existingBrand.brand_name, brandData);
if (!updatedBrand) {
throw new ErrorHandler(500, "Failed to update brand");
}
const updatedBrandWithSpareparts = await this.getBrandById(id);
return updatedBrandWithSpareparts;
return updatedBrand;
} catch (error) {
throw new ErrorHandler(500, `Update failed: ${error.message}`);
throw new ErrorHandler(error.statusCode || 500, error.message);
}
}
}

View File

@@ -25,7 +25,6 @@ const {
const { getFileUploadByPathDb } = require("../db/file_uploads.db");
class ErrorCodeService {
// Get all error codes with pagination and search (without solutions and spareparts)
static async getAllErrorCodes(param) {
try {
const results = await getAllErrorCodesDb(param);
@@ -120,17 +119,6 @@ class ErrorCodeService {
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,
@@ -169,19 +157,36 @@ class ErrorCodeService {
}
// Update error code with solutions and spareparts
static async updateErrorCodeWithFullData(brandId, errorCode, data) {
static async updateErrorCodeWithFullData(brandId, errorCodeId, data) {
try {
const existingErrorCode = await getErrorCodeByBrandAndCodeDb(brandId, errorCode);
const existingErrorCode = await getErrorCodeByIdDb(errorCodeId);
if (!existingErrorCode) throw new ErrorHandler(404, "Error code not found");
await updateErrorCodeDb(brandId, errorCode, {
error_code_name: data.error_code_name,
error_code_description: data.error_code_description,
error_code_color: data.error_code_color,
path_icon: data.path_icon,
is_active: data.is_active,
updated_by: data.updated_by,
});
// Verify the error code belongs to the specified brand
if (existingErrorCode.brand_id !== parseInt(brandId)) {
throw new ErrorHandler(403, "Error code does not belong to specified brand");
}
// Check if there are any error code fields to update
const hasMainFieldUpdate =
data.error_code !== 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);
@@ -235,15 +240,20 @@ class ErrorCodeService {
}
// Soft delete error code
static async deleteErrorCode(brandId, errorCode, deletedBy) {
static async deleteErrorCode(brandId, errorCodeId, deletedBy) {
try {
const errorCodeExist = await getErrorCodeByBrandAndCodeDb(brandId, errorCode);
const errorCodeExist = await getErrorCodeByIdDb(errorCodeId);
if (!errorCodeExist) {
throw new ErrorHandler(404, "Error code not found");
}
const result = await deleteErrorCodeDb(brandId, errorCode, deletedBy);
// Verify the error code belongs to the specified brand
if (errorCodeExist.brand_id !== parseInt(brandId)) {
throw new ErrorHandler(403, "Error code does not belong to specified brand");
}
const result = await deleteErrorCodeDb(brandId, errorCodeExist.error_code, deletedBy);
return result;
} catch (error) {

View File

@@ -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;

View File

@@ -1,64 +1,111 @@
// services/notification_error.service.js
const {
getAllNotificationDb,
getNotificationByIdDb,
} = require('../db/notification_error.db');
InsertNotificationErrorDb,
getUsersNotificationErrorDb,
updateNotificationErrorDb,
} = require("../db/notification_error.db");
const {
getErrorCodeByIdDb,
} = require('../db/brand_code.db');
const {
getSolutionsByErrorCodeIdDb,
} = require('../db/brand_code_solution.db');
const { getErrorCodeByIdDb } = require("../db/brand_code.db");
const { getSolutionsByErrorCodeIdDb } = require("../db/brand_code_solution.db");
const {
getAllNotificationErrorLogDb,
getNotificationErrorLogByNotificationErrorIdDb,
} = require('../db/notification_error_log.db');
} = require("../db/notification_error_log.db");
const { getFileUploadByPathDb } = require('../db/file_uploads.db');
const { getSparepartsByErrorCodeIdDb } = require("../db/brand_sparepart.db");
const { ErrorHandler } = require('../helpers/error');
const {
getNotificationErrorByIdDb,
} = require("../db/notification_error_user.db");
const { getFileUploadByPathDb } = require("../db/file_uploads.db");
const {
generateTokenRedirect,
shortUrltiny,
sendNotifikasi,
} = require("../db/notification_wa.db");
const { ErrorHandler } = require("../helpers/error");
class NotificationService {
// Get all Notifications
static async getAllNotification(param) {
try {
const results = await getAllNotificationDb(param);
results.data.map(element => {
});
if (results && Array.isArray(results.data)) {
results.data = await Promise.all(
results.data.map(async (notification) => {
const usersNotification =
(await getUsersNotificationErrorDb(
notification.notification_error_id
)) || [];
return {
...notification,
users: usersNotification,
};
})
);
}
return results;
} catch (error) {
throw new ErrorHandler(error.statusCode, error.message);
}
}
// Get notification by ID
static async createNotificationError(data) {
try {
if (!data || typeof data !== "object") data = {};
const result = await InsertNotificationErrorDb(data);
return result;
} catch (error) {
return error;
}
}
static async getNotificationById(id) {
try {
const notification = await getNotificationByIdDb(id);
if (!notification || (Array.isArray(notification) && notification.length < 1)) {
throw new ErrorHandler(404, 'Notification not found');
if (
!notification ||
(Array.isArray(notification) && notification.length < 1)
) {
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);
if (errorCode) {
// Get solutions for this error code
const solutions = (await getSolutionsByErrorCodeIdDb(errorCode.error_code_id)) || [];
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;
if (solution.path_solution && solution.type_solution && solution.type_solution !== 'text') {
if (
solution.path_solution &&
solution.type_solution &&
solution.type_solution !== "text"
) {
try {
fileData = await getFileUploadByPathDb(solution.path_solution);
fileData = await getFileUploadByPathDb(
solution.path_solution
);
} catch (e) {
fileData = null;
}
@@ -66,20 +113,24 @@ class NotificationService {
return {
...solution,
file_upload_name: fileData?.file_upload_name || null,
path_document: fileData?.path_document || null
path_document: fileData?.path_document || null,
};
})
);
notification.error_code = {
...errorCode,
solution: solutionsWithDetails
solution: solutionsWithDetails,
spareparts: spareparts,
};
}
}
// Get activity logs for this notification
const notificationLogs = (await getNotificationErrorLogByNotificationErrorIdDb(id)) || [];
const notificationLogs =
(await getNotificationErrorLogByNotificationErrorIdDb(id)) || [];
notification.users = usersNotification;
notification.activity_logs = notificationLogs;
@@ -88,6 +139,128 @@ class NotificationService {
throw new ErrorHandler(error.statusCode, error.message);
}
}
static async updateNotificationError(notification_error_id, data) {
try {
const dataExist = await getNotificationErrorByIdDb(notification_error_id);
if (!dataExist || (Array.isArray(dataExist) && dataExist.length < 1)) {
throw new ErrorHandler(404, "Notification Error User not found");
}
const notification = Array.isArray(dataExist) ? dataExist[0] : dataExist;
if (notification.is_read === true) {
return { success: true, message: "Notification has already been read" };
}
if (!notification.is_read) {
const updateStatus = await updateNotificationErrorDb(
notification_error_id,
{ is_read: true }
);
if (!updateStatus) {
throw new ErrorHandler(500, "Failed to update notification");
}
}
return { success: true, message: "Notification marked as read" };
} catch (error) {
throw new ErrorHandler(error.statusCode || 500, error.message);
}
}
static async resendNotification(id) {
const deviceNotification = await getNotificationByIdDb(id);
if (!deviceNotification) throw new ErrorHandler(404, "Data not found");
const errorCode = await getErrorCodeByIdDb(
deviceNotification.error_code_id
);
const dataExist = await getUsersNotificationErrorDb(id);
const activeUsers =
dataExist?.filter((user) => user.is_active === true) || [];
if (activeUsers.length < 1)
throw new ErrorHandler(404, "No active contacts");
this._executeResendWa(
id,
activeUsers,
deviceNotification,
errorCode
).catch((err) => console.error("Background Process Error:", err));
return {
count: activeUsers.length,
};
}
static async _executeResendWa(
id,
activeUsers,
deviceNotification,
errorCode
) {
console.log(`user active: `, id, activeUsers);
const sendPromises = activeUsers.map(async (user) => {
try {
console.log(`user: ${user.contact_name} (${user.contact_phone})`);
const tokenRedirect = await generateTokenRedirect(
user.contact_phone,
user.contact_name,
id
);
const encodedToken = encodeURIComponent(tokenRedirect);
console.log("token: ", tokenRedirect);
const shortUrl = await shortUrltiny(encodedToken);
console.log("URL:", shortUrl);
const bodyWithUrl =
`Hai ${user.contact_name || "-"}\n` +
`Terjadi peringatan dengan kode ${errorCode?.error_code || "-"} - ${
errorCode?.error_code_name
} pada device ${deviceNotification.device_name || "-"}.\n` +
`Silahkan cek detail pada link berikut:\n ${shortUrl}`;
const resultSend = await sendNotifikasi(
user.contact_phone,
bodyWithUrl
);
console.log("notifikasi wa:", resultSend)
const isSuccess = resultSend?.error ? false : true;
await updateNotificationErrorDb(user.notification_error_id, {
is_send: isSuccess,
is_delivered: isSuccess,
});
return { phone: user.contact_phone, status: "success" };
} catch (err) {
console.error(`Gagal mengirim ke ${user.contact_phone}:`, err.message);
return {
phone: user.contact_phone,
status: "failed",
error: err.message,
};
}
});
const results = await Promise.all(sendPromises);
console.log("result akhir: ", results)
console.log(
`Resend chat: ${
results.filter((r) => r.status === "success").length
}, Gagal: ${results.filter((r) => r.status === "failed").length}`
);
}
}
module.exports = NotificationService;

View File

@@ -7,6 +7,7 @@ const {
deleteNotificationErrorLogDb
} = require('../db/notification_error_log.db');
const { getUserByIdDb } = require('../db/user.db');
const { ErrorHandler } = require('../helpers/error');
class NotificationErrorLogService {
@@ -40,15 +41,34 @@ class NotificationErrorLogService {
}
// Create Notification Error Log
static async createNotificationErrorLog(data) {
static async createNotificationErrorLog(data, userId) {
try {
if (!data || typeof data !== 'object') data = {};
let createdBy = null;
let contactPhone = data.contact_phone || null;
if (userId) {
try {
const user = await getUserByIdDb(userId);
if (user && user.user_id) {
createdBy = Number(user.user_id);
} else {
createdBy = null;
contactPhone = userId;
}
} catch (dbError) {
createdBy = null;
contactPhone = userId;
}
}
const store = {
notification_error_id: data.notification_error_id,
contact_id: data.contact_id,
contact_phone: contactPhone,
notification_error_log_description: data.notification_error_log_description,
created_by: data.created_by
created_by: createdBy
};
const result = await createNotificationErrorLogDb(store);

View File

@@ -0,0 +1,195 @@
const {
getAllNotificationErrorUserDb,
getNotificationErrorUserByIdDb,
createNotificationErrorUserDb,
updateNotificationErrorUserDb,
deleteNotificationErrorUserDb,
} = require("../db/notification_error_user.db");
const {
generateTokenRedirect,
shortUrltiny,
sendNotifikasi,
} = require("../db/notification_wa.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, "Notification Error User 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, "Notification Error User 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, "Notification Error User not found");
}
const result = await deleteNotificationErrorUserDb(id, userId);
return result;
} catch (error) {
throw new ErrorHandler(error.statusCode, error.message);
}
}
static async resendNotificationByUser(id, contact_phone) {
const results = [];
const idUser = Array.isArray(id) ? id : [id];
for (const id of idUser) {
try {
const dataExist = await getNotificationErrorUserByIdDb(id);
if (!dataExist || dataExist.length < 1) {
results.push({
id,
status: "failed",
message: "Data Notification Error User not found",
});
continue;
}
const data = dataExist[0];
if (data.contact_phone !== contact_phone) {
results.push({
id,
status: "failed",
message: `Phone ${contact_phone} not found.`,
});
continue;
}
if (data.contact_is_active === false) {
results.push({
id,
status: "failed",
message: `Contact ${data.contact_phone} not active.`,
});
continue;
}
const tokenRedirect = await generateTokenRedirect(
data.contact_phone,
data.contact_name,
data.notification_error_id
);
const encodedToken = encodeURIComponent(tokenRedirect);
const shortUrl = await shortUrltiny(encodedToken);
const bodyWithUrl =
`Hai ${data.contact_name || "-"}\n` +
`Terjadi peringatan dengan kode error ${data.error_code || "-"} - ${
data.error_code_name || "-"
} ` +
`pada device ${
data.device_name || "-"
}, silahkan cek detail pada link berikut:\n` +
`${shortUrl}`;
const resultSend = await sendNotifikasi(
data.contact_phone,
bodyWithUrl
);
const isSuccess = resultSend?.error ? false : true;
const updateData = {
is_send: isSuccess,
message_error_issue: bodyWithUrl,
};
await updateNotificationErrorUserDb(id, updateData);
if (!isSuccess) {
results.push({
id,
status: "failed",
message: `WhatsApp API Gagal: ${
resultSend?.message || "Unknown Error"
}`,
});
} else {
results.push({
status: 200,
notification_error_user_id: id,
notification_error_id: data.notification_error_id,
device_name: data.device_name,
error_code: data?.error_code,
error_code_name: data?.error_code_name,
users: {
contact_name: data.contact_name,
contact_phone: data.contact_phone,
is_send: isSuccess,
},
});
}
} catch (err) {
results.push({ id, status: "error", message: err.message });
}
}
return results;
}
}
module.exports = NotificationErrorUserService;

View File

@@ -0,0 +1,127 @@
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");
const { getErrorCodeByIdDb } = require("../db/brand_code.db");
const { getDeviceNotificationByIdDb } = require("../db/notification_error.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 === process.env.TOPIC_COD ?? "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 dataUsers = results.data;
for (const chanel of resultChanel) {
const errorCode = await getErrorCodeByIdDb(chanel.value);
const deviceNotification = await getDeviceNotificationByIdDb(
chanel.chanel_id
);
const data = {
error_code_id: chanel.value,
error_chanel: chanel.chanel_id,
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 tokenRedirect = await generateTokenRedirect(
dataUser.contact_phone,
dataUser.contact_name,
resultNotificationError.notification_error_id
);
const encodedToken = encodeURIComponent(tokenRedirect);
const shortUrl = await shortUrltiny(encodedToken);
const bodyMessage =
`Hai ${dataUser.contact_name || "-"}\n` +
`Terjadi peringatan dengan kode error ${errorCode?.error_code || "-"
} - ${errorCode?.error_code_name || "-"} ` +
`pada device ${deviceNotification?.device_name || "-"
}, silahkan cek detail pada link berikut:\n` +
`${shortUrl}`;
const param = {
idData: resultNotificationError.notification_error_id,
userPhone: dataUser.contact_phone,
userName: dataUser.contact_name,
bodyMessage: bodyMessage,
};
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
);
console.log("resultSend: ", resultSend);
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();

View File

@@ -8,91 +8,18 @@ const insertBrandSchema = Joi.object({
brand_type: Joi.string().max(50).optional().allow(""),
brand_manufacture: Joi.string().max(100).required(),
brand_model: Joi.string().max(100).optional().allow(""),
is_active: Joi.boolean().required(),
is_active: Joi.boolean().optional().default(true),
description: Joi.string().max(255).optional().allow(""),
error_code: Joi.array()
.items(
Joi.object({
error_code: Joi.string().max(100).required(),
error_code_name: Joi.string().max(100).required(),
error_code_description: Joi.string().optional().allow(""),
error_code_color: Joi.string().optional().allow(""),
path_icon: Joi.string().optional().allow(""),
is_active: Joi.boolean().required(),
what_action_to_take: Joi.string().optional().allow(""),
spareparts: Joi.array().items(Joi.number().integer()).optional(),
solution: Joi.array()
.items(
Joi.object({
solution_name: Joi.string().max(100).required(),
type_solution: Joi.string()
.valid("text", "pdf", "image", "video", "link")
.required(),
text_solution: Joi.when("type_solution", {
is: "text",
then: Joi.string().required(),
otherwise: Joi.string().optional().allow(""),
}),
path_solution: Joi.when("type_solution", {
is: "text",
then: Joi.string().optional().allow(""),
otherwise: Joi.string().required(),
}),
is_active: Joi.boolean().required(),
})
)
.min(1)
.required(),
})
)
.min(1)
.required(),
});
// Update Brand Validation
const updateBrandSchema = Joi.object({
brand_name: Joi.string().max(100).required(),
brand_name: Joi.string().max(100).optional(),
brand_type: Joi.string().max(50).optional().allow(""),
brand_manufacture: Joi.string().max(100).required(),
brand_manufacture: Joi.string().max(100).optional(),
brand_model: Joi.string().max(100).optional().allow(""),
is_active: Joi.boolean().required(),
is_active: Joi.boolean().optional(),
description: Joi.string().max(255).optional().allow(""),
error_code: Joi.array()
.items(
Joi.object({
error_code: Joi.string().max(100).required(),
error_code_name: Joi.string().max(100).required(),
error_code_description: Joi.string().optional().allow(""),
error_code_color: Joi.string().optional().allow(""),
path_icon: Joi.string().optional().allow(""),
is_active: Joi.boolean().required(),
what_action_to_take: Joi.string().optional().allow(""),
spareparts: Joi.array().items(Joi.number().integer()).optional(),
solution: Joi.array()
.items(
Joi.object({
solution_name: Joi.string().max(100).required(),
type_solution: Joi.string()
.valid("text", "pdf", "image", "video", "link")
.required(),
text_solution: Joi.when("type_solution", {
is: "text",
then: Joi.string().required(),
otherwise: Joi.string().optional().allow(""),
}),
path_solution: Joi.when("type_solution", {
is: "text",
then: Joi.string().optional().allow(""),
otherwise: Joi.string().required(),
}),
is_active: Joi.boolean().optional(),
})
)
.min(1)
.required(),
})
)
.optional(),
}).min(1);
module.exports = {

View File

@@ -13,7 +13,7 @@ const insertContactSchema = Joi.object({
"Phone number must be a valid Indonesian number in format +628XXXXXXXXX",
}),
is_active: Joi.boolean().required(),
contact_type: Joi.string().max(255).optional()
contact_type: Joi.string().max(255).optional().allow(null)
});
const updateContactSchema = Joi.object({
@@ -26,7 +26,7 @@ const updateContactSchema = Joi.object({
"Phone number must be a valid Indonesian number in format +628XXXXXXXXX",
}),
is_active: Joi.boolean().optional(),
contact_type: Joi.string().max(255).optional()
contact_type: Joi.string().max(255).optional().allow(null)
});
module.exports = {

View File

@@ -9,7 +9,7 @@ const insertDeviceSchema = Joi.object({
is_active: Joi.boolean().required(),
brand_id: Joi.number().integer().min(1),
device_location: Joi.string().max(100).required(),
device_description: Joi.string().required(),
device_description: Joi.string(),
ip_address: Joi.string()
.ip({ version: ['ipv4', 'ipv6'] })
.required()

View File

@@ -0,0 +1,73 @@
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)
.optional(),
// .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,
};

View File

@@ -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,25 +40,11 @@ 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",
}),
is_delivered: Joi.boolean().optional().messages({
"boolean.base": "is_delivered must be a boolean",
}),
is_read: Joi.boolean().optional().messages({
"boolean.base": "is_read must be a boolean",
}),
is_active: Joi.boolean().optional().messages({
"boolean.base": "is_active must be a boolean",
}),
});
module.exports = {

View File

@@ -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()
});

View File

@@ -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,
};