From 8fd317083b68e1a7739989794f1c66c430ce6132 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 23 Jun 2026 21:59:02 +0700 Subject: [PATCH] add: feature reminder custom device with cron-job --- db/device.db.js | 40 +- db/notification_error.db.js | 17 + db/sparepart.db.js | 21 +- routes/notifikasi-wa.route.js | 61 ++- services/notifikasi-wa.service.js | 690 ++++++++++++++++++++++++------ 5 files changed, 692 insertions(+), 137 deletions(-) diff --git a/db/device.db.js b/db/device.db.js index 390e80f..0d2428c 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -63,7 +63,6 @@ const getAllDevicesDb = async (searchParams = {}) => { return { data: result.recordset, total }; }; - const getDeviceByIdDb = async (id) => { const queryText = ` SELECT @@ -116,12 +115,15 @@ const updateDeviceDb = async (id, data) => { }; const deleteDeviceDb = async (id, deletedBy) => { + // Jika deletedBy adalah string, konversi ke integer atau gunakan default + const deletedById = typeof deletedBy === 'string' ? parseInt(deletedBy) || 0 : deletedBy || 0; + const queryText = ` UPDATE m_device SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 WHERE device_id = $2 AND deleted_at IS NULL `; - await pool.query(queryText, [deletedBy, id]); + await pool.query(queryText, [deletedById, id]); return true; }; @@ -153,6 +155,34 @@ const getDeviceReminderMonthlyDb = async (id) => { return result.recordset; }; +const getDeviceReminderCustomDb = 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 + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +const updateDeviceReminderCustomDb = async (deviceId, startDate, endDate) => { + const queryText = ` + UPDATE m_device + SET start_date = $1, + end_date = $2, + updated_at = CURRENT_TIMESTAMP + WHERE device_id = $3 AND deleted_at IS NULL + `; + await pool.query(queryText, [startDate, endDate, deviceId]); + return getDeviceByIdDb(deviceId); +}; + + module.exports = { getAllDevicesDb, getDeviceByIdDb, @@ -160,5 +190,7 @@ module.exports = { updateDeviceDb, deleteDeviceDb, getDeviceReminderChanelDb, - getDeviceReminderMonthlyDb -}; + getDeviceReminderMonthlyDb, + getDeviceReminderCustomDb, + updateDeviceReminderCustomDb, +}; \ No newline at end of file diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 16bf23d..b78fee0 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -269,6 +269,22 @@ const getReminderNotificationErrorByMonthlyDb = async (errorCode, monthly, devic const result = await pool.query(queryText, [errorCode, monthly, deviceId]); return result.recordset[0]; }; + +const getReminderNotificationErrorByCustomDb = async (errorCode, custom, deviceId) => { + const queryText = ` + SELECT a.* + FROM notification_error a + WHERE a.error_code_id = $1 + AND a.created_at = $2 + AND a.message_error_issue LIKE '%device_id: ' + CAST($3 AS VARCHAR) + '%' + AND a.is_active = 1 + AND a.deleted_at IS NULL + `; + + const result = await pool.query(queryText, [errorCode, custom, deviceId]); + return result.recordset[0]; +}; + module.exports = { getNotificationByIdDb, getDeviceNotificationByIdDb, @@ -279,6 +295,7 @@ module.exports = { getDeviceChannelReminder, getReminderNotificationErrorByYearlyDb, getReminderNotificationErrorByMonthlyDb, + getReminderNotificationErrorByCustomDb, updateNotificationErrorByChanelReminderDb }; diff --git a/db/sparepart.db.js b/db/sparepart.db.js index f27b038..df2154c 100644 --- a/db/sparepart.db.js +++ b/db/sparepart.db.js @@ -215,6 +215,24 @@ const getSparepartsByMonthlyDb = async (monthly) => { return result.recordset; }; +const getSparepartsByCustomDb = async (custom) => { + + const queryText = ` + SELECT * + FROM m_sparepart + WHERE deleted_at IS NULL + AND EXISTS ( + SELECT 1 + FROM OPENJSON(sparepart_custom) + WHERE value = $1 + ) + `; + + const result = await pool.query(queryText, [custom]); + + return result.recordset; +}; + module.exports = { getAllSparepartDb, getSparepartByIdDb, @@ -224,5 +242,6 @@ module.exports = { updateSparepartDb, deleteSparepartDb, getSparepartsByYearlyDb, - getSparepartsByMonthlyDb + getSparepartsByMonthlyDb, + getSparepartsByCustomDb }; diff --git a/routes/notifikasi-wa.route.js b/routes/notifikasi-wa.route.js index 3c7ce46..3f40aaf 100644 --- a/routes/notifikasi-wa.route.js +++ b/routes/notifikasi-wa.route.js @@ -14,11 +14,70 @@ 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); + const result = await NotifikasiWaService.onMonthlyReminder(id); return res.status(200).json(result); } catch (error) { return res.status(500).json(error); } }); +router.post('/reminder-custom/:id', async (req, res) => { + try { + const { id } = req.params; + const {start_date, end_date} = req.body; + const result = await NotifikasiWaService.onCustomReminder(id, start_date, end_date); + return res.status(200).json(result); + } catch (error) { + return res.status(500).json(error); + } +}); + +router.get('/cron/jobs', async (req, res) => { + try { + const activeJobs = NotifikasiWaService.getActiveCronJobs(); + return res.status(200).json({ + success: true, + total: activeJobs.length, + jobs: activeJobs + }); + } catch (error) { + return res.status(500).json({ + success: false, + message: error.message + }); + } +}); + +router.delete('/cron/jobs/:deviceId', async (req, res) => { + try { + const { deviceId } = req.params; + NotifikasiWaService.stopCronJob(deviceId); + return res.status(200).json({ + success: true, + message: `Cron job untuk device ${deviceId} dihentikan` + }); + } catch (error) { + return res.status(500).json({ + success: false, + message: error.message + }); + } +}); + +// Endpoint untuk menghentikan semua cron job +router.delete('/cron/jobs', async (req, res) => { + try { + NotifikasiWaService.stopAllCronJobs(); + return res.status(200).json({ + success: true, + message: 'Semua cron job dihentikan' + }); + } catch (error) { + return res.status(500).json({ + success: false, + message: error.message + }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/services/notifikasi-wa.service.js b/services/notifikasi-wa.service.js index 948b639..498aea5 100644 --- a/services/notifikasi-wa.service.js +++ b/services/notifikasi-wa.service.js @@ -1,22 +1,27 @@ const { getAllContactDb } = require("../db/contact.db"); + const { InsertNotificationErrorDb, updateNotificationErrorDb, getReminderNotificationErrorByYearlyDb, getDeviceNotificationByIdDb, getReminderNotificationErrorByMonthlyDb, + getReminderNotificationErrorByCustomDb, updateNotificationErrorByChanelReminderDb, } = require("../db/notification_error.db"); + const { createNotificationErrorUserDb, updateNotificationErrorUserDb, getNotificationErrorByIdDb, } = require("../db/notification_error_user.db"); + const { generateTokenRedirect, shortUrltiny, sendNotifikasi, } = require("../db/notification_wa.db"); + const { getErrorCodeByBrandAndCodeDb } = require("../db/brand_code.db"); const { exec } = require("child_process"); @@ -24,12 +29,17 @@ const util = require("util"); const execPromise = util.promisify(exec); const fs = require('fs').promises; const path = require("path"); -const { getDeviceReminderChanelDb, getDeviceReminderMonthlyDb } = require("../db/device.db"); -const { getSparepartsByMonthlyDb } = require("../db/sparepart.db"); +const { getDeviceReminderChanelDb, + getDeviceReminderMonthlyDb, + getDeviceReminderCustomDb, + updateDeviceReminderCustomDb +} = require("../db/device.db"); +const { getSparepartsByMonthlyDb, getSparepartsByCustomDb } = 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 cron = require('node-cron'); const dayjs = require('dayjs'); const utc = require('dayjs/plugin/utc'); const timezone = require('dayjs/plugin/timezone'); @@ -39,8 +49,16 @@ const timeZone = 'Asia/Jakarta'; dayjs.extend(utc); dayjs.extend(timezone); +let reminderCronJob = null; +let isCronInitialized = false; + class NotifikasiWaService { + constructor() { + this.timeZone = 'Asia/Jakarta'; + this.cronJobs = new Map(); + } + async saveReminder(data, isReset = false) { console.log(`Reminder dibuat`); @@ -397,7 +415,7 @@ class NotifikasiWaService { ); } } - item.last_run = now.toISOString(); // tetap UTC (standar DB) + item.last_run = now.toISOString(); item.last_run_indo = now.format('DD-MM-YYYY HH:mm:ss'); // WIB item.count += 1; } @@ -587,165 +605,575 @@ class NotifikasiWaService { } } -async onMonthlySparepartReminder(deviceId) { - try { - const paramDb = { - limit: 100, - page: 1, - criteria: "", - active: true, - }; - - const allDeviceReminders = await getDeviceReminderMonthlyDb(deviceId); + async onMonthlyReminder(deviceId) { + try { + const paramDb = { + limit: 100, + page: 1, + criteria: "", + active: true, + }; - if (!allDeviceReminders || allDeviceReminders.length === 0) { - return { success: false, message: 'Tidak ada data device.' }; - } + const allDeviceReminders = await getDeviceReminderMonthlyDb(deviceId); - const now = dayjs().tz(timeZone); - const currentYear = dayjs().tz(timeZone).year(); - const currentMonth = dayjs().tz(timeZone).month() + 1; + if (!allDeviceReminders || allDeviceReminders.length === 0) { + return { success: false, message: 'Tidak ada data device.' }; + } - for (const deviceReminder of allDeviceReminders) { + const now = dayjs().tz(timeZone); + const currentYear = dayjs().tz(timeZone).year(); + const currentMonth = dayjs().tz(timeZone).month() + 1; - if (deviceReminder?.reminder_at_monthly) { - const deviceName = deviceReminder.device_name ?? '-'; - const currentDeviceId = deviceReminder.device_id; + for (const deviceReminder of allDeviceReminders) { - const reminderAt = dayjs.tz( - deviceReminder.reminder_at_monthly, - timeZone - ); + if (deviceReminder?.reminder_at_monthly) { + const deviceName = deviceReminder.device_name ?? '-'; + const currentDeviceId = deviceReminder.device_id; - 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, - currentDeviceId + const reminderAt = dayjs.tz( + deviceReminder.reminder_at_monthly, + timeZone ); - if (checkNotifExist) { - throw new Error(`Reminder untuk device ${deviceName} device id ${currentDeviceId} bulan ${currentMonth} sudah pernah dibuat.`); - continue; - } + const nowKey = now.format('MM-DD HH:mm'); + const reminderKey = reminderAt.format('MM-DD HH:mm'); - const spareparts = await getSparepartsByMonthlyDb(currentMonth); + const isTriggered = nowKey >= reminderKey; - 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'); - } + if (isTriggered) { + const monthlyCode = parseInt(`${currentMonth}`); - 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} device_id: ${currentDeviceId} in month ${currentMonth}`, - }; + const checkNotifExist = await getReminderNotificationErrorByMonthlyDb( + monthlyCode, + currentMonth, + currentDeviceId + ); - const resultNotificationError = await InsertNotificationErrorDb(data); - const results = await getAllContactDb(paramDb); - const dataUsers = results.data; + if (checkNotifExist) { + throw new Error(`Reminder untuk device ${deviceName} device id ${currentDeviceId} bulan ${currentMonth} sudah pernah dibuat.`); + continue; + } - let isSendNotification = false; + const spareparts = await getSparepartsByMonthlyDb(currentMonth); - for (const dataUser of dataUsers) { - if (dataUser.is_active) { + 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 tokenRedirect = await generateTokenRedirect( - dataUser.contact_phone, - dataUser.contact_name, - resultNotificationError.notification_error_id - ); + 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} device_id: ${currentDeviceId} in month ${currentMonth}`, + }; - const encodedToken = encodeURIComponent(tokenRedirect); - const shortUrl = await shortUrltiny(encodedToken); + const resultNotificationError = await InsertNotificationErrorDb(data); + const results = await getAllContactDb(paramDb); + const dataUsers = results.data; - 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.`; + let isSendNotification = false; - const param = { - idData: resultNotificationError.notification_error_id, - userPhone: dataUser.contact_phone, - userName: dataUser.contact_name, - bodyMessage: bodyMessage, - }; + for (const dataUser of dataUsers) { + if (dataUser.is_active) { - 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 tokenRedirect = await generateTokenRedirect( + dataUser.contact_phone, + dataUser.contact_name, + resultNotificationError.notification_error_id + ); - const resultSend = await sendNotifikasi( - param.userPhone, - param.bodyMessage - ); + const encodedToken = encodeURIComponent(tokenRedirect); + const shortUrl = await shortUrltiny(encodedToken); - await updateNotificationErrorUserDb( - resultNotificationErrorUser[0].notification_error_user_id, - { - is_send: resultSend.success, + 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 updateNotificationErrorUserDb( + resultNotificationErrorUser[0].notification_error_user_id, + { + is_send: resultSend.success, + } + ); + + if (resultSend.success) { + isSendNotification = resultSend.success; } - ); - - if (resultSend.success) { - isSendNotification = resultSend.success; } } + + await updateNotificationErrorDb( + resultNotificationError.notification_error_id, + { + is_send: isSendNotification, + is_delivered: isSendNotification, + } + ); + + 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} bulan ${currentMonth} telah dijalankan.`, + }, false); + } - - await updateNotificationErrorDb( - resultNotificationError.notification_error_id, - { - is_send: isSendNotification, - is_delivered: isSendNotification, - } - ); - - 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} bulan ${currentMonth} telah dijalankan.`, - }, false); - } } + + return { success: true, message: "Reminder has been sent." }; + + } catch (err) { + console.log('Error on Monthly Reminder:', err); + throw err; + } + } + + async onCustomReminder(deviceId, inputStartDate, inputEndDate) { + try { + const paramDb = { + limit: 100, + page: 1, + criteria: "", + active: true, + }; + + if (!inputStartDate || !inputEndDate) { + return { + success: false, + message: 'start_date dan end_date harus diisi. Format: YYYY-MM-DD HH:mm:ss' + }; + } + + const startDateObj = dayjs.tz(inputStartDate, this.timeZone); + const endDateObj = dayjs.tz(inputEndDate, this.timeZone); + + if (!startDateObj.isValid() || !endDateObj.isValid()) { + return { + success: false, + message: 'Format tanggal tidak valid. Gunakan format: YYYY-MM-DD HH:mm:ss' + }; + } + + if (!endDateObj.isAfter(startDateObj)) { + return { + success: false, + message: 'end_date harus lebih besar dari start_date' + }; + } + + const deviceData = await getDeviceReminderCustomDb(deviceId); + + if (!deviceData || deviceData.length === 0) { + return { success: false, message: 'Device tidak ditemukan' }; + } + + const device = deviceData[0]; + const deviceName = device.device_name ?? '-'; + const currentDeviceId = device.device_id; + + const updateResult = await updateDeviceReminderCustomDb( + currentDeviceId, + inputStartDate, + inputEndDate + ); + + if (!updateResult || updateResult.length === 0) { + return { + success: false, + message: 'Gagal menyimpan start_date dan end_date' + }; + } + + console.log(`Berhasil update start_date dan end_date untuk device ${deviceName}`); + + this.stopCronJob(currentDeviceId); + + const scheduleResult = await this.scheduleReminderJob( + currentDeviceId, + deviceName, + startDateObj, + endDateObj + ); + + const now = dayjs().tz(this.timeZone); + const isEndDatePassed = now.isAfter(endDateObj) || now.isSame(endDateObj); + + if (isEndDatePassed) { + console.log(`⏰ End_date sudah terlewati, mengirim reminder sekarang untuk ${deviceName}`); + const sendResult = await this.sendReminderNow( + currentDeviceId, + deviceName, + startDateObj, + endDateObj + ); + + return { + success: true, + message: 'Reminder berhasil dijadwalkan dan dikirim sekarang', + schedule: scheduleResult, + sendResult: sendResult + }; + } + + return { + success: true, + message: `Reminder berhasil dijadwalkan untuk device ${deviceName}`, + schedule: scheduleResult, + nextRun: scheduleResult.nextRun + }; + + } catch (err) { + console.error('Error on Custom Reminder:', err); + throw err; + } + } + + async scheduleReminderJob(deviceId, deviceName, startDateObj, endDateObj) { + try { + const now = dayjs().tz(this.timeZone); + + const targetTime = endDateObj.clone() + .set('second', 0) + .set('millisecond', 0); + + const nextRun = targetTime.format('YYYY-MM-DD HH:mm:ss'); + + console.log(`Reminder untuk ${deviceName} dijadwalkan pada ${nextRun}`); + console.log(`Start Date: ${startDateObj.format('YYYY-MM-DD HH:mm:ss')}`); + console.log(`End Date: ${endDateObj.format('YYYY-MM-DD HH:mm:ss')}`); + + const timeoutId = setTimeout(async () => { + console.log(`Waktu pengiriman tiba untuk ${deviceName} pada ${dayjs().tz(this.timeZone).format('YYYY-MM-DD HH:mm:ss')}`); + try { + const now = dayjs().tz(this.timeZone); + const isEndDatePassed = now.isAfter(endDateObj) || now.isSame(endDateObj); + + if (isEndDatePassed) { + await this.sendReminderNow(deviceId, deviceName, startDateObj, endDateObj); + } else { + console.log(`End_date untuk ${deviceName} belum terlewati, menunggu...`); + this.rescheduleReminderCheck(deviceId, deviceName, startDateObj, endDateObj); + } + } catch (error) { + console.error(`Gagal mengirim reminder untuk ${deviceName}:`, error); + } + }); + + this.cronJobs.set(deviceId, { + timeoutId, + nextRun, + startDate: startDateObj.format('YYYY-MM-DD HH:mm:ss'), + endDate: endDateObj.format('YYYY-MM-DD HH:mm:ss') + }); + + const endHour = endDateObj.hour(); + const endMinute = endDateObj.minute(); + const cronExpression = `${endMinute} ${endHour} * * *`; + + console.log(`Backup cron akan berjalan setiap hari pada ${endHour}:${endMinute}`); + + const cronTask = cron.schedule(cronExpression, async () => { + console.log(`Backup cron job berjalan untuk ${deviceName} pada ${dayjs().tz(this.timeZone).format('YYYY-MM-DD HH:mm:ss')}`); + try { + const now = dayjs().tz(this.timeZone); + const isEndDatePassed = now.isAfter(endDateObj) || now.isSame(endDateObj); + + if (isEndDatePassed) { + const currentMonth = now.month() + 1; + const checkNotifExist = await getReminderNotificationErrorByCustomDb( + parseInt(`${currentMonth}`), + currentMonth, + deviceId + ); + + if (!checkNotifExist) { + await this.sendReminderNow(deviceId, deviceName, startDateObj, endDateObj); + } else { + console.log(`Reminder untuk ${deviceName} sudah pernah dikirim`); + } + + cronTask.stop(); + this.cronJobs.delete(deviceId); + this.cronJobs.delete(`cron_${deviceId}`); + } else { + console.log(`Menunggu end_date untuk ${deviceName}: ${endDateObj.format('YYYY-MM-DD HH:mm:ss')}`); + } + } catch (error) { + console.error(`Backup cron job gagal untuk ${deviceName}:`, error); + } + }, { + timezone: this.timeZone + }); + + this.cronJobs.set(`cron_${deviceId}`, { + cronTask, + nextRun, + startDate: startDateObj.format('YYYY-MM-DD HH:mm:ss'), + endDate: endDateObj.format('YYYY-MM-DD HH:mm:ss') + }); + + return { + nextRun, + targetDate: targetTime.format('YYYY-MM-DD HH:mm:ss'), + endDate: endDateObj.format('YYYY-MM-DD HH:mm:ss'), + cronExpression: cronExpression + }; + + } catch (error) { + console.error('Error scheduling reminder:', error); + throw error; + } + } + + async rescheduleReminderCheck(deviceId, deviceName, startDateObj, endDateObj) { + const checkInterval = setInterval(async () => { + const now = dayjs().tz(this.timeZone); + const isEndDatePassed = now.isAfter(endDateObj) || now.isSame(endDateObj); + + if (isEndDatePassed) { + clearInterval(checkInterval); + console.log(`End_date terlewati untuk ${deviceName}, mengirim reminder...`); + try { + await this.sendReminderNow(deviceId, deviceName, startDateObj, endDateObj); + } catch (error) { + console.error(`Gagal mengirim reminder untuk ${deviceName}:`, error); + } + } + }, 60000); + + this.cronJobs.set(`interval_${deviceId}`, { + intervalId: checkInterval + }); + } + + async sendReminderNow(deviceId, deviceName, startDateObj, endDateObj) { + try { + console.log(`Mengirim reminder untuk ${deviceName}`); + + const now = dayjs().tz(this.timeZone); + const currentMonth = now.month() + 1; + const monthlyCode = parseInt(`${currentMonth}`); + + const checkNotifExist = await getReminderNotificationErrorByCustomDb( + monthlyCode, + currentMonth, + deviceId + ); + + if (checkNotifExist) { + console.log(`Reminder untuk ${deviceName} sudah pernah dibuat`); + return { + success: false, + message: `Reminder untuk device ${deviceName} sudah pernah dibuat.` + }; + } + + 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 custom device ${deviceName} in month ${currentMonth}`, + }; + + const resultNotificationError = await InsertNotificationErrorDb(data); + + const paramDb = { + limit: 100, + page: 1, + criteria: "", + active: true, + }; + const results = await getAllContactDb(paramDb); + const dataUsers = results.data; + + let isSendNotification = false; + + 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\n` + + `Diberitahukan bahwa terdapat sparepart pada device "${deviceName}" ` + + `yang telah memasuki jadwal perawatan.\n\n` + + `Silakan segera lakukan pengecekan dan perawatan untuk memastikan kinerja tetap optimal.\n\n` + + `Detail Periode Perawatan:\n` + + `Start Date: ${startDateObj.format('DD-MM-YYYY HH:mm')}\n` + + `End Date: ${endDateObj.format('DD-MM-YYYY HH:mm')}\n` + + 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 + ); + + if (resultNotificationErrorUser && resultNotificationErrorUser.length > 0) { + await updateNotificationErrorUserDb( + resultNotificationErrorUser[0].notification_error_user_id, + { + is_send: resultSend.success, + } + ); + } + + if (resultSend.success) { + isSendNotification = resultSend.success; + } + } + } + + await updateNotificationErrorDb( + resultNotificationError.notification_error_id, + { + is_send: isSendNotification, + is_delivered: isSendNotification, + } + ); + + await this.saveReminder({ + notification_log: resultNotificationError.notification_error_id, + error_code_id: data['error_code_id'], + error_chanel: data['error_chanel'], + start_at: now.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} bulan ${currentMonth} telah dijalankan.`, + }, false); + + this.stopCronJob(deviceId); + + console.log(`Reminder berhasil dikirim untuk ${deviceName}`); + return { success: true, message: "Reminder has been sent." }; + + } catch (error) { + console.error('Error sending reminder now:', error); + throw error; + } + } + + stopCronJob(deviceId) { + const jobData = this.cronJobs.get(deviceId); + if (jobData && jobData.timeoutId) { + clearTimeout(jobData.timeoutId); + console.log(`Timeout dihentikan untuk device ${deviceId}`); } - return { success: true, message: "Reminder has been sent." }; + const intervalData = this.cronJobs.get(`interval_${deviceId}`); + if (intervalData && intervalData.intervalId) { + clearInterval(intervalData.intervalId); + console.log(`Interval dihentikan untuk device ${deviceId}`); + } - } catch (err) { - console.log('Error onNotificationReminder:', err); - throw err; + const cronData = this.cronJobs.get(`cron_${deviceId}`); + if (cronData && cronData.cronTask) { + cronData.cronTask.stop(); + console.log(`Cron job dihentikan untuk device ${deviceId}`); + } + + this.cronJobs.delete(deviceId); + this.cronJobs.delete(`cron_${deviceId}`); + this.cronJobs.delete(`interval_${deviceId}`); } -} + + + getActiveCronJobs() { + const activeJobs = []; + for (const [key, value] of this.cronJobs) { + if (!key.startsWith('cron_') && !key.startsWith('interval_')) { + activeJobs.push({ + deviceId: key, + nextRun: value.nextRun, + startDate: value.startDate, + endDate: value.endDate + }); + } + } + return activeJobs; + } + + stopAllCronJobs() { + for (const [key, value] of this.cronJobs) { + if (value.timeoutId) { + clearTimeout(value.timeoutId); + } + if (value.intervalId) { + clearInterval(value.intervalId); + } + if (value.cronTask) { + value.cronTask.stop(); + } + } + this.cronJobs.clear(); + console.log('Semua cron job dihentikan'); + } + async restartWhatsapp() { try {