/*********************************************************************************************************************************************************************** * 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 StickyBannerSetting = require('../../models/notifications/sticky_banner_setting.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; } // One shared banner image for the whole rotating sticky bar (see // controllers/admin/notificationBroadcasts.controller.js's // getStickyBannerSetting/updateStickyBannerSetting) — not per-announcement. async function resolveSharedBannerImage(req) { const setting = await StickyBannerSetting.findOne({ where: { id: 1 }, include: [IMAGE_INCLUDE] }); if (!setting?.image) return null; const image = setting.toJSON().image; await attachImageStreamToken(image, req); 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 [notifications, bannerImage] = await Promise.all([ AdminNotification.findAll({ where: { seen: false, show_in_sticky: true, type: 'announcement', ...notInFutureOrExpired(), }, order: [['createdAt', 'DESC']], limit: STICKY_LIMIT, }), resolveSharedBannerImage(req), ]); return R.success(res, 'Sticky announcements fetched.', { announcements: notifications, bannerImage }); } 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 };