mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
units,lesson as standalone
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name : task_due_soon.cron.js
|
||||
* Type : Cron Job
|
||||
* Description : Emits a learner-facing "task_reminder" UserNotification for
|
||||
* each user who has NOT yet completed a task whose deadline
|
||||
* falls ~24h from now. Unlike task_overdue.cron.js (a single
|
||||
* admin-facing status flip), completion here is per-user, so
|
||||
* each candidate task's assigned-group members are checked
|
||||
* individually via checkTaskCompletion before notifying.
|
||||
*
|
||||
* "Falls ~24h from now" = deadline between (now + 23h) and
|
||||
* (now + 24h), a 1-hour sliding window — since this runs
|
||||
* hourly, each task's deadline crosses that window exactly
|
||||
* once, giving a single reminder ~24h before it's due
|
||||
* without needing a separate "already notified" table.
|
||||
*
|
||||
* Schedule : Every hour, 10 minutes past ("10 * * * *"). Registered by
|
||||
* cron/client.cron.js.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 9, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { Op, QueryTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const { Task } = require('../../models/task/task.mdl');
|
||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||
const CronNotificationSetting = require('../../models/notifications/cron_notification_setting.mdl');
|
||||
const { renderNotification } = require('../../services/notificationTemplate.service');
|
||||
const { checkTaskCompletion } = require('../../controllers/client/task.controller');
|
||||
|
||||
const WINDOW_START_MS = 23 * 60 * 60 * 1000;
|
||||
const WINDOW_END_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
async function run() {
|
||||
const settings = await CronNotificationSetting.findOne({ where: { job_name: 'taskDueSoon' } });
|
||||
if (settings && !settings.enabled) return;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
let dueSoonTasks;
|
||||
try {
|
||||
dueSoonTasks = await Task.findAll({
|
||||
attributes: ['task_id', 'task_list_id', 'name', 'deadline'],
|
||||
where: {
|
||||
deadline: {
|
||||
[Op.gte]: new Date(now + WINDOW_START_MS),
|
||||
[Op.lt]: new Date(now + WINDOW_END_MS),
|
||||
},
|
||||
status: { [Op.notIn]: ['completed', 'overdue'] },
|
||||
},
|
||||
raw: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[CRON][TASK DUE SOON] Failed to query upcoming deadlines:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dueSoonTasks.length) return;
|
||||
|
||||
console.log(`[CRON][TASK DUE SOON] ${dueSoonTasks.length} task(s) due in ~24h — resolving affected users.`);
|
||||
|
||||
try {
|
||||
const taskListIds = [...new Set(dueSoonTasks.map((t) => t.task_list_id))];
|
||||
const memberRows = await sequelize.query(
|
||||
`SELECT DISTINCT tlg.task_list_id, ugm.user_id
|
||||
FROM task_list_groups tlg
|
||||
JOIN user_group_members ugm ON ugm.group_id = tlg.group_id
|
||||
AND ugm."deletedAt" IS NULL
|
||||
WHERE tlg.task_list_id IN (:taskListIds)`,
|
||||
{ replacements: { taskListIds }, type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const usersByTaskList = new Map();
|
||||
for (const row of memberRows) {
|
||||
const list = usersByTaskList.get(row.task_list_id) ?? [];
|
||||
list.push(row.user_id);
|
||||
usersByTaskList.set(row.task_list_id, list);
|
||||
}
|
||||
|
||||
const now2 = new Date();
|
||||
let notifiedCount = 0;
|
||||
|
||||
for (const task of dueSoonTasks) {
|
||||
const candidateUserIds = usersByTaskList.get(task.task_list_id) ?? [];
|
||||
if (!candidateUserIds.length) continue;
|
||||
|
||||
const incompleteUserIds = [];
|
||||
for (const userId of candidateUserIds) {
|
||||
const done = await checkTaskCompletion(userId, task.task_id);
|
||||
if (!done) incompleteUserIds.push(userId);
|
||||
}
|
||||
if (!incompleteUserIds.length) continue;
|
||||
|
||||
const notify = await renderNotification({ type: 'task_reminder', data: {
|
||||
taskName: task.name, deadline: task.deadline,
|
||||
} });
|
||||
await UserNotification.bulkCreate(
|
||||
incompleteUserIds.map((user_id) => ({ user_id, ...notify, seen: false, createdAt: now2, updatedAt: now2 })),
|
||||
{ validate: false }
|
||||
);
|
||||
notifiedCount += incompleteUserIds.length;
|
||||
}
|
||||
|
||||
console.log(`[CRON][TASK DUE SOON] Sent ${notifiedCount} reminder(s) across ${dueSoonTasks.length} task(s).`);
|
||||
} catch (err) {
|
||||
console.error('[CRON][TASK DUE SOON] Failed to emit reminders:', err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'taskDueSoon',
|
||||
schedule: '10 * * * *',
|
||||
run,
|
||||
};
|
||||
Reference in New Issue
Block a user