Rev 37508 | Blame | Compare with Previous | Last modification | View Log | RSS feed
$(function () {$(document).on('click', '.send-notification', function () {loadNotificationPanel('main-content');});// ─── Mobile Preview modal ────────────────────────────────────────────// On mobile the preview column is hidden. The floating "Preview" button// moves the .nm-preview-sticky node into the modal body on open, then// puts it back on close — so the same DOM (and all the jQuery selectors// that update it as the admin types) works in both places.$(document).on('click', '#nmPreviewFab', function () {var $preview = $('.nm-preview-sticky');if ($preview.length) {$preview.appendTo('#nmMobilePreviewBody');}$('#nmMobilePreviewModal').modal('show');});$('#nmMobilePreviewModal').on('hidden.bs.modal', function () {var $preview = $(this).find('.nm-preview-sticky');if ($preview.length) {$preview.appendTo('.nm-preview-col');}});// ─── Campaign Group picker ───────────────────────────────────────────function loadCampaignGroups(preselectId) {$.getJSON(context + '/campaignGroups', function (data) {var parsed = typeof data === 'string' ? JSON.parse(data) : data;var $sel = $('#nmCampaignGroup');$sel.find('option.nm-group-opt').remove();var $createNew = $sel.find('option[value="__new__"]');(parsed || []).forEach(function (g) {var opt = $('<option class="nm-group-opt"></option>').attr('value', g.id).text(g.name);$createNew.before(opt);});if (preselectId) {$sel.val(String(preselectId));}});}loadCampaignGroups();$(document).on('change', '#nmCampaignGroup', function () {if ($(this).val() === '__new__') {$(this).val('');$('#nmGroupName, #nmGroupDesc, #nmGroupStarts, #nmGroupEnds').val('');$('#nmGroupModal').modal('show');}});$(document).on('click', '#nmCreateGroupBtn', function () {var name = $('#nmGroupName').val().trim();if (!name) {alert('Group name is required');return;}var body = {name: name,description: $('#nmGroupDesc').val() || null,startsOn: $('#nmGroupStarts').val() || null,endsOn: $('#nmGroupEnds').val() || null};doPostAjaxRequestWithJsonHandler(context + '/campaignGroups', JSON.stringify(body), function (response) {var parsed = typeof response === 'string' ? JSON.parse(response) : response;if (parsed && parsed.id) {$('#nmGroupModal').modal('hide');loadCampaignGroups(parsed.id);} else {alert('Could not create the group. Try again.');}});});// ─── Tab strip ───────────────────────────────────────────────────────$(document).on('click', '.nm-tab', function () {var tab = $(this).data('tab');$('.nm-tab').removeClass('active');$(this).addClass('active');$('.nm-tab-content').removeClass('active');$('.nm-tab-content[data-tab-content="' + tab + '"]').addClass('active');if (tab === 'calendar') {initCalendar();}});// ─── Custom month-grid calendar (replaces FullCalendar 1.5.4) ────────var calRef = null; // { year, month } — 0-indexed monthfunction initCalendar() {if (!calRef) {var now = new Date();calRef = {year: now.getFullYear(), month: now.getMonth()};}renderCalendarMonth();renderScheduledList();renderRecentList();}$(document).on('click', '#ncPrev', function () {calRef.month -= 1;if (calRef.month < 0) {calRef.month = 11;calRef.year -= 1;}renderCalendarMonth();});$(document).on('click', '#ncNext', function () {calRef.month += 1;if (calRef.month > 11) {calRef.month = 0;calRef.year += 1;}renderCalendarMonth();});$(document).on('click', '#ncToday', function () {var now = new Date();calRef = {year: now.getFullYear(), month: now.getMonth()};renderCalendarMonth();});var MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];function renderCalendarMonth() {var y = calRef.year, m = calRef.month;$('#ncTitle').text(MONTH_NAMES[m] + ' ' + y);var firstOfMonth = new Date(y, m, 1);var startDow = firstOfMonth.getDay(); // 0 = Sunvar gridStart = new Date(y, m, 1 - startDow); // first Sunday of the visible gridvar gridEnd = new Date(y, m, 1 - startDow + 42); // 6 weeks × 7 daysvar from = fmtDate(gridStart);var to = fmtDate(gridEnd);// Build the grid skeleton first (so it appears immediately, events fill in on fetch)var $body = $('#ncBody').empty();var today = fmtDate(new Date());var eventsByDate = {}; // filled by AJAXfor (var w = 0; w < 6; w++) {var $tr = $('<tr></tr>');for (var d = 0; d < 7; d++) {var cell = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + w * 7 + d);var dateStr = fmtDate(cell);var isOtherMonth = cell.getMonth() !== m;var isToday = dateStr === today;var $td = $('<td></td>').attr('data-date', dateStr).append('<div class="nc-day-num">' + cell.getDate() + '</div>').append('<div class="nc-events"></div>');if (isOtherMonth) $td.addClass('nc-other-month');if (isToday) $td.addClass('nc-today');$tr.append($td);}$body.append($tr);}// Fetch events for the visible range and inject into cells$.getJSON(context + '/notificationCalendarData?from=' + from + '&to=' + to, function (data) {var events = data || [];events.forEach(function (e) {if (!e.start) return;var startDate = new Date(e.start);var endDate = e.end ? new Date(e.end) : startDate;// Iterate every day the event covers (windows may span days)var cursor = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());var last = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());while (cursor <= last) {var key = fmtDate(cursor);if (!eventsByDate[key]) eventsByDate[key] = [];eventsByDate[key].push(e);cursor.setDate(cursor.getDate() + 1);}});Object.keys(eventsByDate).forEach(function (dateStr) {var $cell = $body.find('td[data-date="' + dateStr + '"] .nc-events');if ($cell.length === 0) return;var dayEvents = eventsByDate[dateStr];var shown = dayEvents.slice(0, 4);shown.forEach(function (e) {var $ev = $('<div class="nc-event"></div>').text(labelForEvent(e)).css('background', e.color || '#3a87ad').data('event', e);$cell.append($ev);});if (dayEvents.length > shown.length) {$cell.append('<div class="nc-more">+' + (dayEvents.length - shown.length) + ' more</div>');}});});}function labelForEvent(e) {var t = e.start ? formatHM(new Date(e.start)) : '';return (t ? t + ' ' : '') + (e.title || '');}function fmtDate(d) {var pad = function (n) {return String(n).padStart(2, '0');};return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());}function formatHM(d) {var pad = function (n) {return String(n).padStart(2, '0');};return pad(d.getHours()) + ':' + pad(d.getMinutes());}// ─── Upcoming Scheduled Notifications list ───────────────────────────var CATEGORY_COLOR = {'ANNOUNCE': '#3a87ad', 'NOTIFICATION': '#4b5563','REMINDER': '#f0ad4e', 'ALERT': '#d9534f'};function categoryBadge(cat) {if (!cat) return '<span style="color:#9ca3af;">—</span>';var color = CATEGORY_COLOR[cat] || '#6b7280';return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;'+ 'background:' + color + '20;color:' + color + ';font-size:11px;font-weight:600;">'+ cat + '</span>';}function statusBadge(st) {var bg = '#eef2ff', fg = '#4338ca';if (st === 'DISPATCHED') {bg = '#dcfce7';fg = '#166534';} else if (st === 'CANCELLED') {bg = '#f3f4f6';fg = '#6b7280';} else if (st === 'FAILED') {bg = '#fee2e2';fg = '#991b1b';}return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;'+ 'background:' + bg + ';color:' + fg + ';font-size:11px;font-weight:600;">'+ st + '</span>';}function escapeHtml(s) {if (s == null) return '';return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');}function fmtWhen(iso) {if (!iso) return '';var d = new Date(iso);var pad = function (n) {return String(n).padStart(2, '0');};return pad(d.getDate()) + '/' + pad(d.getMonth() + 1) + ' ' + formatHM(d);}function renderScheduledList() {var days = $('#nslDays').val() || '30';var $body = $('#nslBody');$body.html('<tr><td colspan="7" style="text-align:center;color:#9ca3af;padding:24px;">Loading…</td></tr>');$.getJSON(context + '/notificationScheduleList?days=' + encodeURIComponent(days), function (rows) {if (!rows || rows.length === 0) {$body.html('<tr><td colspan="7" style="text-align:center;color:#9ca3af;padding:24px;">'+ 'No scheduled notifications in the next ' + days + ' days.</td></tr>');return;}var html = '';rows.forEach(function (r) {var titleLine = r.title ? '<div style="font-weight:600;color:#111;">' + escapeHtml(r.title) + '</div>' : '';var msgLine = r.message ? '<div style="color:#6b7280;font-size:12px;max-width:420px;'+ 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(r.message) + '</div>' : '';var thumb = r.imageUrl ? '<img src="' + escapeHtml(r.imageUrl) + '" '+ 'style="width:36px;height:36px;object-fit:cover;border-radius:4px;margin-right:8px;vertical-align:middle;" />' : '';var content = '<div style="display:flex;align-items:center;">' + thumb+ '<div>' + titleLine + msgLine + '</div></div>';var groupCell = r.groupName? '<span style="display:inline-block;padding:2px 8px;border-radius:10px;background:#eef2ff;color:#4338ca;font-size:11px;font-weight:600;">'+ escapeHtml(r.groupName) + '</span>': '<span style="color:#9ca3af;">—</span>';var audCount = (r.audienceCount != null ? r.audienceCount : 0);var audSummary = r.audienceSummary || '';var audienceCell = '<div style="font-weight:600;">' + audCount.toLocaleString() + '</div>'+ '<div style="font-size:11px;color:#6b7280;">' + escapeHtml(audSummary) + '</div>';var canCancel = r.status === 'SCHEDULED';var actions = '<button type="button" class="nm-btn nm-btn-outline cdm-view-btn" '+ 'data-campaign-id="' + r.campaignId + '" style="margin-right:4px;">View</button>'+ '<button type="button" class="nm-btn nm-btn-outline nsl-add-btn" '+ 'data-campaign-id="' + r.campaignId + '" style="margin-right:4px;">+ Add date</button>'+ '<button type="button" class="nm-btn nm-btn-outline nsl-cancel-btn" '+ 'data-schedule-id="' + r.scheduleId + '" '+ (canCancel ? '' : 'disabled ')+ 'style="color:#dc2626;border-color:#fecaca;">Cancel</button>';html += '<tr>'+ '<td style="white-space:nowrap;font-weight:600;">' + fmtWhen(r.when) + '</td>'+ '<td>' + groupCell + '</td>'+ '<td>' + content + '</td>'+ '<td>' + categoryBadge(r.category) + '</td>'+ '<td style="text-align:right;">' + audienceCell + '</td>'+ '<td>' + statusBadge(r.status) + '</td>'+ '<td style="text-align:right;white-space:nowrap;">' + actions + '</td>'+ '</tr>';});$body.html(html);}).fail(function () {$body.html('<tr><td colspan="7" style="text-align:center;color:#dc2626;padding:24px;">'+ 'Failed to load — check the server log.</td></tr>');});}$(document).on('change', '#nslDays', renderScheduledList);$(document).on('click', '#nslRefresh', renderScheduledList);$(document).on('click', '.nsl-cancel-btn', function () {var scheduleId = $(this).data('schedule-id');if (!scheduleId) return;if (!confirm('Cancel this scheduled notification?')) return;doPostAjaxRequestWithJsonHandler(context + '/notificationSchedule/cancel?scheduleId=' + encodeURIComponent(scheduleId),'{}',function (response) {if (response === 'true' || response === true) {renderScheduledList();renderCalendarMonth();} else {alert('Could not cancel — the schedule may already be dispatched or cancelled.');}});});$(document).on('click', '.nsl-add-btn', function () {var campaignId = $(this).data('campaign-id');if (!campaignId) return;var dateVal = prompt('Add a schedule — enter date (YYYY-MM-DD):');if (!dateVal) return;var timeVal = prompt('Enter time (HH:MM, 24-hour):');if (!timeVal) return;var scheduledAt = dateVal + 'T' + timeVal + ':00';if (new Date(scheduledAt) <= new Date()) {alert('Pick a future date/time.');return;}doPostAjaxRequestWithJsonHandler(context + '/notificationSchedule/add?campaignId=' + encodeURIComponent(campaignId)+ '&scheduledAt=' + encodeURIComponent(scheduledAt),'{}',function (response) {if (response === 'false' || response === false) {alert('Could not add — check date/time and try again.');return;}renderScheduledList();renderCalendarMonth();renderRecentList();});});// ─── Recent Notifications list (all formats, all statuses) ───────────var FORMAT_COLOR = {'PUSH': {bg: '#dbeafe', fg: '#1e40af'},'IN_APP_POPUP': {bg: '#fef3c7', fg: '#92400e'},'STORY': {bg: '#fce7f3', fg: '#9d174d'}};function formatBadge(fmt) {var meta = FORMAT_COLOR[fmt] || {bg: '#f3f4f6', fg: '#6b7280'};var label = fmt === 'IN_APP_POPUP' ? 'POPUP' : (fmt || 'PUSH');return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;'+ 'background:' + meta.bg + ';color:' + meta.fg + ';font-size:11px;font-weight:600;">'+ label + '</span>';}function fmtCreated(iso) {if (!iso) return '';var d = new Date(iso);var pad = function (n) {return String(n).padStart(2, '0');};return pad(d.getDate()) + '/' + pad(d.getMonth() + 1) + '/' + d.getFullYear()+ ' ' + formatHM(d);}function renderRecentList() {var days = $('#nrlDays').val() || '30';var $body = $('#nrlBody');$body.html('<tr><td colspan="8" style="text-align:center;color:#9ca3af;padding:24px;">Loading…</td></tr>');$.getJSON(context + '/notificationCampaignList?days=' + encodeURIComponent(days), function (rows) {if (!rows || rows.length === 0) {$body.html('<tr><td colspan="8" style="text-align:center;color:#9ca3af;padding:24px;">'+ 'No notifications created in the last ' + days + ' days.</td></tr>');return;}var html = '';rows.forEach(function (r) {var titleLine = r.title ? '<div style="font-weight:600;color:#111;">' + escapeHtml(r.title) + '</div>' : '';var msgLine = r.message ? '<div style="color:#6b7280;font-size:12px;max-width:420px;'+ 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(r.message) + '</div>' : '';var thumb = r.imageUrl ? '<img src="' + escapeHtml(r.imageUrl) + '" '+ 'style="width:36px;height:36px;object-fit:cover;border-radius:4px;margin-right:8px;vertical-align:middle;" />' : '';var content = '<div style="display:flex;align-items:center;">' + thumb+ '<div>' + titleLine + msgLine + '</div></div>';var groupCell = r.groupName? '<span style="display:inline-block;padding:2px 8px;border-radius:10px;background:#eef2ff;color:#4338ca;font-size:11px;font-weight:600;">'+ escapeHtml(r.groupName) + '</span>': '<span style="color:#9ca3af;">—</span>';var audCount = (r.audienceCount != null ? r.audienceCount : 0);var audienceCell = '<div style="font-weight:600;">' + audCount.toLocaleString() + '</div>'+ '<div style="font-size:11px;color:#6b7280;">' + escapeHtml(r.audienceSummary || '') + '</div>';var viewBtn = '<button type="button" class="nm-btn nm-btn-outline cdm-view-btn" '+ 'data-campaign-id="' + r.campaignId + '" style="padding:2px 8px;font-size:11px;">View</button>';html += '<tr>'+ '<td style="white-space:nowrap;color:#6b7280;">' + fmtCreated(r.createdAt) + '</td>'+ '<td>' + formatBadge(r.format) + '</td>'+ '<td>' + groupCell + '</td>'+ '<td>' + content + '</td>'+ '<td>' + categoryBadge(r.category) + '</td>'+ '<td style="font-size:12px;color:#374151;">' + escapeHtml(r.delivery || '—') + '</td>'+ '<td style="text-align:right;">' + audienceCell + '</td>'+ '<td style="text-align:right;">' + viewBtn + '</td>'+ '</tr>';});$body.html(html);}).fail(function () {$body.html('<tr><td colspan="8" style="text-align:center;color:#dc2626;padding:24px;">'+ 'Failed to load — check the server log.</td></tr>');});}$(document).on('change', '#nrlDays', renderRecentList);$(document).on('click', '#nrlRefresh', renderRecentList);// ─── Full campaign detail modal (content + audience list + schedules) ─function scheduleStatusBadge(st) {var map = {'SCHEDULED': {bg: '#eef2ff', fg: '#4338ca'},'DISPATCHED': {bg: '#dcfce7', fg: '#166534'},'CANCELLED': {bg: '#f3f4f6', fg: '#6b7280'},'FAILED': {bg: '#fee2e2', fg: '#991b1b'}};var meta = map[st] || {bg: '#f3f4f6', fg: '#6b7280'};return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;'+ 'background:' + meta.bg + ';color:' + meta.fg + ';font-size:11px;font-weight:600;">'+ st + '</span>';}function contentFieldRow(label, value) {if (value == null || value === '') return '';return '<tr><td style="padding:4px 8px;color:#6b7280;width:140px;">' + escapeHtml(label) + '</td>'+ '<td style="padding:4px 8px;color:#111;">' + escapeHtml(value) + '</td></tr>';}function renderCampaignDetail(campaignId) {var $body = $('#cdmBody');$('#cdmTitle').text('Campaign detail');$body.html('<div style="text-align:center;color:#9ca3af;padding:24px;">Loading…</div>');$('#campaignDetailModal').modal('show');$.getJSON(context + '/notificationCampaignDetail?campaignId=' + encodeURIComponent(campaignId), function (data) {if (!data || !data.campaign) {$body.html('<div style="text-align:center;color:#dc2626;padding:24px;">Campaign not found.</div>');return;}var c = data.campaign;var content = c.content || {};$('#cdmTitle').text(c.name || 'Campaign #' + c.id);// ── Section: header infovar headerHtml = '<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;">'+ formatBadge(c.format)+ categoryBadge(c.category)+ (c.groupName? '<span style="display:inline-block;padding:2px 8px;border-radius:10px;background:#eef2ff;color:#4338ca;font-size:11px;font-weight:600;">'+ escapeHtml(c.groupName) + '</span>': '')+ '<span style="color:#6b7280;font-size:12px;padding:2px 0;">Created: '+ escapeHtml(fmtCreated(c.createdAt)) + '</span>'+ '</div>';// ── Section: contentvar img = content.imageUrl? '<div style="margin-bottom:8px;"><img src="' + escapeHtml(content.imageUrl)+ '" style="max-width:200px;max-height:120px;border-radius:6px;border:1px solid #e5e7eb;" /></div>': '';var contentTable = '<table style="width:100%;font-size:13px;border-collapse:collapse;">'+ contentFieldRow('Title', content.title)+ contentFieldRow('Message', content.message)+ contentFieldRow('Button Label', content.buttonLabel)+ contentFieldRow('URL', content.url)+ contentFieldRow('Show CTA', content.showCta === true || content.showCta === 'true' ? 'Yes' : (content.showCta === false ? 'No' : null))+ contentFieldRow('Show Close', content.showClose === true || content.showClose === 'true' ? 'Yes' : (content.showClose === false ? 'No' : null))+ contentFieldRow('Show On Page', content.showOnPage)+ contentFieldRow('Auto Close (s)', content.autoCloseSeconds)+ contentFieldRow('Frequency Cap', content.frequencyCap)+ contentFieldRow('Live From', content.liveFrom ? fmtCreated(content.liveFrom) : null)+ contentFieldRow('Live Until', content.liveUntil ? fmtCreated(content.liveUntil) : null)+ '</table>';var contentSection = '<div style="margin-bottom:20px;padding:12px;background:#f9fafb;border-radius:6px;">'+ '<b style="color:#111;font-size:13px;">Content</b><div style="margin-top:8px;">'+ img + contentTable + '</div></div>';// ── Section: audiencevar aud = data.audience || {};var users = aud.sampleUsers || [];var userRows = '';if (users.length === 0) {userRows = '<tr><td colspan="4" style="text-align:center;color:#9ca3af;padding:12px;">No users resolved.</td></tr>';} else {users.forEach(function (u) {userRows += '<tr>'+ '<td>' + escapeHtml(u.name || '—') + '</td>'+ '<td>' + escapeHtml(u.mobile || '—') + '</td>'+ '<td>' + escapeHtml(u.city || '—') + '</td>'+ '<td>' + escapeHtml(u.state || '—') + '</td>'+ '</tr>';});}var audienceHeader = '<b style="color:#111;font-size:13px;">Audience</b>'+ ' <span style="color:#111;font-weight:600;">' + (aud.totalCount || 0).toLocaleString() + '</span>'+ ' <span style="color:#6b7280;font-size:12px;">(' + escapeHtml(aud.summary || '') + ')</span>'+ (aud.truncated ? ' <span style="color:#f59e0b;font-size:11px;">— showing first ' + users.length + '</span>' : '');var audienceSection = '<div style="margin-bottom:20px;padding:12px;background:#f9fafb;border-radius:6px;">'+ audienceHeader+ '<div style="margin-top:8px;max-height:300px;overflow-y:auto;">'+ '<table class="table table-striped" style="margin:0;font-size:12px;">'+ '<thead><tr><th>Name</th><th>Mobile</th><th>City</th><th>State</th></tr></thead>'+ '<tbody>' + userRows + '</tbody>'+ '</table></div></div>';// ── Section: schedulesvar scheds = data.schedules || [];var schedRows = '';if (scheds.length === 0) {schedRows = '<tr><td colspan="4" style="text-align:center;color:#9ca3af;padding:12px;">No schedules — sent immediately.</td></tr>';} else {scheds.forEach(function (s) {schedRows += '<tr>'+ '<td>' + escapeHtml(fmtCreated(s.scheduledAt)) + '</td>'+ '<td>' + scheduleStatusBadge(s.status) + '</td>'+ '<td>' + (s.dispatchedAt ? escapeHtml(fmtCreated(s.dispatchedAt)) : '—') + '</td>'+ '<td style="color:#6b7280;">#' + s.id + '</td>'+ '</tr>';});}var schedulesSection = '<div style="padding:12px;background:#f9fafb;border-radius:6px;">'+ '<b style="color:#111;font-size:13px;">Schedules (' + scheds.length + ')</b>'+ '<div style="margin-top:8px;max-height:250px;overflow-y:auto;">'+ '<table class="table table-striped" style="margin:0;font-size:12px;">'+ '<thead><tr><th>Scheduled At</th><th>Status</th><th>Dispatched At</th><th>ID</th></tr></thead>'+ '<tbody>' + schedRows + '</tbody>'+ '</table></div></div>';$body.html(headerHtml + contentSection + audienceSection + schedulesSection);}).fail(function () {$body.html('<div style="text-align:center;color:#dc2626;padding:24px;">Failed to load — check the server log.</div>');});}$(document).on('click', '.cdm-view-btn', function () {var campaignId = $(this).data('campaign-id');if (campaignId) renderCampaignDetail(campaignId);});// Event click → detail modal (same modal FullCalendar was using)$(document).on('click', '.nc-event', function () {var event = $(this).data('event');if (!event) return;var kind = event.kind || 'push-schedule';$('#ncDetailCampaign').text(event.title);var startD = new Date(event.start);var whenStr = fmtDateHuman(startD);if (event.end && (kind === 'popup-window' || kind === 'story-window')) {whenStr += ' → ' + fmtDateHuman(new Date(event.end));}$('#ncDetailWhen').text(whenStr);$('#ncDetailStatus').text(event.status || event.category || '—');$('#ncDetailScheduleId').text(event.id);// Reset "add another schedule" section every time the modal opens$('#ncAddScheduleBox').hide();$('#ncAddErr').hide().text('');$('#ncAddDate').val('');$('#ncAddTime').val('');var $addBtn = $('#ncAddScheduleBtn').off('click').hide();// Only PUSH schedules can be duplicated — popup/story are windowed, not repeatableif (kind === 'push-schedule' && event.campaignId) {$addBtn.show().on('click', function () {$('#ncAddScheduleBox').toggle();if ($('#ncAddScheduleBox').is(':visible') && !$('#ncAddDate').val()) {var d = new Date();$('#ncAddDate').val(fmtDate(d));$('#ncAddTime').val(formatHM(d));}});$('#ncAddSaveBtn').off('click').on('click', function () {var dateVal = $('#ncAddDate').val();var timeVal = $('#ncAddTime').val();if (!dateVal || !timeVal) {$('#ncAddErr').text('Pick both a date and a time.').show();return;}// Backend parses LocalDateTime — send "yyyy-MM-ddTHH:mm:00"var scheduledAt = dateVal + 'T' + timeVal + ':00';if (new Date(scheduledAt) <= new Date()) {$('#ncAddErr').text('Pick a future date/time.').show();return;}$('#ncAddErr').hide();doPostAjaxRequestWithJsonHandler(context + '/notificationSchedule/add?campaignId=' + encodeURIComponent(event.campaignId)+ '&scheduledAt=' + encodeURIComponent(scheduledAt),'{}',function (response) {// response is either the new schedule id (number) or 'false'if (response === 'false' || response === false) {$('#ncAddErr').text('Could not add — check date/time and try again.').show();return;}$('#scheduleDetailModal').modal('hide');renderCalendarMonth();renderScheduledList();renderRecentList();});});}var $btn = $('#ncCancelBtn').off('click');if (kind === 'push-schedule') {if (event.status !== 'SCHEDULED') {$btn.prop('disabled', true).text('Not cancellable');} else {$btn.prop('disabled', false).text('Cancel Schedule').on('click', function () {if (!confirm('Cancel this scheduled notification?')) return;doPostAjaxRequestWithJsonHandler(context + '/notificationSchedule/cancel?scheduleId=' + encodeURIComponent(event.id),'{}',function (response) {$('#scheduleDetailModal').modal('hide');if (response === 'true' || response === true) {renderCalendarMonth();renderScheduledList();renderRecentList();} else {alert('Could not cancel — the schedule may already be dispatched or cancelled.');}});});}} else {var alreadyEnded = event.end && new Date(event.end) < new Date();if (alreadyEnded) {$btn.prop('disabled', true).text('Already ended');} else {$btn.prop('disabled', false).text('Deactivate now').on('click', function () {var noun = kind === 'popup-window' ? 'pop-up' : 'story';if (!confirm('Deactivate this ' + noun + ' immediately? Partners will stop seeing it.')) return;doPostAjaxRequestWithJsonHandler(context + '/notificationCampaign/deactivate?campaignId=' + encodeURIComponent(event.campaignId),'{}',function (response) {$('#scheduleDetailModal').modal('hide');if (response === 'true' || response === true) {renderCalendarMonth();renderScheduledList();renderRecentList();} else {alert('Could not deactivate — the row may be missing or already ended.');}});});}}$('#scheduleDetailModal').modal('show');});function fmtDateHuman(d) {var pad = function (n) {return String(n).padStart(2, '0');};return pad(d.getDate()) + '/' + pad(d.getMonth() + 1) + '/' + d.getFullYear() + ' ' + formatHM(d);}// ─── Format cards ────────────────────────────────────────────────────var FORMAT_META = {'PUSH': {cta: 'Send now', detailsTitle: 'Notification details', label: 'Push notification'},'IN_APP_POPUP': {cta: 'Publish now', detailsTitle: 'Pop-up details', label: 'In-app pop-up'},'STORY': {cta: 'Publish now', detailsTitle: 'Story details', label: 'Story'}};// Per-format cache of title + message. Push, Popup and Story are meant for// different surfaces (tray vs modal vs story rail) with different constraints,// so their text should not bleed across formats when the admin switches.var contentByFormat = {PUSH: {title: '', message: ''},IN_APP_POPUP: {title: '', message: ''},STORY: {title: '', message: ''}};$(document).on('click', '.nm-format', function () {var fmt = $(this).data('format');var prevFmt = activeFormat();// Snapshot outgoing format's text before we swapif (prevFmt && contentByFormat[prevFmt]) {contentByFormat[prevFmt].title = $('#title').val();contentByFormat[prevFmt].message = $('#message').val();}$('.nm-format').removeClass('active');$(this).addClass('active');$('.nm-panel').removeClass('active').hide();$('.nm-panel[data-panel="' + fmt + '"]').addClass('active').show();$('.nm-preview').removeClass('active').hide();$('.nm-preview[data-preview="' + fmt + '"]').addClass('active').show();// Restore incoming format's cached text (empty if the admin never typed// anything for this format yet). `trigger('input')` refreshes the char-count// and the live preview widgets.var cached = contentByFormat[fmt] || {title: '', message: ''};$('#title').val(cached.title).trigger('input');$('#message').val(cached.message).trigger('input');var meta = FORMAT_META[fmt];$('#nmPrimaryCtaLabel').text(meta.cta);$('#nmDetailsTitle').text(meta.detailsTitle);$('#nmSummaryFormat').text(meta.label);refreshCategorySummary();refreshExpirySummary();});// Keep the cache in sync so switching mid-typing preserves the current text.$(document).on('input', '#title', function () {var f = activeFormat();if (contentByFormat[f]) {contentByFormat[f].title = $(this).val();}});$(document).on('input', '#message', function () {var f = activeFormat();if (contentByFormat[f]) {contentByFormat[f].message = $(this).val();}});function activeFormat() {return $('.nm-format.active').data('format') || 'PUSH';}function getActiveCategorySelect() {var fmt = activeFormat();if (fmt === 'IN_APP_POPUP') return $('#nmCategoryPopup');if (fmt === 'STORY') return $('#nmCategoryStory');return $('#nmCategory');}// ─── Live preview updates ────────────────────────────────────────────$(document).on('input', '#title', function () {var v = $(this).val();$('#titleCount').text(v.length);var fallback = 'Restock offer: up to 40% margin this week';$('#nmPreviewTitlePush,#nmPreviewTitlePopup,#nmPreviewTitleStory').text(v || fallback);});$(document).on('input', '#message', function () {var v = $(this).val();$('#messageCount').text(v.length);var fallback = 'Order now and earn extra margin on staples. Free logistics on orders above ₹5,000. Tap to view the catalogue.';$('#nmPreviewBodyPush,#nmPreviewBodyPopup,#nmPreviewBodyStory').text(v || fallback);});$(document).on('input', '#buttonLabelPopup', function () {$('#nmPreviewCtaPopup').text($(this).val() || 'View offer');});$(document).on('input', '#buttonLabelStory', function () {$('#nmPreviewCtaStory').text(($(this).val() || 'View offer') + ' ↑');});$(document).on('change', '#showClose', function () {$('#nmPopupPreviewClose').toggle($(this).is(':checked'));});$(document).on('change', '#showCta', function () {$('#nmPreviewCtaPopup').toggle($(this).is(':checked'));});$(document).on('change', '#expiresAfterPolicy', function () {$('#storyExpiryOnDateField').toggle($(this).val() === 'ON_DATE');refreshExpirySummary();});// ─── Summary bindings ───────────────────────────────────────────────$(document).on('input', '#campaignName', function () {$('#nmSummaryCampaign').text($(this).val() || '—');});$(document).on('change', '#nmCategory,#nmCategoryPopup,#nmCategoryStory', refreshCategorySummary);$(document).on('change', '#expireSat,#popupLiveUntil,#storyExpiryOnDate,#storyLiveFrom,#popupLiveFrom', refreshExpirySummary);$(document).on('change', '#selectedUrl', function () {var v = $(this).val();$('#nmSummaryTap').text((v && v !== 'other') ? v : '—');});$(document).on('input', '#url', function () {if ($('#selectedUrl').val() === 'other') {$('#nmSummaryTap').text($(this).val() || '—');}});// "None" tap behaviour hides the Link picker entirely so the admin can ship// an info-only notification (no navigation, no CTA).$(document).on('change', '#type', function () {if ($(this).val() === 'none') {$('#nmLinkField').hide();$('#selectedUrl').val('');$('#url').val('').hide();$('#nmSummaryTap').text('None');} else {$('#nmLinkField').show();}});$(document).on('change', '#fofo-users, #state-users, #internal-users', updateRecipientSummary);function refreshCategorySummary() {$('#nmSummaryCategory').text(getActiveCategorySelect().find('option:selected').text() || 'Not set');}function refreshExpirySummary() {var v;var fmt = activeFormat();if (fmt === 'PUSH') {if ($('#nmHasExpiry').val() === 'no') {v = 'Never (lifetime)';} else {v = $('#expireSat').val();}} else if (fmt === 'IN_APP_POPUP') {v = $('#popupLiveUntil').val();} else if (fmt === 'STORY') {v = $('#expiresAfterPolicy').val() === 'ON_DATE'? $('#storyExpiryOnDate').val(): '24 h from publish';}$('#nmSummaryExpires').text(v || '—');}// Show/hide expiry date input based on the lifetime toggle$(document).on('change', '#nmHasExpiry', function () {$('#nmExpiryDateField').toggle($(this).val() === 'yes');if ($(this).val() === 'no') {$('#expireSat').val('');}refreshExpirySummary();});function updateRecipientSummary() {var partners = ($('#fofo-users').val() || []).length;var states = ($('#state-users').val() || []).length;var users = ($('#internal-users').val() || []).length;var total = partners + states + users;if (total === 0) {$('#nmSummaryRecipients').text('Not selected');return;}var parts = [];if (partners) {parts.push(partners + ' partner' + (partners > 1 ? 's' : ''));}if (states) {parts.push(states + ' state' + (states > 1 ? 's' : ''));}if (users) {parts.push(users + ' user' + (users > 1 ? 's' : ''));}$('#nmSummaryRecipients').text(parts.join(' · '));}// ─── Image dropzone ─────────────────────────────────────────────────$(document).on('click', '.nm-dropzone', function (e) {if ($(e.target).is('.nm-file-input')) {return;}$(this).find('.nm-file-input').trigger('click');});$(document).on('dragover', '.nm-dropzone', function (e) {e.preventDefault();$(this).addClass('nm-dropzone-hover');});$(document).on('dragleave dragend drop', '.nm-dropzone', function () {$(this).removeClass('nm-dropzone-hover');});$(document).on('drop', '.nm-dropzone', function (e) {e.preventDefault();handleFiles(this, e.originalEvent.dataTransfer.files);});$(document).on('change', '.nm-file-input', function () {handleFiles($(this).closest('.nm-dropzone')[0], this.files);this.value = ''; // allow re-selecting the same file});$(document).on('click', '.nm-thumb-x', function (e) {e.stopPropagation();var $list = $(this).closest('.nm-thumb-list');$(this).closest('.nm-thumb').remove();if ($list.data('target') === 'popupImage') {refreshPopupPreviewImage();}});// Reflect the uploaded/removed popup image in the live preview card.function refreshPopupPreviewImage() {var $firstImg = $('.nm-thumb-list[data-target="popupImage"] .nm-thumb img').first();var $preview = $('#nmPreviewImagePopup');if ($firstImg.length && $firstImg.attr('src')) {$preview.attr('src', $firstImg.attr('src')).show();} else {$preview.attr('src', '').hide();}}function handleFiles(dropzone, fileList) {if (!fileList || fileList.length === 0) {return;}var $dz = $(dropzone);var target = $dz.data('target');var multi = $dz.data('multi') === true;var $list = $('.nm-thumb-list[data-target="' + target + '"]');if (!multi) {$list.empty();}for (var i = 0; i < fileList.length; i++) {uploadOne(fileList[i], $list);if (!multi) {break;}}}function uploadOne(file, $list) {if (!file.type || file.type.indexOf('image/') !== 0) {alert('Only image files are accepted.');return;}if (file.size > 5 * 1024 * 1024) {alert('Image is larger than 5 MB — please compress and retry.');return;}var $thumb = $('<li class="nm-thumb"></li>');var $spinner = $('<div class="nm-thumb-spinner"><i class="fa fa-spinner fa-spin"></i></div>');$thumb.append($spinner);$list.append($thumb);// Local preview via FileReadervar reader = new FileReader();reader.onload = function (e) {$thumb.prepend('<img src="' + e.target.result + '">');if ($list.data('target') === 'popupImage') {refreshPopupPreviewImage();}};reader.readAsDataURL(file);// Upload to server; store the returned URL/id on the thumb.doAjaxUploadRequestHandler(context + '/document-upload', 'POST', file, function (response) {var docId = response && response.response && response.response.document_id;if (!docId) {$thumb.remove();alert('Upload failed. Try again.');return;}var url = context + '/document/' + docId;$thumb.data('doc-id', docId).data('url', url);$spinner.remove();$thumb.append('<span class="nm-thumb-x">×</span>');});}// ─── Linked entities: schemes / offers / products ────────────────────// Cache keyed by URL (which encodes recipient filters). Type + same recipients → cache hit.var LINKABLE_CACHE = {};function buildLinkableUrl(type) {var endpoint = type === 'SCHEME' ? 'schemes' : 'offers';var fofoIds = $('#fofo-users').val() || [];var stateIds = $('#state-users').val() || [];var qs = [];if (fofoIds.length) {qs.push('fofoIds=' + fofoIds.join(','));}if (stateIds.length) {qs.push('stateIds=' + stateIds.join(','));}return context + '/notificationLinkables/' + endpoint + (qs.length ? '?' + qs.join('&') : '');}// If recipients change while a scheme/offer picker is open, refetch with the new filter.$(document).on('change', '#fofo-users, #state-users', function () {LINKABLE_CACHE = {}; // invalidate the whole cache — filters affect every fetchvar type = $('#linkableType').val();if (type === 'SCHEME' || type === 'OFFER') {$('#linkableType').trigger('change'); // reload the picker with new filters}});$(document).on('change', '#linkableType', function () {var type = $(this).val();var $picker = $('#linkablePicker');var $searchWrap = $('#linkableSearchWrap');if (!type) {$picker.hide();$searchWrap.hide();return;}if (type === 'PRODUCT') {$picker.hide();$searchWrap.show();initLinkableProductTypeahead();$('#linkableSearchInput').val('').focus();return;}// SCHEME / OFFER — build the URL from the current recipient selection.$searchWrap.hide();$picker.show().html('<option value="">— Loading —</option>');var url = buildLinkableUrl(type);if (LINKABLE_CACHE[url]) {renderLinkableOptions($picker, LINKABLE_CACHE[url]);} else {$.getJSON(url, function (data) {var parsed = typeof data === 'string' ? JSON.parse(data) : data;LINKABLE_CACHE[url] = parsed;renderLinkableOptions($picker, parsed);}).fail(function () {$picker.html('<option value="">— Failed to load —</option>');});}});function renderLinkableOptions($sel, items) {var opts = '<option value="">— Pick one —</option>';items.forEach(function (it) {opts += '<option value="' + it.id + '" data-label="' + escAttr(it.label) + '" data-type="' + it.type + '">'+ escHtml(it.label) + '</option>';});$sel.html(opts);}$(document).on('change', '#linkablePicker', function () {var $opt = $(this).find('option:selected');if (!$opt.val()) {return;}addLinkedEntity($opt.data('type'), parseInt($opt.val(), 10), $opt.data('label'));$(this).val('');});// Product typeahead — Bootstrap 3 typeahead widget against Solr-backed /entity.// Same widget the rest of the app uses (getItemAheadOptions pattern) so behaviour and// dropdown positioning stay consistent.function initLinkableProductTypeahead() {var $input = $('#linkableSearchInput');if (!$input.length || $input.data('nm-typeahead-init')) {return;}$input.data('nm-typeahead-init', true);$input.typeahead('destroy').typeahead({source: function (q, process) {if (q && q.length >= 2) {return $.ajax(context + '/entity', {global: false,data: {query: q, limit: 30, activeOnly: true},success: function (data) {var parsed = typeof data === 'string' ? JSON.parse(data) : data;process(parsed || []);}});}},delay: 250,items: 20,minLength: 2,displayText: function (item) {return item.title_s || ('Catalog ' + item.catalogId_i);},afterSelect: function (item) {var label = item.title_s || ('Catalog ' + item.catalogId_i);addLinkedEntity('PRODUCT', item.catalogId_i, label);$input.val('');}});}function addLinkedEntity(type, id, label) {var key = type + ':' + id;var duplicate = false;$('#linkedEntitiesList li').each(function () {if ($(this).data('key') === key) {duplicate = true;}});if (duplicate) {return;}var $li = $('<li class="nm-chip"></li>').attr('data-key', key).data({type: type, id: id, label: label}).text('[' + type.charAt(0) + '] ' + label).append(' <span class="nm-chip-x removeLinkedEntity">×</span>');$('#linkedEntitiesList').append($li);}$(document).on('click', '.removeLinkedEntity', function () {$(this).closest('li').remove();});function collectLinkedEntities() {var out = [];$('#linkedEntitiesList li').each(function () {out.push({type: $(this).data('type'), id: $(this).data('id'), label: $(this).data('label')});});return out;}function escHtml(s) {return String(s || '').replace(/[&<>]/g, function (c) {return {'&': '&', '<': '<', '>': '>'}[c];});}function escAttr(s) {return String(s || '').replace(/"/g, '"');}function collectImages(target) {var out = [];$('.nm-thumb-list[data-target="' + target + '"] .nm-thumb').each(function () {var url = $(this).data('url');if (url) {out.push(url);}});return out;}// ─── Category → legacy message_type mapping ─────────────────────────var CATEGORY_TO_MESSAGE_TYPE = {'ANNOUNCE': 'scheme', // was OFFER — same underlying route'NOTIFICATION': 'notification', // was INFO'REMINDER': 'reminder','ALERT': 'pricechange'};// ─── Schedule modal ─────────────────────────────────────────────────$(document).on('click', '#nmScheduleBtn', function () {$('#nmScheduleModal').modal('show');refreshScheduleButtonState(); // set initial disabled state on open});// "Schedule & save" is only meaningful when at least one date has been added.// Grey out until then so the admin can't submit an empty schedule.function refreshScheduleButtonState() {var hasDates = $('#scheduleDatesList li').length > 0;$('#nmSubmitScheduleBtn').prop('disabled', !hasDates).css('opacity', hasDates ? 1 : 0.5).css('cursor', hasDates ? 'pointer' : 'not-allowed');}$(document).on('click', '#addScheduleDate', function () {var raw = $('#scheduleDateInput').val();if (!raw) {alert('Pick a date and time');return;}var iso = raw.length === 16 ? raw + ':00' : raw;var duplicate = false;$('#scheduleDatesList li').each(function () {if ($(this).data('ts') === iso) {duplicate = true;}});if (duplicate) {return;}var $li = $('<li class="nm-chip"></li>').attr('data-ts', iso).text(raw.replace('T', ' ')).append(' <span class="nm-chip-x removeScheduleDate">×</span>');$('#scheduleDatesList').append($li);$('#scheduleDateInput').val('');refreshScheduleButtonState();});$(document).on('click', '.removeScheduleDate', function () {$(this).closest('li').remove();refreshScheduleButtonState();});$(document).on('click', '#nmSubmitScheduleBtn', function () {var scheduledTimestamps = collectScheduledTimestamps();if (scheduledTimestamps.length === 0) {alert('Add at least one date, or use "Send now" to send immediately.');return;}if (!validateAndConfirm('Schedule for ' + scheduledTimestamps.length + ' date(s)?')) {return;}submitNotification(scheduledTimestamps, function () {$('#nmScheduleModal').modal('hide');$('#scheduleDatesList').empty();});});function collectScheduledTimestamps() {var list = [];$('#scheduleDatesList li').each(function () {list.push($(this).data('ts'));});return list;}// ─── Send / Publish now ─────────────────────────────────────────────$(document).on('click', '.create-notification-button', function () {var confirmText = activeFormat() === 'PUSH' ? 'Send this notification now?' : 'Publish now?';if (!validateAndConfirm(confirmText)) {return;}submitNotification([], function () { /* stay on the form */});});function validateAndConfirm(confirmText) {var campaignName = $('#campaignName').val();var title = $('#title').val();var message = $('#message').val();var category = getActiveCategorySelect().val();var type = $('#type').val();var fmt = activeFormat();if (!campaignName) {alert('Campaign name is required');return false;}if (!title) {alert('Title is required');return false;}if (!message) {alert('Message is required');return false;}if (!category) {alert('Category is required');return false;}if (!type) {alert('Tap action is required');return false;}if (fmt === 'PUSH' && $('#nmHasExpiry').val() === 'yes' && !$('#expireSat').val()) {alert('Expiry date is required (or switch life span to "Lifetime")');return false;}if (fmt === 'IN_APP_POPUP' && !$('#showOnPage').val()) {alert('"Show on page (route)" is required — e.g. /dashboard');return false;}if (fmt === 'IN_APP_POPUP' && !$('#popupLiveUntil').val()) {alert('"Live until" date is required');return false;}if (fmt === 'STORY' && $('#expiresAfterPolicy').val() === 'ON_DATE' && !$('#storyExpiryOnDate').val()) {alert('"Expires on" date is required');return false;}if (type === 'url') {var linkVal = $('#selectedUrl').val();var effectiveUrl = (linkVal === 'other') ? $('#url').val() : linkVal;if (!effectiveUrl) {alert('Link to open is required');return false;}}return confirm(confirmText);}function submitNotification(scheduledTimestamps, onSuccess) {var fmt = activeFormat();var typeVal = $('#type').val();var linkVal = $('#selectedUrl').val();var effectiveUrl = (typeVal === 'none')? '': (linkVal === 'other' ? $('#url').val() : linkVal);var category = getActiveCategorySelect().val();var selectedGroup = $('#nmCampaignGroup').val();var campaignGroupId = (selectedGroup && selectedGroup !== '__new__') ? parseInt(selectedGroup, 10) : null;var payload = {stateIds: $('#state-users').val(),userIds: ($('#fofo-users').val() || []).concat($('#internal-users').val() || []),campaignGroupId: campaignGroupId,campaignName: $('#campaignName').val(),title: $('#title').val(),message: $('#message').val(),type: $('#type').val(),url: effectiveUrl,messageType: CATEGORY_TO_MESSAGE_TYPE[category],category: category,format: fmt,linkedEntities: collectLinkedEntities()};if (fmt === 'PUSH') {var pushImgs = collectImages('pushImages');payload.images = pushImgs;payload.imageUrl = pushImgs[0] || ''; // first as legacy imageUrl for older readerspayload.showImage = pushImgs.length > 0 ? 'true' : 'false';payload.documentId = localStorage.getItem('notifyDocument');// Lifetime toggle: only send an expiry when the admin explicitly chose one.// null / omit → mobile treats as never-expiring.payload.expiresat = $('#nmHasExpiry').val() === 'yes'? getDatesFromPicker('input[name="dateTime"]').startDate: null;} else if (fmt === 'IN_APP_POPUP') {var popupImgs = collectImages('popupImage');payload.imageUrl = popupImgs[0] || '';payload.buttonLabel = $('#buttonLabelPopup').val();payload.showCta = $('#showCta').is(':checked');payload.showClose = $('#showClose').is(':checked');payload.showOnPage = $('#showOnPage').val() || null;payload.autoCloseSeconds = $('#autoCloseSeconds').val() ? parseInt($('#autoCloseSeconds').val(), 10) : null;payload.frequencyCap = 'ALWAYS';payload.liveFrom = normalizeDate($('#popupLiveFrom').val());// liveUntil is a date input; admin intent is "still visible through end of that day",// so anchor at 23:59:59 instead of the default midnight start-of-day.payload.liveUntil = normalizeDate($('#popupLiveUntil').val(), true);payload.expiresat = payload.liveUntil;} else if (fmt === 'STORY') {payload.buttonLabel = $('#buttonLabelStory').val();payload.storyCards = collectImages('storyCards');payload.postInCategory = $('#postInCategory').val();payload.playDurationSeconds = parseInt($('#playDurationSeconds').val(), 10);payload.expiresAfterPolicy = $('#expiresAfterPolicy').val();payload.liveFrom = normalizeDate($('#storyLiveFrom').val());// Anchor a concrete liveUntil + expiresat so downstream (React app, cron, expiry check)// doesn't have to derive it. FIXED_HOURS = liveFrom + 24 h; ON_DATE = whatever the admin picks.if (payload.expiresAfterPolicy === 'ON_DATE') {payload.expiresat = normalizeDate($('#storyExpiryOnDate').val());payload.liveUntil = payload.expiresat;} else {payload.liveUntil = addHoursIso(payload.liveFrom, 24);payload.expiresat = payload.liveUntil;}}if (scheduledTimestamps && scheduledTimestamps.length > 0) {payload.scheduledTimestamps = scheduledTimestamps;}doPostAjaxRequestWithJsonHandler(context + '/sendNotification',JSON.stringify(payload),function (response) {if (response === 'true' || response === true) {alert(scheduledTimestamps.length > 0? 'Scheduled successfully': (fmt === 'PUSH' ? 'Sent successfully' : 'Published successfully'));localStorage.removeItem('notifyDocument');if (onSuccess) {onSuccess();}} else {alert('Something went wrong. Check the campaign and try again.');}});}function normalizeDate(v, endOfDay) {if (!v) {return null;}if (v.length === 10) {return v + (endOfDay ? 'T23:59:59' : 'T00:00:00');}if (v.length === 16) {return v + ':00';}return v;}function addHoursIso(isoLocal, hours) {if (!isoLocal) {return null;}var d = new Date(isoLocal);if (isNaN(d.getTime())) {return null;}d.setHours(d.getHours() + hours);// Back to local-ISO without the trailing Z.var pad = function (n) {return String(n).padStart(2, '0');};return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())+ 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());}// ─── Reset ──────────────────────────────────────────────────────────$(document).on('click', '#nmResetBtn', function () {if (!confirm('Clear all fields?')) {return;}$('input[type="text"], input[type="date"], input[type="datetime-local"], textarea').val('');$('#titleCount, #messageCount').text('0');$('#showImage').prop('checked', false).val('false');$('#showCta, #showClose').prop('checked', true);$('#fofo-users, #state-users, #internal-users').val([]).multiselect('refresh');$('#type').val('url');$('#selectedUrl').val('').trigger('change');$('.nm-select[name="category"]').val('').trigger('change');$('.nm-thumb-list').empty();$('#scheduleDatesList').empty();$('#linkedEntitiesList').empty();$('#linkableType').val('');$('#linkablePicker').hide();$('#linkableSearchWrap').hide();$('#nmCampaignGroup').val('');// Reset life span toggle to default (lifetime / no expiry)$('#nmHasExpiry').val('no').trigger('change');// Clear the per-format title/message cache toocontentByFormat.PUSH = {title: '', message: ''};contentByFormat.IN_APP_POPUP = {title: '', message: ''};contentByFormat.STORY = {title: '', message: ''};// Clear popup preview imagerefreshPopupPreviewImage();$('#url').hide();$('.nm-format').removeClass('active');$('.nm-format[data-format="PUSH"]').addClass('active').trigger('click');$('#nmSummaryCampaign, #nmSummaryTap, #nmSummaryExpires').text('—');$('#nmSummaryRecipients').text('Not selected');$('#nmSummaryCategory').text('Not set');$('#nmSummarySchedule').text('Send now');localStorage.removeItem('notifyDocument');});});function loadNotificationPanel(domId) {localStorage.removeItem('notifyDocument');doGetAjaxRequestHandler(context + '/sendNotificationPanel', function (response) {$('#' + domId).html(response);});}// Attachment document upload (Push format only — separate from image dropzones)$(document).on('input', '#notifyDocument', function () {if (confirm('Document has been selected, do you want to upload?')) {var fileSelector = $('#notifyDocument')[0];if (fileSelector !== undefined && fileSelector.files[0] !== undefined) {var uploadUrl = context + '/document-upload';var file = this.files[0];doAjaxUploadRequestHandler(uploadUrl, 'POST', file, function (response) {var documentId = response.response.document_id;localStorage.setItem('notifyDocument', documentId);});}}});