diff --git a/db/device.db.js b/db/device.db.js index 390e80f..4249874 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; }; @@ -147,12 +149,51 @@ const getDeviceReminderMonthlyDb = async (id) => { 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 + WHERE a.device_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); return result.recordset; }; +const updateDeviceReminderMonthlyDb = async (deviceId, reminderMonthly) => { + const queryText = ` + UPDATE m_device + SET reminder_at_monthly = $1, + updated_at = CURRENT_TIMESTAMP + WHERE device_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [reminderMonthly, deviceId]); + return getDeviceByIdDb(deviceId); +}; + +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 +201,8 @@ module.exports = { updateDeviceDb, deleteDeviceDb, getDeviceReminderChanelDb, - getDeviceReminderMonthlyDb -}; + getDeviceReminderMonthlyDb, + getDeviceReminderCustomDb, + updateDeviceReminderCustomDb, + updateDeviceReminderMonthlyDb +}; \ No newline at end of file diff --git a/db/notification_error.db.js b/db/notification_error.db.js index 3c52b5f..b78fee0 100644 --- a/db/notification_error.db.js +++ b/db/notification_error.db.js @@ -255,20 +255,33 @@ const getReminderNotificationErrorByYearlyDb = async (errorCode, chanel, year) = return result.recordset[0]; }; -const getReminderNotificationErrorByMonthlyDb = async (errorCode, monthly) => { +const getReminderNotificationErrorByMonthlyDb = async (errorCode, monthly, deviceId) => { 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.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, monthly]); + 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]; }; @@ -282,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/package.json b/package.json index 3f46979..fd477b7 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "mqtt": "^5.14.0", "mssql": "^11.0.1", "multer": "^1.4.5-lts.2", + "node-cron": "^4.5.0", "nodemailer": "^6.8.0", "pg": "^8.8.0", "pino": "^6.11.3", diff --git a/routes/notifikasi-wa.route.js b/routes/notifikasi-wa.route.js index 3c7ce46..e75cc1b 100644 --- a/routes/notifikasi-wa.route.js +++ b/routes/notifikasi-wa.route.js @@ -11,14 +11,74 @@ router.post('/restart-wa', async (req, res) => { } }); -router.post('/reminder-sparepart/:id', async (req, res) => { +router.post('/reminder-monthly/:id', async (req, res) => { try { const { id } = req.params; - const result = await NotifikasiWaService.onMonthlySparepartReminder(id); + const {reminder_at_monthly} = req.body + const result = await NotifikasiWaService.onMonthlyReminder(id, reminder_at_monthly); 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/scheduler/reminder.json b/scheduler/reminder.json index 0637a08..e69de29 100644 --- a/scheduler/reminder.json +++ b/scheduler/reminder.json @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/services/notifikasi-wa.service.js b/services/notifikasi-wa.service.js index 35f1828..7e7368a 100644 --- a/services/notifikasi-wa.service.js +++ b/services/notifikasi-wa.service.js @@ -1,35 +1,53 @@ 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 { + getDeviceReminderChanelDb, + getDeviceReminderMonthlyDb, + getDeviceReminderCustomDb, + updateDeviceReminderCustomDb, + updateDeviceReminderMonthlyDb +} = require("../db/device.db"); + +const { + getSparepartsByMonthlyDb, + getSparepartsByCustomDb +} = require("../db/sparepart.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, 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 cron = require('node-cron'); const dayjs = require('dayjs'); const utc = require('dayjs/plugin/utc'); const timezone = require('dayjs/plugin/timezone'); @@ -39,8 +57,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 +423,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; } @@ -490,15 +516,13 @@ class NotifikasiWaService { }; 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) { + if (dataUser.is_active) { const tokenRedirect = await generateTokenRedirect( dataUser.contact_phone, @@ -523,15 +547,13 @@ class NotifikasiWaService { 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 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, @@ -563,7 +585,7 @@ class NotifikasiWaService { 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'}`, + message: `Reminder untuk ${deviceName} dengan reminder ${chanel.value} telah dijalankan pada ${now.format('DD-MM-YYYY HH:mm:ss')}}`, }, false); if (resultSend.success) { @@ -591,172 +613,717 @@ class NotifikasiWaService { } } - async onMonthlySparepartReminder(deviceId) { + + async onMonthlyReminder(deviceId, reminderMonthly) { + try { + console.log(`🔄 Memulai onMonthlyReminder untuk device ID: ${deviceId}`); + + const paramDb = { + limit: 100, + page: 1, + criteria: "", + active: true, + }; + + // 1. Validasi device + console.log(`📡 Mengambil data device dengan ID: ${deviceId}`); + const devices = await getDeviceReminderMonthlyDb(deviceId); + + if (!devices || devices.length === 0) { + console.error(`❌ Device dengan ID ${deviceId} tidak ditemukan`); + return { + success: false, + message: `Device dengan ID ${deviceId} tidak ditemukan di database` + }; + } + + const device = devices[0]; + const deviceName = device.device_name; + const currentDeviceId = device.device_id; + + console.log(`✅ Device ditemukan: ${deviceName} (ID: ${currentDeviceId})`); + + // 2. Validasi dan update reminder_at_monthly jika diperlukan + let targetReminderTime = device.reminder_at_monthly; + console.log(`📅 Reminder time dari DB: ${targetReminderTime}`); + + if (reminderMonthly) { + console.log(`📝 Mengupdate reminder time ke: ${reminderMonthly}`); + const isValidDate = dayjs.tz(reminderMonthly, this.timeZone).isValid(); + if (!isValidDate) { + return { + success: false, + message: 'Format reminderMonthly tidak valid. Gunakan format YYYY-MM-DD HH:mm:ss' + }; + } + + const updatedDevice = await updateDeviceReminderMonthlyDb(deviceId, reminderMonthly); + if (!updatedDevice) { + return { + success: false, + message: 'Gagal mengupdate reminder monthly.' + }; + } + + targetReminderTime = reminderMonthly; + console.log(`✅ Reminder time berhasil diupdate ke: ${targetReminderTime}`); + } else if (!targetReminderTime) { + console.log(`⚠️ Tidak ada reminder time, setting default ke sekarang + 1 jam`); + const reminderTime = dayjs().tz(this.timeZone).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss'); + const updatedDevice = await updateDeviceReminderMonthlyDb(deviceId, reminderTime); + if (!updatedDevice) { + return { + success: false, + message: 'Gagal mengupdate reminder monthly dengan default time.' + }; + } + targetReminderTime = reminderTime; + console.log(`✅ Default reminder time: ${targetReminderTime}`); + } + + console.log(`📅 Target reminder time: ${targetReminderTime}`); + + // 3. Parse target reminder time + const reminderAt = dayjs.tz(targetReminderTime, this.timeZone); + const now = dayjs().tz(this.timeZone); + + console.log(`🕐 Waktu sekarang: ${now.format('YYYY-MM-DD HH:mm:ss')}`); + console.log(`🕐 Waktu reminder: ${reminderAt.format('YYYY-MM-DD HH:mm:ss')}`); + + // 4. Jika sudah lewat atau sama, langsung kirim (sama seperti onCustomReminder) + const isEndDatePassed = now.isAfter(reminderAt) || now.isSame(reminderAt); + + if (isEndDatePassed) { + console.log(`⏰ Reminder time sudah terlewati, mengirim reminder sekarang untuk ${deviceName}`); + const sendResult = await this.sendMonthlyReminderNow( + currentDeviceId, + deviceName, + reminderAt + ); + return sendResult; + } + + // 5. Jika belum waktunya, jadwalkan cron job (sama seperti onCustomReminder) + console.log(`⏰ Menjadwalkan reminder untuk ${deviceName} pada ${reminderAt.format('YYYY-MM-DD HH:mm:ss')}`); + + // Stop cron job lama jika ada + this.stopCronJob(currentDeviceId); + + // Schedule cron job + const scheduleResult = await this.scheduleMonthlyReminderJob( + currentDeviceId, + deviceName, + reminderAt + ); + + return { + success: true, + message: `Reminder berhasil dijadwalkan untuk device ${deviceName}`, + schedule: scheduleResult, + nextRun: scheduleResult.nextRun + }; + + } catch (error) { + console.error('❌ Error onMonthlyReminder:', error); + console.error('❌ Stack trace:', error.stack); + return { + success: false, + message: 'Error pada onMonthlyReminder: ' + error.message, + error: error, + stack: error.stack + }; + } + } + + async scheduleMonthlyReminderJob(deviceId, deviceName, reminderAt) { + try { + const now = dayjs().tz(this.timeZone); + + const targetTime = reminderAt.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(`Reminder At: ${reminderAt.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 { + await this.sendMonthlyReminderNow(deviceId, deviceName, reminderAt); + } catch (error) { + console.error(`Gagal mengirim reminder untuk ${deviceName}:`, error); + } + }); + + this.cronJobs.set(deviceId, { + timeoutId, + nextRun, + reminderAt: reminderAt.format('YYYY-MM-DD HH:mm:ss') + }); + + const cronExpression = `${reminderAt.minute()} ${reminderAt.hour()} * * *`; + console.log(`Backup cron akan berjalan setiap hari pada ${reminderAt.hour()}:${reminderAt.minute()}`); + + 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(reminderAt) || now.isSame(reminderAt); + + if (isEndDatePassed) { + const currentMonth = now.month() + 1; + const checkNotifExist = await getReminderNotificationErrorByMonthlyDb( + parseInt(`${currentMonth}`), + currentMonth, + deviceId + ); + + if (!checkNotifExist) { + await this.sendMonthlyReminderNow(deviceId, deviceName, reminderAt); + } else { + console.log(`Reminder untuk ${deviceName} sudah pernah dikirim`); + } + + cronTask.stop(); + this.cronJobs.delete(`cron_${deviceId}`); + } else { + console.log(`Menunggu reminder untuk ${deviceName}: ${reminderAt.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, + reminderAt: reminderAt.format('YYYY-MM-DD HH:mm:ss') + }); + + return { + nextRun, + targetDate: targetTime.format('YYYY-MM-DD HH:mm:ss'), + reminderAt: reminderAt.format('YYYY-MM-DD HH:mm:ss'), + cronExpression: cronExpression, + }; + + } catch (error) { + console.error('Error scheduling monthly reminder:', error); + throw error; + } + } + + async sendMonthlyReminderNow(deviceId, deviceName, reminderAt) { + try { + console.log(`Mengirim reminder untuk ${deviceName}`); + + const paramDb = { + limit: 100, + page: 1, + criteria: "", + active: true, + }; + + const now = dayjs().tz(this.timeZone); + const currentMonth = now.month() + 1; + const currentYear = now.year(); + const monthlyCode = parseInt(`${currentMonth}`); + + const checkNotifExist = await getReminderNotificationErrorByMonthlyDb( + monthlyCode, + currentMonth, + deviceId + ); + + if (checkNotifExist) { + console.log(`Reminder untuk ${deviceName} bulan ${currentMonth} sudah pernah dibuat`); + return { + success: false, + message: `Reminder untuk device ${deviceName} sudah pernah dibuat.` + }; + } + + const spareparts = await getSparepartsByMonthlyDb(currentMonth); + const sparepartList = spareparts && spareparts.length > 0 + ? "\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 monthly device ${deviceName} in month ${currentMonth}`, + }; + + const resultNotificationError = await InsertNotificationErrorDb(data); + if (!resultNotificationError) { + return { + success: false, + message: 'Gagal menyimpan notifikasi error' + }; + } + + const results = await getAllContactDb(paramDb); + const dataUsers = results.data; + + let isSendNotification = false; + let successCount = 0; + let failCount = 0; + + 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 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 + ); + + if (resultNotificationErrorUser && resultNotificationErrorUser.length > 0) { + await updateNotificationErrorUserDb( + resultNotificationErrorUser[0].notification_error_user_id, + { + is_send: resultSend.success, + } + ); + } + + if (resultSend.success) { + isSendNotification = true; + } + } + } + + 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 monthly 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 monthly reminder now:', error); + throw error; + } + } + + async onCustomReminder(deviceId, inputStartDate, inputEndDate) { try { const paramDb = { limit: 100, page: 1, criteria: "", - active: 1, + active: true, }; - const allDeviceReminders = await getDeviceReminderMonthlyDb(deviceId); - - if (!allDeviceReminders || allDeviceReminders.length === 0) { - return { success: false, message: 'Tidak ada data device.' }; + if (!inputStartDate || !inputEndDate) { + return { + success: false, + message: 'start_date dan end_date harus diisi. Format: YYYY-MM-DD HH:mm:ss' + }; } - const now = dayjs().tz(timeZone); - const currentYear = dayjs().tz(timeZone).year(); - const currentMonth = dayjs().tz(timeZone).month() + 1; + const startDateObj = dayjs.tz(inputStartDate, this.timeZone); + const endDateObj = dayjs.tz(inputEndDate, this.timeZone); - for (const deviceReminder of allDeviceReminders) { + if (!startDateObj.isValid() || !endDateObj.isValid()) { + return { + success: false, + message: 'Format tanggal tidak valid. Gunakan format: YYYY-MM-DD HH:mm:ss' + }; + } - if (deviceReminder?.reminder_at_monthly) { - const deviceName = deviceReminder.device_name ?? '-'; + if (!endDateObj.isAfter(startDateObj)) { + return { + success: false, + message: 'end_date harus lebih besar dari start_date' + }; + } - const reminderAt = dayjs.tz( - deviceReminder.reminder_at_monthly, - timeZone - ); + const deviceData = await getDeviceReminderCustomDb(deviceId); - const nowKey = now.format('MM-DD HH:mm'); + if (!deviceData || deviceData.length === 0) { + return { success: false, message: 'Device tidak ditemukan' }; + } - const reminderKey = reminderAt.format('MM-DD HH:mm'); + const device = deviceData[0]; + const deviceName = device.device_name ?? '-'; + const currentDeviceId = device.device_id; - const isTriggered = nowKey >= reminderKey; + const updateResult = await updateDeviceReminderCustomDb( + currentDeviceId, + inputStartDate, + inputEndDate + ); - if (isTriggered) { - const monthlyCode = parseInt(`${currentMonth}`); + if (!updateResult || updateResult.length === 0) { + return { + success: false, + message: 'Gagal menyimpan start_date dan end_date' + }; + } - const checkNotifExist = await getReminderNotificationErrorByMonthlyDb( - monthlyCode, - currentMonth + console.log(`Berhasil update start_date dan end_date untuk device ${deviceName}`); + + this.stopCronJob(currentDeviceId); + + const scheduleResult = await this.scheduleCustomReminderJob( + 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.sendCustomReminderNow( + 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 scheduleCustomReminderJob(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.sendCustomReminderNow(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) { - - 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, - } - ); + await this.sendCustomReminderNow(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 sendCustomReminderNow(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; } } } - } catch (err) { - console.log('Error onNotificationReminder:', err); - throw err; + 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}`); + } + + const intervalData = this.cronJobs.get(`interval_${deviceId}`); + if (intervalData && intervalData.intervalId) { + clearInterval(intervalData.intervalId); + console.log(`Interval dihentikan untuk device ${deviceId}`); + } + + 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}`); + } + + async restartWhatsapp() { try {