mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
revise ads and alerts
This commit is contained in:
@@ -61,8 +61,6 @@ function deriveStatus(advertisement) {
|
||||
return "active";
|
||||
}
|
||||
|
||||
const ALLOWED_CTA_VARIANTS = ["default", "outline"];
|
||||
|
||||
function normalizeCtas(ctas) {
|
||||
if (!Array.isArray(ctas)) return [];
|
||||
return ctas
|
||||
@@ -71,12 +69,22 @@ function normalizeCtas(ctas) {
|
||||
.map((c, i) => ({
|
||||
label: c.label.trim(),
|
||||
link: c.link.trim(),
|
||||
// First CTA defaults to "default" (primary), second to "outline" — but
|
||||
// an explicit, valid variant from the client always wins.
|
||||
variant: ALLOWED_CTA_VARIANTS.includes(c.variant) ? c.variant : (i === 0 ? "default" : "outline"),
|
||||
// Variant is always derived from position — first CTA is "default"
|
||||
// (primary), second is "outline" — not user-selectable, so any
|
||||
// client-sent variant is ignored.
|
||||
variant: i === 0 ? "default" : "outline",
|
||||
}));
|
||||
}
|
||||
|
||||
// Hard cap: max 2 badge labels per advertisement (matches MAX_BADGE_LABELS on the frontend)
|
||||
function normalizeBadgeLabels(labels) {
|
||||
if (!Array.isArray(labels)) return [];
|
||||
return labels
|
||||
.filter((l) => typeof l === "string" && l.trim().length > 0)
|
||||
.map((l) => l.trim())
|
||||
.slice(0, 2);
|
||||
}
|
||||
|
||||
async function applyAdvertisementFields(advertisement, body) {
|
||||
// placement is the only settable "where" — type/format is always derived
|
||||
// from the placement's registry entry, never accepted directly from the body.
|
||||
@@ -103,7 +111,7 @@ async function applyAdvertisementFields(advertisement, body) {
|
||||
advertisement.content_mode = body.content_mode;
|
||||
}
|
||||
|
||||
if (body.badge_label !== undefined) advertisement.badge_label = body.badge_label;
|
||||
if (body.badge_labels !== undefined) advertisement.badge_labels = normalizeBadgeLabels(body.badge_labels);
|
||||
if (body.headline !== undefined) advertisement.headline = body.headline;
|
||||
if (body.description !== undefined) advertisement.description = body.description;
|
||||
if (body.image_url !== undefined) advertisement.image_url = body.image_url;
|
||||
|
||||
@@ -17,7 +17,7 @@ const mediaToken = require('../../services/mediaToken.service');
|
||||
const R = require('../../utils/response.util');
|
||||
const { notInFutureOrExpired } = require('../../utils/notificationVisibility.util');
|
||||
|
||||
const STICKY_LIMIT = 3;
|
||||
const STICKY_LIMIT = 2;
|
||||
const IMAGE_INCLUDE = {
|
||||
model: mdl_Assets,
|
||||
as: 'image',
|
||||
|
||||
@@ -66,7 +66,7 @@ async function countActiveSticky(excludeId = null) {
|
||||
});
|
||||
}
|
||||
|
||||
const MAX_ACTIVE_STICKY = 3;
|
||||
const MAX_ACTIVE_STICKY = 2;
|
||||
const ACTIVE_STICKY_CAP_MESSAGE = `Maximum of ${MAX_ACTIVE_STICKY} active sticky alerts right now — this stays in Draft until one ends or is archived.`;
|
||||
|
||||
async function validateImageAssetId(image_asset_id) {
|
||||
@@ -97,7 +97,7 @@ async function propagateNotificationVisibility(where, { show_in_sticky, show_in_
|
||||
|
||||
async function applyBroadcastFields(broadcast, body) {
|
||||
if (body.title !== undefined) broadcast.title = body.title;
|
||||
if (body.message !== undefined) broadcast.message = body.message;
|
||||
if (body.message !== undefined) broadcast.message = body.message || null;
|
||||
if (body.link_url !== undefined) broadcast.link_url = body.link_url?.trim() || null;
|
||||
if (body.link_label !== undefined) broadcast.link_label = body.link_label?.trim() || null;
|
||||
if (body.color !== undefined) broadcast.color = body.color || 'indigo';
|
||||
@@ -254,7 +254,6 @@ exports.createBroadcast = async (req, res) => {
|
||||
} = req.body;
|
||||
|
||||
if (!title) return R.error(res, "title is required.", 400);
|
||||
if (!message) return R.error(res, "message is required.", 400);
|
||||
if (!target_type) return R.error(res, "target_type is required.", 400);
|
||||
if (!ALLOWED_TARGET_TYPES.includes(target_type)) return R.error(res, `Invalid target_type. Must be one of: ${ALLOWED_TARGET_TYPES.join(", ")}`, 400);
|
||||
if (SCOPED_TARGET_TYPES.includes(target_type) && !target_id) return R.error(res, "target_id is required for this target_type.", 400);
|
||||
@@ -265,6 +264,12 @@ exports.createBroadcast = async (req, res) => {
|
||||
if (!showSticky && !showNotifs) {
|
||||
return R.error(res, "At least one of show_in_sticky or show_in_notifications must be enabled.", 400);
|
||||
}
|
||||
if (showSticky && showNotifs) {
|
||||
return R.error(res, "Choose only one: Sticky or Notifications.", 400);
|
||||
}
|
||||
if (showNotifs && !message) {
|
||||
return R.error(res, "message is required for Notifications alerts.", 400);
|
||||
}
|
||||
|
||||
if (start_date && end_date && new Date(start_date) > new Date(end_date)) {
|
||||
return R.error(res, "Start date must be before end date.", 400);
|
||||
@@ -278,7 +283,7 @@ exports.createBroadcast = async (req, res) => {
|
||||
try {
|
||||
const broadcast = await NotificationBroadcast.build({
|
||||
title,
|
||||
message,
|
||||
message: message || null,
|
||||
link_url: link_url?.trim() || null,
|
||||
link_label: link_label?.trim() || null,
|
||||
color: color || 'indigo',
|
||||
@@ -328,6 +333,16 @@ exports.updateBroadcast = async (req, res) => {
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
if (broadcast.show_in_sticky && broadcast.show_in_notifications) {
|
||||
const err = new Error("Choose only one: Sticky or Notifications.");
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
if (broadcast.show_in_notifications && !broadcast.message) {
|
||||
const err = new Error("message is required for Notifications alerts.");
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Editing a live broadcast to newly flip on show_in_sticky is the same
|
||||
// "activate a sticky slot" action as sendBroadcast — must respect the
|
||||
@@ -352,7 +367,9 @@ exports.updateBroadcast = async (req, res) => {
|
||||
if (broadcast.status === 'sent') {
|
||||
const propagated = {
|
||||
title: broadcast.title,
|
||||
message: broadcast.message,
|
||||
// admin_notifications/user_notifications.message stays NOT NULL —
|
||||
// sticky-mode broadcasts have a null message here, so fall back to "".
|
||||
message: broadcast.message || "",
|
||||
color: broadcast.color,
|
||||
image_asset_id: broadcast.image_asset_id,
|
||||
show_in_sticky: broadcast.show_in_sticky,
|
||||
@@ -419,7 +436,7 @@ exports.sendBroadcast = async (req, res) => {
|
||||
|
||||
const baseNotify = NOTIFICATION_REGISTRY.broadcast.build({
|
||||
title: broadcast.title,
|
||||
message: broadcast.message,
|
||||
message: broadcast.message || "",
|
||||
targetType,
|
||||
targetId,
|
||||
linkUrl: broadcast.link_url,
|
||||
@@ -458,7 +475,7 @@ exports.sendBroadcast = async (req, res) => {
|
||||
user_id,
|
||||
...(targetType === 'task_list'
|
||||
? NOTIFICATION_REGISTRY.broadcast.build({
|
||||
title: broadcast.title, message: broadcast.message, targetType, targetId,
|
||||
title: broadcast.title, message: broadcast.message || "", targetType, targetId,
|
||||
groupId: groupByUser[user_id] ?? null,
|
||||
linkUrl: broadcast.link_url,
|
||||
linkLabel: broadcast.link_label,
|
||||
|
||||
Reference in New Issue
Block a user