Files
starr-philproperties/controllers/admin/notification.controller.js
T
2026-08-03 12:03:02 +08:00

137 lines
6.2 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name : notification.controller.js
* Type : Controller (Admin)
* Description : Admin notification management.
* GET /admin/notifications — paginated list, newest first
* GET /admin/notifications/unseen — unseen count only
* GET /admin/notifications/sticky — current sticky announcement, if any
* PATCH /admin/notifications/:id/seen — mark one as seen
* PATCH /admin/notifications/seen-all — mark all as seen
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 19, 2026
***********************************************************************************************************************************************************************/
const AdminNotification = require('../../models/notifications/admin_notification.mdl');
const mdl_Assets = require('../../models/assets/assets.mdl');
const mediaToken = require('../../services/mediaToken.service');
const R = require('../../utils/response.util');
const { notInFutureOrExpired } = require('../../utils/notificationVisibility.util');
const STICKY_LIMIT = 3;
const IMAGE_INCLUDE = {
model: mdl_Assets,
as: 'image',
attributes: ["asset_id", "display_name", "file_url", "thumbnail_url", "storage_provider", "storage_key", "file_type", "mime_type"],
required: false,
};
// Mirrors controllers/admin/advertisements.controller.js's attachImageStreamToken.
async function attachImageStreamToken(image, req) {
if (!image || image.storage_provider !== 's3' || !mediaToken.SUPPORTED_TYPES.includes(image.file_type)) {
return image;
}
const ip = mediaToken.resolveIp(req);
const { token } = await mediaToken.issueForAsset(image, req.user?.user_id, ip);
image.stream_token = token;
image.file_url = null;
image.thumbnail_url = null;
delete image.storage_key;
return image;
}
// ─── GET /admin/notifications ─────────────────────────────────────────────────
async function list(req, res) {
try {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(50, parseInt(req.query.limit) || 20);
const offset = (page - 1) * limit;
const { count, rows } = await AdminNotification.findAndCountAll({
order: [['createdAt', 'DESC']],
limit,
offset,
where: { show_in_notifications: true, ...notInFutureOrExpired() },
});
return R.success(res, 'Notifications fetched.', {
notifications: rows,
pagination: { page, limit, total: count, pages: Math.ceil(count / limit) },
});
} catch (err) {
console.error('[NOTIFICATION] list error:', err);
return R.error(res, 'Failed to fetch notifications.');
}
}
// ─── GET /admin/notifications/unseen ─────────────────────────────────────────
async function unseenCount(req, res) {
try {
const count = await AdminNotification.count({ where: { seen: false, show_in_notifications: true, ...notInFutureOrExpired() } });
return R.success(res, 'Unseen count fetched.', { count });
} catch (err) {
console.error('[NOTIFICATION] unseenCount error:', err);
return R.error(res, 'Failed to fetch unseen count.');
}
}
// ─── GET /admin/notifications/sticky ──────────────────────────────────────────
// Not user-scoped, same as list()/unseenCount() above — one shared sticky
// banner for every admin. Whoever dismisses it first dismisses it for all.
async function stickyAnnouncement(req, res) {
try {
const rows = await AdminNotification.findAll({
where: {
seen: false,
show_in_sticky: true,
type: 'announcement',
...notInFutureOrExpired(),
},
include: [IMAGE_INCLUDE],
order: [['createdAt', 'DESC']],
limit: STICKY_LIMIT,
});
const notifications = await Promise.all(rows.map(async (row) => {
const json = row.toJSON();
if (json.image) await attachImageStreamToken(json.image, req);
return json;
}));
return R.success(res, 'Sticky alerts fetched.', { announcements: notifications });
} catch (err) {
console.error('[NOTIFICATION] stickyAnnouncement error:', err);
return R.error(res, 'Failed to fetch sticky announcement.');
}
}
// ─── PATCH /admin/notifications/:id/seen ─────────────────────────────────────
async function markSeen(req, res) {
try {
const notification = await AdminNotification.findByPk(req.params.id);
if (!notification) return R.error(res, 'Notification not found.', 404);
await notification.update({ seen: true, seen_at: new Date() });
return R.success(res, 'Notification marked as seen.', notification);
} catch (err) {
console.error('[NOTIFICATION] markSeen error:', err);
return R.error(res, 'Failed to mark notification as seen.');
}
}
// ─── PATCH /admin/notifications/seen-all ─────────────────────────────────────
async function markAllSeen(req, res) {
try {
const now = new Date();
const [count] = await AdminNotification.update(
{ seen: true, seen_at: now },
{ where: { seen: false } }
);
return R.success(res, `${count} notification(s) marked as seen.`, { count });
} catch (err) {
console.error('[NOTIFICATION] markAllSeen error:', err);
return R.error(res, 'Failed to mark all notifications as seen.');
}
}
module.exports = { list, unseenCount, stickyAnnouncement, markSeen, markAllSeen };