diff --git a/db/contact.db.js b/db/contact.db.js index 2b1cff5..1b653ee 100644 --- a/db/contact.db.js +++ b/db/contact.db.js @@ -25,6 +25,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" }, + { column: "a.email", param: searchParams.email, type: "string" }, ], queryParams ); diff --git a/db/device.db.js b/db/device.db.js index b2b9968..390e80f 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -139,11 +139,26 @@ const getDeviceReminderChanelDb = async (id) => { return result.recordset; }; +const getDeviceReminderMonthlyDb = async (id) => { + const queryText = ` + SELECT + a.*, + b.brand_name, + COALESCE(a.device_code, '') + ' - ' + COALESCE(a.device_name, '') AS device_code_name + FROM m_device a + LEFT JOIN m_brands b ON a.brand_id = b.brand_id + WHERE a.device_id = $1 AND a.deleted_at IS NULL AND a.reminder_at_monthly IS NOT NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + module.exports = { getAllDevicesDb, getDeviceByIdDb, createDeviceDb, updateDeviceDb, deleteDeviceDb, - getDeviceReminderChanelDb + getDeviceReminderChanelDb, + getDeviceReminderMonthlyDb }; diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 3e7505f..3c52b5f 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -255,6 +255,23 @@ const getReminderNotificationErrorByYearlyDb = async (errorCode, chanel, year) = return result.recordset[0]; }; +const getReminderNotificationErrorByMonthlyDb = async (errorCode, monthly) => { + const queryText = ` + SELECT a.* + + FROM notification_error a + + WHERE a.error_code_id = $1 + AND MONTH(a.created_at) = $2 + AND a.message_error_issue LIKE 'reminder%' + AND a.is_active = 1 + AND a.deleted_at IS NULL + `; + + const result = await pool.query(queryText, [errorCode, monthly]); + return result.recordset[0]; +}; + module.exports = { getNotificationByIdDb, getDeviceNotificationByIdDb, @@ -264,6 +281,7 @@ module.exports = { getUsersNotificationErrorDb, getDeviceChannelReminder, getReminderNotificationErrorByYearlyDb, + getReminderNotificationErrorByMonthlyDb, updateNotificationErrorByChanelReminderDb }; diff --git a/db/sparepart.db.js b/db/sparepart.db.js index d6d50f0..f27b038 100644 --- a/db/sparepart.db.js +++ b/db/sparepart.db.js @@ -197,6 +197,24 @@ const getSparepartsByYearlyDb = async (yearly) => { return result.recordset; }; +const getSparepartsByMonthlyDb = async (monthly) => { + + const queryText = ` + SELECT * + FROM m_sparepart + WHERE deleted_at IS NULL + AND EXISTS ( + SELECT 1 + FROM OPENJSON(sparepart_monthly) + WHERE value = $1 + ) + `; + + const result = await pool.query(queryText, [monthly]); + + return result.recordset; +}; + module.exports = { getAllSparepartDb, getSparepartByIdDb, @@ -205,5 +223,6 @@ module.exports = { createSparepartDb, updateSparepartDb, deleteSparepartDb, - getSparepartsByYearlyDb + getSparepartsByYearlyDb, + getSparepartsByMonthlyDb }; diff --git a/routes/notifikasi-wa.route.js b/routes/notifikasi-wa.route.js index 8fd7b3a..3c7ce46 100644 --- a/routes/notifikasi-wa.route.js +++ b/routes/notifikasi-wa.route.js @@ -11,4 +11,14 @@ router.post('/restart-wa', async (req, res) => { } }); +router.post('/reminder-sparepart/:id', async (req, res) => { + try { + const { id } = req.params; + const result = await NotifikasiWaService.onMonthlySparepartReminder(id); + return res.status(200).json(result); + } catch (error) { + return res.status(500).json(error); + } +}); + module.exports = router; \ No newline at end of file diff --git a/scheduler/log.json b/scheduler/log.json deleted file mode 100644 index 0637a08..0000000 --- a/scheduler/log.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/services/notification_error.service.js b/services/notification_error.service.js index 03f423a..638ab42 100644 --- a/services/notification_error.service.js +++ b/services/notification_error.service.js @@ -31,7 +31,7 @@ const { } = require("../db/notification_wa.db"); const { ErrorHandler } = require("../helpers/error"); -const { getSparepartsByYearlyDb } = require("../db/sparepart.db"); +const { getSparepartsByYearlyDb, getSparepartsByMonthlyDb } = require("../db/sparepart.db"); const notifikasiWaService = require("./notifikasi-wa.service"); @@ -140,11 +140,18 @@ class NotificationService { notification.is_reminder = false; notification.spareparts_reminder = []; if (notification.message_error_issue) { - const sparepartsReminder = await getSparepartsByYearlyDb(notification.error_code_id); - notification.spareparts_reminder = sparepartsReminder ?? []; + const [sparepartsYearly, sparepartsMonthly] = await Promise.all([ + getSparepartsByYearlyDb(notification.error_code_id), + getSparepartsByMonthlyDb(notification.error_code_id) + ]); + + notification.spareparts_reminder = [ + ...(sparepartsYearly ?? []), + ...(sparepartsMonthly ?? []) + ]; + notification.is_reminder = true; - } return notification; @@ -172,7 +179,7 @@ class NotificationService { if (!notification.is_read) { const updateStatus = await updateNotificationErrorDb( notification_error_id, - { is_read: true} + { is_read: true } ); if (!updateStatus) { diff --git a/services/notifikasi-wa.service.js b/services/notifikasi-wa.service.js index d66e778..35f1828 100644 --- a/services/notifikasi-wa.service.js +++ b/services/notifikasi-wa.service.js @@ -3,6 +3,8 @@ const { InsertNotificationErrorDb, updateNotificationErrorDb, getReminderNotificationErrorByYearlyDb, + getDeviceNotificationByIdDb, + getReminderNotificationErrorByMonthlyDb, updateNotificationErrorByChanelReminderDb, } = require("../db/notification_error.db"); const { @@ -16,18 +18,17 @@ const { sendNotifikasi, } = require("../db/notification_wa.db"); const { getErrorCodeByBrandAndCodeDb } = require("../db/brand_code.db"); -const { getDeviceNotificationByIdDb } = require("../db/notification_error.db"); const { exec } = require("child_process"); const util = require("util"); const execPromise = util.promisify(exec); const fs = require('fs').promises; const path = require("path"); -const { getDeviceReminderChanelDb } = require("../db/device.db"); - +const { getDeviceReminderChanelDb, getDeviceReminderMonthlyDb } = require("../db/device.db"); +const { getSparepartsByMonthlyDb } = require("../db/sparepart.db"); const baseDir = path.resolve(__dirname, '../scheduler'); const filePath = path.join(baseDir, 'reminder.json'); -const filePathLog = path.join(baseDir, 'log.json'); +// const filePathLog = path.join(baseDir, 'log.json'); const dayjs = require('dayjs'); const utc = require('dayjs/plugin/utc'); @@ -41,7 +42,7 @@ dayjs.extend(timezone); class NotifikasiWaService { async saveReminder(data, isReset = false) { - console.log(`Reminder proses dibuat`); + console.log(`Reminder dibuat`); try { let existing = []; @@ -95,58 +96,57 @@ class NotifikasiWaService { await fs.writeFile(filePath, JSON.stringify(finalData, null, 2), 'utf-8'); - await this.saveLogReminder({ - message: `Reminder berhasil disimpan`, - data, - }); + // await this.saveReminder({ + // message: `Reminder berhasil disimpan`, + // data, + // }); - console.log(`Reminder berhasil disimpan`); } catch (error) { - console.log('Error saveReminder:', error); + next(error); } } - async saveLogReminder(data) { - console.log(`Reminder Log dibuat`); + // async saveLogReminder(data) { + // console.log(`Reminder Log dibuat`); - try { - let existing = []; + // try { + // let existing = []; - // pastikan folder ada - await fs.mkdir(path.dirname(filePathLog), { recursive: true }); + // // pastikan folder ada + // await fs.mkdir(path.dirname(filePathLog), { recursive: true }); - try { - const raw = await fs.readFile(filePathLog, 'utf-8'); - if (raw) { - existing = JSON.parse(raw); - } - } catch (err) { - // file belum ada → buat file kosong - await fs.writeFile(filePathLog, '[]', 'utf-8'); - existing = []; - } + // try { + // const raw = await fs.readFile(filePathLog, 'utf-8'); + // if (raw) { + // existing = JSON.parse(raw); + // } + // } catch (err) { + // // file belum ada → buat file kosong + // await fs.writeFile(filePathLog, '[]', 'utf-8'); + // existing = []; + // } - const now = dayjs().tz(timeZone); + // const now = dayjs().tz(timeZone); - const newData = { - ...data, + // const newData = { + // ...data, - // 🔹 jam (HH:mm) WIB - time: now.format('HH:mm'), + // // 🔹 jam (HH:mm) WIB + // time: now.format('HH:mm'), - // 🔹 timestamp UTC (untuk DB) - created_at: now.toISOString() - }; + // // 🔹 timestamp UTC (untuk DB) + // created_at: now.toISOString() + // }; - await existing.push(newData); + // await existing.push(newData); - await fs.writeFile(filePathLog, JSON.stringify(existing, null, 2), 'utf-8'); + // await fs.writeFile(filePathLog, JSON.stringify(existing, null, 2), 'utf-8'); - console.log(`Reminder Log berhasil disimpan`); - } catch (error) { - console.log('Error saveReminder:', error); - } - } + // console.log(`Reminder Log berhasil disimpan`); + // } catch (error) { + // console.log('Error saveReminder:', error); + // } + // } async onNotification(topic, message) { try { @@ -298,7 +298,7 @@ class NotifikasiWaService { await this.saveReminder(newData, true); - await this.saveLogReminder({ + await this.saveReminder({ message: `Reminder ${id} berhasil dihapus` }) @@ -326,7 +326,7 @@ class NotifikasiWaService { } ); - await this.saveLogReminder({ + await this.saveReminder({ message: `Reminder ${id} berhasil dihapus` }) @@ -378,7 +378,7 @@ class NotifikasiWaService { dataUser.bodyMessage ); - await this.saveLogReminder(resultSend); + await this.saveReminder(resultSend); await updateNotificationErrorUserDb( dataUser.notification_error_user_id, @@ -491,21 +491,6 @@ class NotifikasiWaService { const resultNotificationError = await InsertNotificationErrorDb(data); - await this.saveReminder({ - notification_log: resultNotificationError.notification_error_id, - error_code_id: data['error_code_id'], - error_chanel: data['error_chanel'], - start_at: reminderAt.format('HH:mm'), - interval: 1, - max: 3, - active: 1, - count: 0, - last_run: now.toISOString(), - last_run_indo: now.format('DD-MM-YYYY HH:mm:ss'), - next_run: null, - next_run_indo: null - }, false); - const results = await getAllContactDb(paramDb); const dataUsers = results.data; @@ -513,7 +498,7 @@ class NotifikasiWaService { let isSendNotification = false; for (const dataUser of dataUsers) { - if (dataUser.is_active) { + if (dataUser.is_active || dataUser.is_active === 1) { const tokenRedirect = await generateTokenRedirect( dataUser.contact_phone, @@ -529,8 +514,7 @@ class NotifikasiWaService { `Hai ${dataUser.contact_name || "-"},\n` + `Diberitahukan bahwa terdapat sparepart pada device "${deviceName ?? "-"}" ` + `yang telah memasuki jadwal perawatan tahunan.\n` + - `\nSilakan segera lakukan pengecekan dan perawatan untuk memastikan kinerja tetap optimal.` + - `\nDetail sparepart dapat dilihat pada link berikut:\n${shortUrl}`; + `\nSilakan segera lakukan pengecekan dan perawatan untuk memastikan kinerja tetap optimal.`; const param = { idData: resultNotificationError.notification_error_id, @@ -554,10 +538,10 @@ class NotifikasiWaService { param.bodyMessage ); - await this.saveLogReminder({ - message: `Reminder dijalankan`, - resultSend - }) + // await this.saveLogReminder({ + // message: `Reminder dijalankan`, + // resultSend + // }) await updateNotificationErrorUserDb( resultNotificationErrorUser[0].notification_error_user_id, @@ -566,6 +550,22 @@ class NotifikasiWaService { } ); + await this.saveReminder({ + notification_log: resultNotificationError.notification_error_id, + error_code_id: data['error_code_id'], + error_chanel: data['error_chanel'], + start_at: reminderAt.format('HH:mm'), + interval: 1, + max: 3, + active: 1, + count: 0, + last_run: now.toISOString(), + last_run_indo: now.format('DD-MM-YYYY HH:mm:ss'), + next_run: null, + next_run_indo: null, + message: `Reminder untuk ${deviceName} dengan reminder ${chanel.value} telah dijalankan pada ${now.format('DD-MM-YYYY HH:mm:ss')}, hasil pengiriman: ${resultSend.success ? 'sukses' : 'gagal'}`, + }, false); + if (resultSend.success) { isSendNotification = resultSend.success; } @@ -591,27 +591,198 @@ class NotifikasiWaService { } } + async onMonthlySparepartReminder(deviceId) { + try { + const paramDb = { + limit: 100, + page: 1, + criteria: "", + active: 1, + }; + + const allDeviceReminders = await getDeviceReminderMonthlyDb(deviceId); + + if (!allDeviceReminders || allDeviceReminders.length === 0) { + return { success: false, message: 'Tidak ada data device.' }; + } + + const now = dayjs().tz(timeZone); + const currentYear = dayjs().tz(timeZone).year(); + const currentMonth = dayjs().tz(timeZone).month() + 1; + + for (const deviceReminder of allDeviceReminders) { + + if (deviceReminder?.reminder_at_monthly) { + const deviceName = deviceReminder.device_name ?? '-'; + + const reminderAt = dayjs.tz( + deviceReminder.reminder_at_monthly, + timeZone + ); + + const nowKey = now.format('MM-DD HH:mm'); + + const reminderKey = reminderAt.format('MM-DD HH:mm'); + + const isTriggered = nowKey >= reminderKey; + + if (isTriggered) { + const monthlyCode = parseInt(`${currentMonth}`); + + const checkNotifExist = await getReminderNotificationErrorByMonthlyDb( + monthlyCode, + currentMonth + ); + + if (!checkNotifExist) { + + const spareparts = await getSparepartsByMonthlyDb(currentMonth); + + let sparepartList = ""; + if (spareparts && spareparts.length > 0) { + sparepartList = "\n\nDaftar sparepart yang perlu diperiksa:\n" + + spareparts.map((sp, idx) => `${idx + 1}. ${sp.sparepart_name || 'Sparepart'}`).join('\n'); + } + + const data = { + error_code_id: monthlyCode, + error_chanel: 0, + is_send: 0, + is_delivered: 0, + is_read: 0, + is_active: 1, + message_error_issue: `reminder device ${deviceName} in month ${currentMonth}`, + }; + + const resultNotificationError = await InsertNotificationErrorDb(data); + + const results = await getAllContactDb(paramDb); + + const dataUsers = results.data; + + let isSendNotification = false; + + for (const dataUser of dataUsers) { + if (dataUser.is_active || dataUser.is_active === 1) { + + return { success: false, message: 'Tidak ada data user yang aktif.' }; + + 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\n` + + `Diberitahukan bahwa terdapat sparepart pada device "${deviceName}" ` + + `yang telah memasuki jadwal perawatan bulanan untuk bulan ${currentMonth} pada tahun ${currentYear}.${sparepartList}\n\n` + + `Silakan segera lakukan pengecekan dan perawatan untuk memastikan kinerja tetap optimal.`; + + const param = { + idData: resultNotificationError.notification_error_id, + userPhone: dataUser.contact_phone, + userName: dataUser.contact_name, + bodyMessage: bodyMessage, + }; + + const resultNotificationErrorUser = + await createNotificationErrorUserDb({ + notification_error_id: param.idData, + + contact_phone: param.userPhone, + contact_name: param.userName, + message_error_issue: param.bodyMessage, + is_send: false, + }); + + const resultSend = await sendNotifikasi( + param.userPhone, + param.bodyMessage + ); + + // await this.saveLogReminder({ + // message: `Reminder dijalankan`, + // resultSend + // }) + + await updateNotificationErrorUserDb( + resultNotificationErrorUser[0].notification_error_user_id, + { + is_send: resultSend.success, + } + ); + + await this.saveReminder({ + notification_log: resultNotificationError.notification_error_id, + error_code_id: data['error_code_id'], + error_chanel: data['error_chanel'], + start_at: reminderAt.format('HH:mm'), + interval: 1, + max: 3, + active: 1, + count: 0, + last_run: now.toISOString(), + last_run_indo: now.format('DD-MM-YYYY HH:mm:ss'), + next_run: null, + next_run_indo: null, + message: `Reminder untuk ${deviceName} dengan reminder bulan ${currentMonth} telah dijalankan pada ${now.format('DD-MM-YYYY HH:mm:ss')}, hasil pengiriman: ${resultSend.success ? 'sukses' : 'gagal'}`, + }, false); + + if (resultSend.success) { + isSendNotification = resultSend.success; + } + } + } + + await updateNotificationErrorDb( + resultNotificationError.notification_error_id, + { + is_send: isSendNotification, + is_delivered: isSendNotification, + } + ); + } + } + } + } + + } catch (err) { + console.log('Error onNotificationReminder:', err); + throw err; + } + } + + async restartWhatsapp() { try { - const processName = "Whatsapp-API-notification" && "cod-whatsapp-notif"; + + const processName = "cod-whatsapp-notif"; const { stdout } = await execPromise("pm2 jlist"); - const processes = JSON.parse(stdout); + const cleanJSON = stdout.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); + + const startJSON = cleanJSON.indexOf('['); + const endJSON = cleanJSON.lastIndexOf(']') + 1; + + if (startJSON === -1 || endJSON === 0) { + throw new Error(`Terminal tidak mengembalikan JSON yang valid. Output: ${stdout}`); + } + + const index = cleanJSON.slice(startJSON, endJSON); + const processes = JSON.parse(index); const waProcess = processes.find((p) => p.name === processName); - if (!waProcess) throw new Error(`PM2 ${processName} not found`); + if (!waProcess) throw new Error(`PM2 proses dengan nama "${processName}" tidak ditemukan!`); const waProcessId = waProcess.pm_id; const pathDelete = waProcess.pm2_env.pm_cwd; - // const execLogs = (cmd) => { - // const p = exec(cmd); - // p.stdout.pipe(process.stdout); - // p.stderr.pipe(process.stderr); - // return new Promise((res) => p.on("exit", res)); - // }; - - // console.log(`stop proses id: ${waProcessId}`) - await execPromise(`start powershell -Command "pm2 stop ${waProcessId}"`); + // console.log(`stop proses id: ${waProcessId}`); + await execPromise(`pm2 stop ${waProcessId}`); const pathFolderDelete = [ path.join(pathDelete, ".wwebjs_auth"), @@ -620,19 +791,25 @@ class NotifikasiWaService { // console.log(`path: ${pathDelete}`); - pathFolderDelete.forEach((dir) => { - if (fs.existsSync(dir)) { - // console.log(`path folder: ${dir}`); - fs.rmSync(dir, { recursive: true, force: true }); + for (const dir of pathFolderDelete) { + try { + await fs.access(dir); + console.log(`path folder ditemukan, menghapus: ${dir}`); + + await fs.rm(dir, { recursive: true, force: true }); + } catch (fsErr) { + if (fsErr.code !== 'ENOENT') { + console.error(`Gagal memproses folder ${dir}:`, fsErr.message); + } } - }); + } // console.log(`start proses id: ${waProcessId}`); - await execPromise(`start powershell -Command "pm2 start ${waProcessId}"`); + await execPromise(`pm2 start ${waProcessId}`); return { success: true, message: "WhatsApp has been restarted." }; } catch (err) { - return err; + return { success: false, error: err.message || err }; } } } diff --git a/validate/contact.schema.js b/validate/contact.schema.js index 04aaa0a..6671330 100644 --- a/validate/contact.schema.js +++ b/validate/contact.schema.js @@ -13,7 +13,8 @@ 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().allow(null) + contact_type: Joi.string().max(255).optional().allow(null), + email: Joi.string().email().required() }); const updateContactSchema = Joi.object({ @@ -26,7 +27,8 @@ 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().allow(null) + contact_type: Joi.string().max(255).optional().allow(null), + email: Joi.string().email().required() }); module.exports = {