// Announcement data access helpers. const { fetchPagedRows } = require('./utils'); const { ANNOUNCEMENT_ICON_KEYS, DEFAULT_ANNOUNCEMENT_ICON, normalizeAnnouncementIcon: normalizeAnnouncementIconFromConfig } = require('./announcement-icons'); const { validateMaxLength } = require('./utils'); const SHORT_LABEL_MAX_LENGTH = 255; const ANNOUNCEMENT_TYPES = ['lower-third', 'fullscreen', 'top-banner']; const ANNOUNCEMENT_COLORS = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'dark', 'light']; function normalizeAnnouncementType(value) { const normalized = String(value || '').trim().toLowerCase(); if (normalized === 'center-card') { return 'fullscreen'; } return ANNOUNCEMENT_TYPES.includes(normalized) ? normalized : 'lower-third'; } function normalizeAnnouncementColor(value) { const normalized = String(value || '').trim().toLowerCase(); return ANNOUNCEMENT_COLORS.includes(normalized) ? normalized : 'primary'; } function normalizeAnnouncementMessage(value) { return String(value || '').replace(/\r\n/g, '\n').trim(); } function normalizeAnnouncementShortLabel(value) { return String(value || '').replace(/\r\n/g, ' ').trim(); } function normalizeAnnouncementDurationSeconds(value) { const seconds = Math.max(0, Math.round(Number(value) || 0)); return seconds > 0 ? seconds : null; } function normalizeAnnouncementScreenIds(value) { if (Array.isArray(value)) { return value.map(function (screenId) { return Number(screenId) || 0; }).filter(function (screenId) { return screenId > 0; }); } return String(value || '') .split(',') .map(function (screenId) { return Number(screenId) || 0; }) .filter(function (screenId) { return screenId > 0; }); } function buildAnnouncementPayload(input) { const message = normalizeAnnouncementMessage(input && input.message); const shortLabel = validateMaxLength(normalizeAnnouncementShortLabel(input && input.short_label), SHORT_LABEL_MAX_LENGTH, 'Announcement short description'); const announcementType = normalizeAnnouncementType(input && input.announcement_type); const colorKey = normalizeAnnouncementColor(input && input.color_key); const iconKey = normalizeAnnouncementIconFromConfig(input && input.icon_key); const durationSeconds = normalizeAnnouncementDurationSeconds(input && input.duration_seconds); return { message, shortLabel, announcementType, colorKey, iconKey, durationSeconds, expiresAt: durationSeconds ? new Date(Date.now() + (durationSeconds * 1000)) : null }; } function normalizeAnnouncementRow(row) { if (!row) { return null; } return { id: Number(row.id) || null, message: normalizeAnnouncementMessage(row.message), short_label: normalizeAnnouncementShortLabel(row.short_label), announcement_type: normalizeAnnouncementType(row.announcement_type), color_key: normalizeAnnouncementColor(row.color_key), icon_key: normalizeAnnouncementIconFromConfig(row.icon_key), duration_seconds: row.duration_seconds === null || row.duration_seconds === undefined ? null : Math.max(0, Math.round(Number(row.duration_seconds) || 0)), expires_at: row.expires_at || null, screen_target_count: row.screen_target_count === null || row.screen_target_count === undefined ? null : Math.max(0, Math.round(Number(row.screen_target_count) || 0)), total_screen_count: row.total_screen_count === null || row.total_screen_count === undefined ? null : Math.max(0, Math.round(Number(row.total_screen_count) || 0)), screen_targets_label: String(row.screen_targets_label || '').trim(), screen_ids: normalizeAnnouncementScreenIds(row.screen_ids), created_at: row.created_at || null, modified_at: row.modified_at || null, created_by: row.created_by || null, modified_by: row.modified_by || null }; } async function fetchAnnouncementById(pool, id) { const [rows] = await pool.query(` SELECT a.id, a.message, a.short_label, a.announcement_type, a.color_key, a.icon_key, a.duration_seconds, a.expires_at, a.created_at, a.modified_at, a.created_by, a.modified_by, GROUP_CONCAT(DISTINCT aas.screen_id ORDER BY aas.screen_id SEPARATOR ',') AS screen_ids FROM d_announcements a LEFT JOIN d_announcement_screens aas ON aas.announcement_id = a.id WHERE a.id = ? GROUP BY a.id `, [id]); return normalizeAnnouncementRow(rows[0] || null); } async function fetchAnnouncementsPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) { const paged = await fetchPagedRows(pool, { selectSql: `SELECT a.id, a.message, a.short_label, a.announcement_type, a.color_key, a.icon_key, a.duration_seconds, a.expires_at, a.created_at, a.modified_at, a.created_by, a.modified_by, COUNT(DISTINCT aas.screen_id) AS screen_target_count, (SELECT COUNT(*) FROM d_screens) AS total_screen_count, COALESCE( GROUP_CONCAT(DISTINCT COALESCE(NULLIF(TRIM(s.name), ''), s.slug) ORDER BY COALESCE(NULLIF(TRIM(s.name), ''), s.slug) SEPARATOR ', '), '' ) AS screen_targets_label FROM d_announcements a LEFT JOIN d_announcement_screens aas ON aas.announcement_id = a.id LEFT JOIN d_screens s ON s.id = aas.screen_id GROUP BY a.id`, countSql: 'SELECT COUNT(*) AS count FROM d_announcements', searchColumns: ['message', 'short_label', 'announcement_type', 'color_key'], searchTerm: searchTerm, sortColumns: { description: 'short_label', message: 'message', type: 'announcement_type', color: 'color_key', status: 'expires_at', expires: 'expires_at', created: 'created_at', modified: 'modified_at' }, sortKey: sortKey, sortDirection: sortDirection, page: page, pageSize: pageSize }); return Object.assign({ announcements: (paged.rows || []).map(normalizeAnnouncementRow) }, paged); } async function fetchActiveAnnouncement(pool, screenSlug) { const [rows] = await pool.query(` SELECT a.id, a.message, a.short_label, a.announcement_type, a.color_key, a.icon_key, a.duration_seconds, a.expires_at, a.created_at, a.modified_at, a.created_by, a.modified_by, GROUP_CONCAT(DISTINCT aas.screen_id ORDER BY aas.screen_id SEPARATOR ',') AS screen_ids FROM d_announcements a LEFT JOIN d_announcement_screens aas ON aas.announcement_id = a.id WHERE (a.expires_at IS NULL OR a.expires_at > CURRENT_TIMESTAMP) AND EXISTS ( SELECT 1 FROM d_announcement_screens restriction JOIN d_screens screen ON screen.id = restriction.screen_id WHERE restriction.announcement_id = a.id AND screen.slug = ? ) GROUP BY a.id ORDER BY a.modified_at DESC, a.created_at DESC, a.id DESC LIMIT 1 `, [screenSlug]); return normalizeAnnouncementRow(rows[0] || null); } module.exports = { ANNOUNCEMENT_TYPES, ANNOUNCEMENT_COLORS, ANNOUNCEMENT_ICONS: ANNOUNCEMENT_ICON_KEYS, DEFAULT_ANNOUNCEMENT_ICON, normalizeAnnouncementType, normalizeAnnouncementColor, normalizeAnnouncementIcon: normalizeAnnouncementIconFromConfig, normalizeAnnouncementShortLabel, buildAnnouncementPayload, fetchAnnouncementById, fetchAnnouncementsPage, fetchActiveAnnouncement };