Subversion Repositories SmartDukaan

Rev

Rev 30646 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 30646 Rev 37508
Line 1... Line 1...
1
$(function() {
1
$(function () {
2
	$(document).on('click', '.send-notification', function() {
2
    $(document).on('click', '.send-notification', function () {
-
 
3
        loadNotificationPanel('main-content');
-
 
4
    });
3
 
5
 
-
 
6
    // ─── Mobile Preview modal ────────────────────────────────────────────
-
 
7
    // On mobile the preview column is hidden. The floating "Preview" button
-
 
8
    // moves the .nm-preview-sticky node into the modal body on open, then
-
 
9
    // puts it back on close — so the same DOM (and all the jQuery selectors
-
 
10
    // that update it as the admin types) works in both places.
-
 
11
    $(document).on('click', '#nmPreviewFab', function () {
-
 
12
        var $preview = $('.nm-preview-sticky');
-
 
13
        if ($preview.length) {
-
 
14
            $preview.appendTo('#nmMobilePreviewBody');
-
 
15
        }
-
 
16
        $('#nmMobilePreviewModal').modal('show');
-
 
17
    });
-
 
18
    $('#nmMobilePreviewModal').on('hidden.bs.modal', function () {
-
 
19
        var $preview = $(this).find('.nm-preview-sticky');
-
 
20
        if ($preview.length) {
4
		loadNotificationPanel("main-content");
21
            $preview.appendTo('.nm-preview-col');
-
 
22
        }
5
	});
23
	});
6
 
24
 
7
	$(document).on('click', '.create-notification-button',
25
    // ─── Campaign Group picker ───────────────────────────────────────────
8
					function() {
26
    function loadCampaignGroups(preselectId) {
9
						console.log(localStorage.getItem("notifyDocument"));
27
        $.getJSON(context + '/campaignGroups', function (data) {
10
						var userIds = $('#fofo-users').val().concat(
28
            var parsed = typeof data === 'string' ? JSON.parse(data) : data;
11
								$('#internal-users').val());
29
            var $sel = $('#nmCampaignGroup');
12
						console.log("multipleUsers", userIds);
30
            $sel.find('option.nm-group-opt').remove();
13
						
31
            var $createNew = $sel.find('option[value="__new__"]');
14
						var stateIds = $('#state-users').val();
32
            (parsed || []).forEach(function (g) {
15
						
33
                var opt = $('<option class="nm-group-opt"></option>')
16
						console.log("stateIds"+stateIds);
34
                    .attr('value', g.id).text(g.name);
17
 
35
                $createNew.before(opt);
18
						var campaignName = $('input[name="campaignName"]')
36
            });
19
								.val();
37
            if (preselectId) {
20
						var title = $('input[name="title"]').val();
38
                $sel.val(String(preselectId));
21
						var message = $('input[name="message"]').val();
39
            }
22
						var imageUrl = $('input[name="imageUrl"]').val();
40
        });
23
						var type = $('#type').val();
41
    }
24
						var expiryTime = $('#expireSat').val();
42
 
25
 
43
    loadCampaignGroups();
26
						var val = $("#selectedUrl").val();
44
 
27
						console.log(val);
45
    $(document).on('change', '#nmCampaignGroup', function () {
28
						if (val == "other") {
46
        if ($(this).val() === '__new__') {
29
							url = $('input[name="url"]').val();
47
            $(this).val('');
30
						} else {
48
            $('#nmGroupName, #nmGroupDesc, #nmGroupStarts, #nmGroupEnds').val('');
31
							url = val;
49
            $('#nmGroupModal').modal('show');
32
						}
50
        }
33
 
51
    });
34
						var messageType = $('#messageType').val();
52
 
35
 
53
    $(document).on('click', '#nmCreateGroupBtn', function () {
36
						$('#showImage').on('change', function() {
54
        var name = $('#nmGroupName').val().trim();
37
							this.value = this.checked ? true : false;
55
        if (!name) {
38
						}).change();
56
            alert('Group name is required');
39
 
57
            return;
40
						if (campaignName === ""
58
        }
41
								&& title === "" && message === ""
59
        var body = {
42
								&& expiryTime === "" && type === ""
60
            name: name,
43
								&& messageType === "") {
61
            description: $('#nmGroupDesc').val() || null,
44
							alert("Field can't be empty");
62
            startsOn: $('#nmGroupStarts').val() || null,
45
							return;
63
            endsOn: $('#nmGroupEnds').val() || null
46
						}
64
        };
47
					
65
        doPostAjaxRequestWithJsonHandler(context + '/campaignGroups', JSON.stringify(body), function (response) {
48
						if (campaignName === "") {
66
            var parsed = typeof response === 'string' ? JSON.parse(response) : response;
49
							alert("Campaign name is required");
67
            if (parsed && parsed.id) {
50
							return;
68
                $('#nmGroupModal').modal('hide');
51
						}
69
                loadCampaignGroups(parsed.id);
52
						if (title === "") {
70
            } else {
53
							alert("Title Name is required");
71
                alert('Could not create the group. Try again.');
54
							return;
72
            }
55
						}
73
        });
56
						if (message === "") {
74
    });
57
							alert("Message is required");
75
 
58
							return;
76
    // ─── Tab strip ───────────────────────────────────────────────────────
59
						}
77
    $(document).on('click', '.nm-tab', function () {
60
						if (expiryTime === "") {
78
        var tab = $(this).data('tab');
61
							alert("ExpiryTime is required");
79
        $('.nm-tab').removeClass('active');
62
							return;
80
        $(this).addClass('active');
63
						}
81
        $('.nm-tab-content').removeClass('active');
64
 
82
        $('.nm-tab-content[data-tab-content="' + tab + '"]').addClass('active');
65
						if (messageType === null) {
83
 
66
							alert("message type is required");
84
        if (tab === 'calendar') {
-
 
85
            initCalendar();
-
 
86
        }
-
 
87
    });
-
 
88
 
-
 
89
    // ─── Custom month-grid calendar (replaces FullCalendar 1.5.4) ────────
-
 
90
    var calRef = null;   // { year, month }  — 0-indexed month
-
 
91
 
-
 
92
    function initCalendar() {
-
 
93
        if (!calRef) {
-
 
94
            var now = new Date();
-
 
95
            calRef = {year: now.getFullYear(), month: now.getMonth()};
-
 
96
        }
-
 
97
        renderCalendarMonth();
-
 
98
        renderScheduledList();
-
 
99
        renderRecentList();
-
 
100
    }
-
 
101
 
-
 
102
    $(document).on('click', '#ncPrev', function () {
-
 
103
        calRef.month -= 1;
-
 
104
        if (calRef.month < 0) {
-
 
105
            calRef.month = 11;
-
 
106
            calRef.year -= 1;
-
 
107
        }
-
 
108
        renderCalendarMonth();
-
 
109
    });
-
 
110
    $(document).on('click', '#ncNext', function () {
-
 
111
        calRef.month += 1;
-
 
112
        if (calRef.month > 11) {
-
 
113
            calRef.month = 0;
-
 
114
            calRef.year += 1;
-
 
115
        }
-
 
116
        renderCalendarMonth();
-
 
117
    });
-
 
118
    $(document).on('click', '#ncToday', function () {
-
 
119
        var now = new Date();
-
 
120
        calRef = {year: now.getFullYear(), month: now.getMonth()};
-
 
121
        renderCalendarMonth();
-
 
122
    });
-
 
123
 
-
 
124
    var MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
-
 
125
 
-
 
126
    function renderCalendarMonth() {
-
 
127
        var y = calRef.year, m = calRef.month;
-
 
128
        $('#ncTitle').text(MONTH_NAMES[m] + ' ' + y);
-
 
129
 
-
 
130
        var firstOfMonth = new Date(y, m, 1);
-
 
131
        var startDow = firstOfMonth.getDay();                // 0 = Sun
-
 
132
        var gridStart = new Date(y, m, 1 - startDow);        // first Sunday of the visible grid
-
 
133
        var gridEnd = new Date(y, m, 1 - startDow + 42);     // 6 weeks × 7 days
-
 
134
 
-
 
135
        var from = fmtDate(gridStart);
-
 
136
        var to = fmtDate(gridEnd);
-
 
137
 
-
 
138
        // Build the grid skeleton first (so it appears immediately, events fill in on fetch)
-
 
139
        var $body = $('#ncBody').empty();
-
 
140
        var today = fmtDate(new Date());
-
 
141
        var eventsByDate = {};  // filled by AJAX
-
 
142
 
-
 
143
        for (var w = 0; w < 6; w++) {
-
 
144
            var $tr = $('<tr></tr>');
-
 
145
            for (var d = 0; d < 7; d++) {
-
 
146
                var cell = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + w * 7 + d);
-
 
147
                var dateStr = fmtDate(cell);
-
 
148
                var isOtherMonth = cell.getMonth() !== m;
-
 
149
                var isToday = dateStr === today;
-
 
150
                var $td = $('<td></td>')
-
 
151
                    .attr('data-date', dateStr)
-
 
152
                    .append('<div class="nc-day-num">' + cell.getDate() + '</div>')
-
 
153
                    .append('<div class="nc-events"></div>');
-
 
154
                if (isOtherMonth) $td.addClass('nc-other-month');
-
 
155
                if (isToday) $td.addClass('nc-today');
-
 
156
                $tr.append($td);
-
 
157
            }
-
 
158
            $body.append($tr);
-
 
159
        }
-
 
160
 
-
 
161
        // Fetch events for the visible range and inject into cells
-
 
162
        $.getJSON(context + '/notificationCalendarData?from=' + from + '&to=' + to, function (data) {
-
 
163
            var events = data || [];
-
 
164
            events.forEach(function (e) {
-
 
165
                if (!e.start) return;
-
 
166
                var startDate = new Date(e.start);
-
 
167
                var endDate = e.end ? new Date(e.end) : startDate;
-
 
168
                // Iterate every day the event covers (windows may span days)
-
 
169
                var cursor = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
-
 
170
                var last = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
-
 
171
                while (cursor <= last) {
-
 
172
                    var key = fmtDate(cursor);
-
 
173
                    if (!eventsByDate[key]) eventsByDate[key] = [];
-
 
174
                    eventsByDate[key].push(e);
-
 
175
                    cursor.setDate(cursor.getDate() + 1);
-
 
176
                }
-
 
177
            });
-
 
178
 
-
 
179
            Object.keys(eventsByDate).forEach(function (dateStr) {
-
 
180
                var $cell = $body.find('td[data-date="' + dateStr + '"] .nc-events');
-
 
181
                if ($cell.length === 0) return;
-
 
182
                var dayEvents = eventsByDate[dateStr];
-
 
183
                var shown = dayEvents.slice(0, 4);
-
 
184
                shown.forEach(function (e) {
-
 
185
                    var $ev = $('<div class="nc-event"></div>')
-
 
186
                        .text(labelForEvent(e))
-
 
187
                        .css('background', e.color || '#3a87ad')
-
 
188
                        .data('event', e);
-
 
189
                    $cell.append($ev);
-
 
190
                });
-
 
191
                if (dayEvents.length > shown.length) {
-
 
192
                    $cell.append('<div class="nc-more">+' + (dayEvents.length - shown.length) + ' more</div>');
-
 
193
                }
-
 
194
            });
-
 
195
        });
-
 
196
    }
-
 
197
 
-
 
198
    function labelForEvent(e) {
-
 
199
        var t = e.start ? formatHM(new Date(e.start)) : '';
-
 
200
        return (t ? t + ' ' : '') + (e.title || '');
-
 
201
    }
-
 
202
 
-
 
203
    function fmtDate(d) {
-
 
204
        var pad = function (n) {
-
 
205
            return String(n).padStart(2, '0');
-
 
206
        };
-
 
207
        return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
-
 
208
    }
-
 
209
 
-
 
210
    function formatHM(d) {
-
 
211
        var pad = function (n) {
-
 
212
            return String(n).padStart(2, '0');
-
 
213
        };
-
 
214
        return pad(d.getHours()) + ':' + pad(d.getMinutes());
-
 
215
    }
-
 
216
 
-
 
217
    // ─── Upcoming Scheduled Notifications list ───────────────────────────
-
 
218
    var CATEGORY_COLOR = {
-
 
219
        'ANNOUNCE': '#3a87ad', 'NOTIFICATION': '#4b5563',
-
 
220
        'REMINDER': '#f0ad4e', 'ALERT': '#d9534f'
-
 
221
    };
-
 
222
 
-
 
223
    function categoryBadge(cat) {
-
 
224
        if (!cat) return '<span style="color:#9ca3af;">—</span>';
-
 
225
        var color = CATEGORY_COLOR[cat] || '#6b7280';
-
 
226
        return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;'
-
 
227
            + 'background:' + color + '20;color:' + color + ';font-size:11px;font-weight:600;">'
-
 
228
            + cat + '</span>';
-
 
229
    }
-
 
230
 
-
 
231
    function statusBadge(st) {
-
 
232
        var bg = '#eef2ff', fg = '#4338ca';
-
 
233
        if (st === 'DISPATCHED') {
-
 
234
            bg = '#dcfce7';
-
 
235
            fg = '#166534';
-
 
236
        } else if (st === 'CANCELLED') {
-
 
237
            bg = '#f3f4f6';
-
 
238
            fg = '#6b7280';
-
 
239
        } else if (st === 'FAILED') {
-
 
240
            bg = '#fee2e2';
-
 
241
            fg = '#991b1b';
-
 
242
        }
-
 
243
        return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;'
-
 
244
            + 'background:' + bg + ';color:' + fg + ';font-size:11px;font-weight:600;">'
-
 
245
            + st + '</span>';
-
 
246
    }
-
 
247
 
-
 
248
    function escapeHtml(s) {
-
 
249
        if (s == null) return '';
-
 
250
        return String(s)
-
 
251
            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
-
 
252
            .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
-
 
253
    }
-
 
254
 
-
 
255
    function fmtWhen(iso) {
-
 
256
        if (!iso) return '';
-
 
257
        var d = new Date(iso);
-
 
258
        var pad = function (n) {
-
 
259
            return String(n).padStart(2, '0');
-
 
260
        };
-
 
261
        return pad(d.getDate()) + '/' + pad(d.getMonth() + 1) + ' ' + formatHM(d);
-
 
262
    }
-
 
263
 
-
 
264
    function renderScheduledList() {
-
 
265
        var days = $('#nslDays').val() || '30';
-
 
266
        var $body = $('#nslBody');
-
 
267
        $body.html('<tr><td colspan="7" style="text-align:center;color:#9ca3af;padding:24px;">Loading…</td></tr>');
-
 
268
        $.getJSON(context + '/notificationScheduleList?days=' + encodeURIComponent(days), function (rows) {
-
 
269
            if (!rows || rows.length === 0) {
-
 
270
                $body.html('<tr><td colspan="7" style="text-align:center;color:#9ca3af;padding:24px;">'
-
 
271
                    + 'No scheduled notifications in the next ' + days + ' days.</td></tr>');
-
 
272
                return;
-
 
273
            }
-
 
274
            var html = '';
-
 
275
            rows.forEach(function (r) {
-
 
276
                var titleLine = r.title ? '<div style="font-weight:600;color:#111;">' + escapeHtml(r.title) + '</div>' : '';
-
 
277
                var msgLine = r.message ? '<div style="color:#6b7280;font-size:12px;max-width:420px;'
-
 
278
                    + 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(r.message) + '</div>' : '';
-
 
279
                var thumb = r.imageUrl ? '<img src="' + escapeHtml(r.imageUrl) + '" '
-
 
280
                    + 'style="width:36px;height:36px;object-fit:cover;border-radius:4px;margin-right:8px;vertical-align:middle;" />' : '';
-
 
281
                var content = '<div style="display:flex;align-items:center;">' + thumb
-
 
282
                    + '<div>' + titleLine + msgLine + '</div></div>';
-
 
283
                var groupCell = r.groupName
-
 
284
                    ? '<span style="display:inline-block;padding:2px 8px;border-radius:10px;background:#eef2ff;color:#4338ca;font-size:11px;font-weight:600;">'
-
 
285
                    + escapeHtml(r.groupName) + '</span>'
-
 
286
                    : '<span style="color:#9ca3af;">—</span>';
-
 
287
                var audCount = (r.audienceCount != null ? r.audienceCount : 0);
-
 
288
                var audSummary = r.audienceSummary || '';
-
 
289
                var audienceCell = '<div style="font-weight:600;">' + audCount.toLocaleString() + '</div>'
-
 
290
                    + '<div style="font-size:11px;color:#6b7280;">' + escapeHtml(audSummary) + '</div>';
-
 
291
                var canCancel = r.status === 'SCHEDULED';
-
 
292
                var actions = '<button type="button" class="nm-btn nm-btn-outline cdm-view-btn" '
-
 
293
                    + 'data-campaign-id="' + r.campaignId + '" style="margin-right:4px;">View</button>'
-
 
294
                    + '<button type="button" class="nm-btn nm-btn-outline nsl-add-btn" '
-
 
295
                    + 'data-campaign-id="' + r.campaignId + '" style="margin-right:4px;">+ Add date</button>'
-
 
296
                    + '<button type="button" class="nm-btn nm-btn-outline nsl-cancel-btn" '
-
 
297
                    + 'data-schedule-id="' + r.scheduleId + '" '
-
 
298
                    + (canCancel ? '' : 'disabled ')
-
 
299
                    + 'style="color:#dc2626;border-color:#fecaca;">Cancel</button>';
-
 
300
                html += '<tr>'
-
 
301
                    + '<td style="white-space:nowrap;font-weight:600;">' + fmtWhen(r.when) + '</td>'
-
 
302
                    + '<td>' + groupCell + '</td>'
-
 
303
                    + '<td>' + content + '</td>'
-
 
304
                    + '<td>' + categoryBadge(r.category) + '</td>'
-
 
305
                    + '<td style="text-align:right;">' + audienceCell + '</td>'
-
 
306
                    + '<td>' + statusBadge(r.status) + '</td>'
-
 
307
                    + '<td style="text-align:right;white-space:nowrap;">' + actions + '</td>'
-
 
308
                    + '</tr>';
-
 
309
            });
-
 
310
            $body.html(html);
-
 
311
        }).fail(function () {
-
 
312
            $body.html('<tr><td colspan="7" style="text-align:center;color:#dc2626;padding:24px;">'
-
 
313
                + 'Failed to load — check the server log.</td></tr>');
-
 
314
        });
-
 
315
    }
-
 
316
 
-
 
317
    $(document).on('change', '#nslDays', renderScheduledList);
-
 
318
    $(document).on('click', '#nslRefresh', renderScheduledList);
-
 
319
    $(document).on('click', '.nsl-cancel-btn', function () {
-
 
320
        var scheduleId = $(this).data('schedule-id');
-
 
321
        if (!scheduleId) return;
-
 
322
        if (!confirm('Cancel this scheduled notification?')) return;
-
 
323
        doPostAjaxRequestWithJsonHandler(
-
 
324
            context + '/notificationSchedule/cancel?scheduleId=' + encodeURIComponent(scheduleId),
-
 
325
            '{}',
-
 
326
            function (response) {
-
 
327
                if (response === 'true' || response === true) {
-
 
328
                    renderScheduledList();
-
 
329
                    renderCalendarMonth();
-
 
330
                } else {
-
 
331
                    alert('Could not cancel — the schedule may already be dispatched or cancelled.');
-
 
332
                }
-
 
333
            });
-
 
334
    });
-
 
335
    $(document).on('click', '.nsl-add-btn', function () {
-
 
336
        var campaignId = $(this).data('campaign-id');
-
 
337
        if (!campaignId) return;
-
 
338
        var dateVal = prompt('Add a schedule — enter date (YYYY-MM-DD):');
-
 
339
        if (!dateVal) return;
-
 
340
        var timeVal = prompt('Enter time (HH:MM, 24-hour):');
-
 
341
        if (!timeVal) return;
-
 
342
        var scheduledAt = dateVal + 'T' + timeVal + ':00';
-
 
343
        if (new Date(scheduledAt) <= new Date()) {
-
 
344
            alert('Pick a future date/time.');
-
 
345
            return;
-
 
346
        }
-
 
347
        doPostAjaxRequestWithJsonHandler(
-
 
348
            context + '/notificationSchedule/add?campaignId=' + encodeURIComponent(campaignId)
-
 
349
            + '&scheduledAt=' + encodeURIComponent(scheduledAt),
-
 
350
            '{}',
-
 
351
            function (response) {
-
 
352
                if (response === 'false' || response === false) {
-
 
353
                    alert('Could not add — check date/time and try again.');
-
 
354
                    return;
-
 
355
                }
-
 
356
                renderScheduledList();
-
 
357
                renderCalendarMonth();
-
 
358
                renderRecentList();
-
 
359
            });
-
 
360
    });
-
 
361
 
-
 
362
    // ─── Recent Notifications list (all formats, all statuses) ───────────
-
 
363
    var FORMAT_COLOR = {
-
 
364
        'PUSH': {bg: '#dbeafe', fg: '#1e40af'},
-
 
365
        'IN_APP_POPUP': {bg: '#fef3c7', fg: '#92400e'},
-
 
366
        'STORY': {bg: '#fce7f3', fg: '#9d174d'}
-
 
367
    };
-
 
368
 
-
 
369
    function formatBadge(fmt) {
-
 
370
        var meta = FORMAT_COLOR[fmt] || {bg: '#f3f4f6', fg: '#6b7280'};
-
 
371
        var label = fmt === 'IN_APP_POPUP' ? 'POPUP' : (fmt || 'PUSH');
-
 
372
        return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;'
-
 
373
            + 'background:' + meta.bg + ';color:' + meta.fg + ';font-size:11px;font-weight:600;">'
-
 
374
            + label + '</span>';
-
 
375
    }
-
 
376
 
-
 
377
    function fmtCreated(iso) {
-
 
378
        if (!iso) return '';
-
 
379
        var d = new Date(iso);
-
 
380
        var pad = function (n) {
-
 
381
            return String(n).padStart(2, '0');
-
 
382
        };
-
 
383
        return pad(d.getDate()) + '/' + pad(d.getMonth() + 1) + '/' + d.getFullYear()
-
 
384
            + ' ' + formatHM(d);
-
 
385
    }
-
 
386
 
-
 
387
    function renderRecentList() {
-
 
388
        var days = $('#nrlDays').val() || '30';
-
 
389
        var $body = $('#nrlBody');
-
 
390
        $body.html('<tr><td colspan="8" style="text-align:center;color:#9ca3af;padding:24px;">Loading…</td></tr>');
-
 
391
        $.getJSON(context + '/notificationCampaignList?days=' + encodeURIComponent(days), function (rows) {
-
 
392
            if (!rows || rows.length === 0) {
-
 
393
                $body.html('<tr><td colspan="8" style="text-align:center;color:#9ca3af;padding:24px;">'
-
 
394
                    + 'No notifications created in the last ' + days + ' days.</td></tr>');
-
 
395
                return;
-
 
396
            }
-
 
397
            var html = '';
-
 
398
            rows.forEach(function (r) {
-
 
399
                var titleLine = r.title ? '<div style="font-weight:600;color:#111;">' + escapeHtml(r.title) + '</div>' : '';
-
 
400
                var msgLine = r.message ? '<div style="color:#6b7280;font-size:12px;max-width:420px;'
-
 
401
                    + 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(r.message) + '</div>' : '';
-
 
402
                var thumb = r.imageUrl ? '<img src="' + escapeHtml(r.imageUrl) + '" '
-
 
403
                    + 'style="width:36px;height:36px;object-fit:cover;border-radius:4px;margin-right:8px;vertical-align:middle;" />' : '';
-
 
404
                var content = '<div style="display:flex;align-items:center;">' + thumb
-
 
405
                    + '<div>' + titleLine + msgLine + '</div></div>';
-
 
406
                var groupCell = r.groupName
-
 
407
                    ? '<span style="display:inline-block;padding:2px 8px;border-radius:10px;background:#eef2ff;color:#4338ca;font-size:11px;font-weight:600;">'
-
 
408
                    + escapeHtml(r.groupName) + '</span>'
-
 
409
                    : '<span style="color:#9ca3af;">—</span>';
-
 
410
                var audCount = (r.audienceCount != null ? r.audienceCount : 0);
-
 
411
                var audienceCell = '<div style="font-weight:600;">' + audCount.toLocaleString() + '</div>'
-
 
412
                    + '<div style="font-size:11px;color:#6b7280;">' + escapeHtml(r.audienceSummary || '') + '</div>';
-
 
413
                var viewBtn = '<button type="button" class="nm-btn nm-btn-outline cdm-view-btn" '
-
 
414
                    + 'data-campaign-id="' + r.campaignId + '" style="padding:2px 8px;font-size:11px;">View</button>';
-
 
415
                html += '<tr>'
-
 
416
                    + '<td style="white-space:nowrap;color:#6b7280;">' + fmtCreated(r.createdAt) + '</td>'
-
 
417
                    + '<td>' + formatBadge(r.format) + '</td>'
-
 
418
                    + '<td>' + groupCell + '</td>'
-
 
419
                    + '<td>' + content + '</td>'
-
 
420
                    + '<td>' + categoryBadge(r.category) + '</td>'
-
 
421
                    + '<td style="font-size:12px;color:#374151;">' + escapeHtml(r.delivery || '—') + '</td>'
-
 
422
                    + '<td style="text-align:right;">' + audienceCell + '</td>'
-
 
423
                    + '<td style="text-align:right;">' + viewBtn + '</td>'
-
 
424
                    + '</tr>';
-
 
425
            });
-
 
426
            $body.html(html);
-
 
427
        }).fail(function () {
-
 
428
            $body.html('<tr><td colspan="8" style="text-align:center;color:#dc2626;padding:24px;">'
-
 
429
                + 'Failed to load — check the server log.</td></tr>');
-
 
430
        });
-
 
431
    }
-
 
432
 
-
 
433
    $(document).on('change', '#nrlDays', renderRecentList);
-
 
434
    $(document).on('click', '#nrlRefresh', renderRecentList);
-
 
435
 
-
 
436
    // ─── Full campaign detail modal (content + audience list + schedules) ─
-
 
437
    function scheduleStatusBadge(st) {
-
 
438
        var map = {
-
 
439
            'SCHEDULED': {bg: '#eef2ff', fg: '#4338ca'},
-
 
440
            'DISPATCHED': {bg: '#dcfce7', fg: '#166534'},
-
 
441
            'CANCELLED': {bg: '#f3f4f6', fg: '#6b7280'},
-
 
442
            'FAILED': {bg: '#fee2e2', fg: '#991b1b'}
-
 
443
        };
-
 
444
        var meta = map[st] || {bg: '#f3f4f6', fg: '#6b7280'};
-
 
445
        return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;'
-
 
446
            + 'background:' + meta.bg + ';color:' + meta.fg + ';font-size:11px;font-weight:600;">'
-
 
447
            + st + '</span>';
-
 
448
    }
-
 
449
 
-
 
450
    function contentFieldRow(label, value) {
-
 
451
        if (value == null || value === '') return '';
-
 
452
        return '<tr><td style="padding:4px 8px;color:#6b7280;width:140px;">' + escapeHtml(label) + '</td>'
-
 
453
            + '<td style="padding:4px 8px;color:#111;">' + escapeHtml(value) + '</td></tr>';
-
 
454
    }
-
 
455
 
-
 
456
    function renderCampaignDetail(campaignId) {
-
 
457
        var $body = $('#cdmBody');
-
 
458
        $('#cdmTitle').text('Campaign detail');
-
 
459
        $body.html('<div style="text-align:center;color:#9ca3af;padding:24px;">Loading…</div>');
-
 
460
        $('#campaignDetailModal').modal('show');
-
 
461
 
-
 
462
        $.getJSON(context + '/notificationCampaignDetail?campaignId=' + encodeURIComponent(campaignId), function (data) {
-
 
463
            if (!data || !data.campaign) {
-
 
464
                $body.html('<div style="text-align:center;color:#dc2626;padding:24px;">Campaign not found.</div>');
-
 
465
                return;
-
 
466
            }
-
 
467
            var c = data.campaign;
-
 
468
            var content = c.content || {};
-
 
469
            $('#cdmTitle').text(c.name || 'Campaign #' + c.id);
-
 
470
 
-
 
471
            // ── Section: header info
-
 
472
            var headerHtml = '<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;">'
-
 
473
                + formatBadge(c.format)
-
 
474
                + categoryBadge(c.category)
-
 
475
                + (c.groupName
-
 
476
                    ? '<span style="display:inline-block;padding:2px 8px;border-radius:10px;background:#eef2ff;color:#4338ca;font-size:11px;font-weight:600;">'
-
 
477
                    + escapeHtml(c.groupName) + '</span>'
-
 
478
                    : '')
-
 
479
                + '<span style="color:#6b7280;font-size:12px;padding:2px 0;">Created: '
-
 
480
                + escapeHtml(fmtCreated(c.createdAt)) + '</span>'
-
 
481
                + '</div>';
-
 
482
 
-
 
483
            // ── Section: content
-
 
484
            var img = content.imageUrl
-
 
485
                ? '<div style="margin-bottom:8px;"><img src="' + escapeHtml(content.imageUrl)
-
 
486
                + '" style="max-width:200px;max-height:120px;border-radius:6px;border:1px solid #e5e7eb;" /></div>'
-
 
487
                : '';
-
 
488
            var contentTable = '<table style="width:100%;font-size:13px;border-collapse:collapse;">'
-
 
489
                + contentFieldRow('Title', content.title)
-
 
490
                + contentFieldRow('Message', content.message)
-
 
491
                + contentFieldRow('Button Label', content.buttonLabel)
-
 
492
                + contentFieldRow('URL', content.url)
-
 
493
                + contentFieldRow('Show CTA', content.showCta === true || content.showCta === 'true' ? 'Yes' : (content.showCta === false ? 'No' : null))
-
 
494
                + contentFieldRow('Show Close', content.showClose === true || content.showClose === 'true' ? 'Yes' : (content.showClose === false ? 'No' : null))
-
 
495
                + contentFieldRow('Show On Page', content.showOnPage)
-
 
496
                + contentFieldRow('Auto Close (s)', content.autoCloseSeconds)
-
 
497
                + contentFieldRow('Frequency Cap', content.frequencyCap)
-
 
498
                + contentFieldRow('Live From', content.liveFrom ? fmtCreated(content.liveFrom) : null)
-
 
499
                + contentFieldRow('Live Until', content.liveUntil ? fmtCreated(content.liveUntil) : null)
-
 
500
                + '</table>';
-
 
501
            var contentSection = '<div style="margin-bottom:20px;padding:12px;background:#f9fafb;border-radius:6px;">'
-
 
502
                + '<b style="color:#111;font-size:13px;">Content</b><div style="margin-top:8px;">'
-
 
503
                + img + contentTable + '</div></div>';
-
 
504
 
-
 
505
            // ── Section: audience
-
 
506
            var aud = data.audience || {};
-
 
507
            var users = aud.sampleUsers || [];
-
 
508
            var userRows = '';
-
 
509
            if (users.length === 0) {
-
 
510
                userRows = '<tr><td colspan="4" style="text-align:center;color:#9ca3af;padding:12px;">No users resolved.</td></tr>';
-
 
511
            } else {
-
 
512
                users.forEach(function (u) {
-
 
513
                    userRows += '<tr>'
-
 
514
                        + '<td>' + escapeHtml(u.name || '—') + '</td>'
-
 
515
                        + '<td>' + escapeHtml(u.mobile || '—') + '</td>'
-
 
516
                        + '<td>' + escapeHtml(u.city || '—') + '</td>'
-
 
517
                        + '<td>' + escapeHtml(u.state || '—') + '</td>'
-
 
518
                        + '</tr>';
-
 
519
                });
-
 
520
            }
-
 
521
            var audienceHeader = '<b style="color:#111;font-size:13px;">Audience</b>'
-
 
522
                + ' <span style="color:#111;font-weight:600;">' + (aud.totalCount || 0).toLocaleString() + '</span>'
-
 
523
                + ' <span style="color:#6b7280;font-size:12px;">(' + escapeHtml(aud.summary || '') + ')</span>'
-
 
524
                + (aud.truncated ? ' <span style="color:#f59e0b;font-size:11px;">— showing first ' + users.length + '</span>' : '');
-
 
525
            var audienceSection = '<div style="margin-bottom:20px;padding:12px;background:#f9fafb;border-radius:6px;">'
-
 
526
                + audienceHeader
-
 
527
                + '<div style="margin-top:8px;max-height:300px;overflow-y:auto;">'
-
 
528
                + '<table class="table table-striped" style="margin:0;font-size:12px;">'
-
 
529
                + '<thead><tr><th>Name</th><th>Mobile</th><th>City</th><th>State</th></tr></thead>'
-
 
530
                + '<tbody>' + userRows + '</tbody>'
-
 
531
                + '</table></div></div>';
-
 
532
 
-
 
533
            // ── Section: schedules
-
 
534
            var scheds = data.schedules || [];
-
 
535
            var schedRows = '';
-
 
536
            if (scheds.length === 0) {
-
 
537
                schedRows = '<tr><td colspan="4" style="text-align:center;color:#9ca3af;padding:12px;">No schedules — sent immediately.</td></tr>';
-
 
538
            } else {
-
 
539
                scheds.forEach(function (s) {
-
 
540
                    schedRows += '<tr>'
-
 
541
                        + '<td>' + escapeHtml(fmtCreated(s.scheduledAt)) + '</td>'
-
 
542
                        + '<td>' + scheduleStatusBadge(s.status) + '</td>'
-
 
543
                        + '<td>' + (s.dispatchedAt ? escapeHtml(fmtCreated(s.dispatchedAt)) : '—') + '</td>'
-
 
544
                        + '<td style="color:#6b7280;">#' + s.id + '</td>'
-
 
545
                        + '</tr>';
-
 
546
                });
-
 
547
            }
-
 
548
            var schedulesSection = '<div style="padding:12px;background:#f9fafb;border-radius:6px;">'
-
 
549
                + '<b style="color:#111;font-size:13px;">Schedules (' + scheds.length + ')</b>'
-
 
550
                + '<div style="margin-top:8px;max-height:250px;overflow-y:auto;">'
-
 
551
                + '<table class="table table-striped" style="margin:0;font-size:12px;">'
-
 
552
                + '<thead><tr><th>Scheduled At</th><th>Status</th><th>Dispatched At</th><th>ID</th></tr></thead>'
-
 
553
                + '<tbody>' + schedRows + '</tbody>'
-
 
554
                + '</table></div></div>';
-
 
555
 
-
 
556
            $body.html(headerHtml + contentSection + audienceSection + schedulesSection);
-
 
557
        }).fail(function () {
-
 
558
            $body.html('<div style="text-align:center;color:#dc2626;padding:24px;">Failed to load — check the server log.</div>');
-
 
559
        });
-
 
560
    }
-
 
561
 
-
 
562
    $(document).on('click', '.cdm-view-btn', function () {
-
 
563
        var campaignId = $(this).data('campaign-id');
-
 
564
        if (campaignId) renderCampaignDetail(campaignId);
-
 
565
    });
-
 
566
 
-
 
567
    // Event click → detail modal (same modal FullCalendar was using)
-
 
568
    $(document).on('click', '.nc-event', function () {
-
 
569
        var event = $(this).data('event');
-
 
570
        if (!event) return;
-
 
571
        var kind = event.kind || 'push-schedule';
-
 
572
        $('#ncDetailCampaign').text(event.title);
-
 
573
        var startD = new Date(event.start);
-
 
574
        var whenStr = fmtDateHuman(startD);
-
 
575
        if (event.end && (kind === 'popup-window' || kind === 'story-window')) {
-
 
576
            whenStr += ' → ' + fmtDateHuman(new Date(event.end));
-
 
577
        }
-
 
578
        $('#ncDetailWhen').text(whenStr);
-
 
579
        $('#ncDetailStatus').text(event.status || event.category || '—');
-
 
580
        $('#ncDetailScheduleId').text(event.id);
-
 
581
 
-
 
582
        // Reset "add another schedule" section every time the modal opens
-
 
583
        $('#ncAddScheduleBox').hide();
-
 
584
        $('#ncAddErr').hide().text('');
-
 
585
        $('#ncAddDate').val('');
-
 
586
        $('#ncAddTime').val('');
-
 
587
        var $addBtn = $('#ncAddScheduleBtn').off('click').hide();
-
 
588
        // Only PUSH schedules can be duplicated — popup/story are windowed, not repeatable
-
 
589
        if (kind === 'push-schedule' && event.campaignId) {
-
 
590
            $addBtn.show().on('click', function () {
-
 
591
                $('#ncAddScheduleBox').toggle();
-
 
592
                if ($('#ncAddScheduleBox').is(':visible') && !$('#ncAddDate').val()) {
-
 
593
                    var d = new Date();
-
 
594
                    $('#ncAddDate').val(fmtDate(d));
-
 
595
                    $('#ncAddTime').val(formatHM(d));
-
 
596
                }
-
 
597
            });
-
 
598
            $('#ncAddSaveBtn').off('click').on('click', function () {
-
 
599
                var dateVal = $('#ncAddDate').val();
-
 
600
                var timeVal = $('#ncAddTime').val();
-
 
601
                if (!dateVal || !timeVal) {
-
 
602
                    $('#ncAddErr').text('Pick both a date and a time.').show();
-
 
603
                    return;
-
 
604
                }
-
 
605
                // Backend parses LocalDateTime — send "yyyy-MM-ddTHH:mm:00"
-
 
606
                var scheduledAt = dateVal + 'T' + timeVal + ':00';
-
 
607
                if (new Date(scheduledAt) <= new Date()) {
-
 
608
                    $('#ncAddErr').text('Pick a future date/time.').show();
-
 
609
                    return;
-
 
610
                }
-
 
611
                $('#ncAddErr').hide();
-
 
612
                doPostAjaxRequestWithJsonHandler(
-
 
613
                    context + '/notificationSchedule/add?campaignId=' + encodeURIComponent(event.campaignId)
-
 
614
                    + '&scheduledAt=' + encodeURIComponent(scheduledAt),
-
 
615
                    '{}',
-
 
616
                    function (response) {
-
 
617
                        // response is either the new schedule id (number) or 'false'
-
 
618
                        if (response === 'false' || response === false) {
-
 
619
                            $('#ncAddErr').text('Could not add — check date/time and try again.').show();
67
							return;
620
							return;
68
						}
621
						}
69
						if (type === "") {
622
                        $('#scheduleDetailModal').modal('hide');
70
							alert("type is required");
623
                        renderCalendarMonth();
71
							return;
624
                        renderScheduledList();
72
						}
625
                        renderRecentList();
73
						if (type === "url") {
626
                    });
74
							if (url === "") {
627
            });
75
								alert("url is required");
628
        }
76
								return;
629
 
77
							}
630
        var $btn = $('#ncCancelBtn').off('click');
78
 
631
        if (kind === 'push-schedule') {
79
						}
632
            if (event.status !== 'SCHEDULED') {
80
 
633
                $btn.prop('disabled', true).text('Not cancellable');
81
						var notificationData = {};
634
            } else {
82
						
635
                $btn.prop('disabled', false).text('Cancel Schedule').on('click', function () {
83
						notificationData['stateIds'] = $('#state-users').val();
636
                    if (!confirm('Cancel this scheduled notification?')) return;
84
						
637
                    doPostAjaxRequestWithJsonHandler(
85
						notificationData['userIds'] = $('#fofo-users').val()
638
                        context + '/notificationSchedule/cancel?scheduleId=' + encodeURIComponent(event.id),
86
								.concat($('#internal-users').val());
639
                        '{}',
87
						notificationData['campaignName'] = $(
640
                        function (response) {
88
								'input[name="campaignName"]').val();
641
                            $('#scheduleDetailModal').modal('hide');
89
						notificationData['title'] = $('input[name="title"]')
642
                            if (response === 'true' || response === true) {
90
								.val();
643
                                renderCalendarMonth();
91
						notificationData['message'] = $('#message').val();
644
                                renderScheduledList();
92
						notificationData['imageUrl'] = $(
645
                                renderRecentList();
93
								'input[name="imageUrl"]').val();
646
                            } else {
94
						notificationData['type'] = $('#type').val();
647
                                alert('Could not cancel — the schedule may already be dispatched or cancelled.');
95
						notificationData['url'] = url;
648
                            }
96
						notificationData['expiresat'] = getDatesFromPicker('input[name="dateTime"]').startDate;
649
                        });
97
						notificationData['showImage'] = $(
650
                });
98
							'input[name="showImage"]').val();
651
            }
99
						notificationData['documentId'] = localStorage
652
        } else {
100
								.getItem("notifyDocument");
653
            var alreadyEnded = event.end && new Date(event.end) < new Date();
101
						notificationData['messageType'] = $('#messageType')
654
            if (alreadyEnded) {
102
								.val();
655
                $btn.prop('disabled', true).text('Already ended');
103
						console.log(notificationData)
656
            } else {
104
 
657
                $btn.prop('disabled', false).text('Deactivate now').on('click', function () {
105
						if (confirm("Are you sure you want to send notification!") == true) {
658
                    var noun = kind === 'popup-window' ? 'pop-up' : 'story';
106
							doPostAjaxRequestWithJsonHandler(context
659
                    if (!confirm('Deactivate this ' + noun + ' immediately? Partners will stop seeing it.')) return;
107
									+ "/sendNotification", JSON
660
                    doPostAjaxRequestWithJsonHandler(
108
									.stringify(notificationData), function(
661
                        context + '/notificationCampaign/deactivate?campaignId=' + encodeURIComponent(event.campaignId),
109
									response) {
662
                        '{}',
110
								if (response == 'true') {
663
                        function (response) {
111
									alert("successfully send");
664
                            $('#scheduleDetailModal').modal('hide');
112
									loadNotificationPanel("main-content");
665
                            if (response === 'true' || response === true) {
113
									localStorage.removeItem("notifyDocument");
666
                                renderCalendarMonth();
114
								}
667
                                renderScheduledList();
115
							});
668
                                renderRecentList();
116
 
669
                            } else {
117
							return false;
670
                                alert('Could not deactivate — the row may be missing or already ended.');
118
						}
671
                            }
119
 
672
                        });
120
					});
673
                });
121
 
674
            }
-
 
675
        }
-
 
676
        $('#scheduleDetailModal').modal('show');
-
 
677
    });
-
 
678
 
-
 
679
    function fmtDateHuman(d) {
-
 
680
        var pad = function (n) {
-
 
681
            return String(n).padStart(2, '0');
-
 
682
        };
-
 
683
        return pad(d.getDate()) + '/' + pad(d.getMonth() + 1) + '/' + d.getFullYear() + ' ' + formatHM(d);
-
 
684
    }
-
 
685
 
-
 
686
    // ─── Format cards ────────────────────────────────────────────────────
-
 
687
    var FORMAT_META = {
-
 
688
        'PUSH': {cta: 'Send now', detailsTitle: 'Notification details', label: 'Push notification'},
-
 
689
        'IN_APP_POPUP': {cta: 'Publish now', detailsTitle: 'Pop-up details', label: 'In-app pop-up'},
-
 
690
        'STORY': {cta: 'Publish now', detailsTitle: 'Story details', label: 'Story'}
-
 
691
    };
-
 
692
 
-
 
693
    // Per-format cache of title + message. Push, Popup and Story are meant for
-
 
694
    // different surfaces (tray vs modal vs story rail) with different constraints,
-
 
695
    // so their text should not bleed across formats when the admin switches.
-
 
696
    var contentByFormat = {
-
 
697
        PUSH: {title: '', message: ''},
-
 
698
        IN_APP_POPUP: {title: '', message: ''},
-
 
699
        STORY: {title: '', message: ''}
-
 
700
    };
-
 
701
 
-
 
702
    $(document).on('click', '.nm-format', function () {
-
 
703
        var fmt = $(this).data('format');
-
 
704
        var prevFmt = activeFormat();
-
 
705
        // Snapshot outgoing format's text before we swap
-
 
706
        if (prevFmt && contentByFormat[prevFmt]) {
-
 
707
            contentByFormat[prevFmt].title = $('#title').val();
-
 
708
            contentByFormat[prevFmt].message = $('#message').val();
-
 
709
        }
-
 
710
 
-
 
711
        $('.nm-format').removeClass('active');
-
 
712
        $(this).addClass('active');
-
 
713
        $('.nm-panel').removeClass('active').hide();
-
 
714
        $('.nm-panel[data-panel="' + fmt + '"]').addClass('active').show();
-
 
715
        $('.nm-preview').removeClass('active').hide();
-
 
716
        $('.nm-preview[data-preview="' + fmt + '"]').addClass('active').show();
-
 
717
 
-
 
718
        // Restore incoming format's cached text (empty if the admin never typed
-
 
719
        // anything for this format yet). `trigger('input')` refreshes the char-count
-
 
720
        // and the live preview widgets.
-
 
721
        var cached = contentByFormat[fmt] || {title: '', message: ''};
-
 
722
        $('#title').val(cached.title).trigger('input');
-
 
723
        $('#message').val(cached.message).trigger('input');
-
 
724
 
-
 
725
        var meta = FORMAT_META[fmt];
-
 
726
        $('#nmPrimaryCtaLabel').text(meta.cta);
-
 
727
        $('#nmDetailsTitle').text(meta.detailsTitle);
-
 
728
        $('#nmSummaryFormat').text(meta.label);
-
 
729
        refreshCategorySummary();
-
 
730
        refreshExpirySummary();
-
 
731
    });
-
 
732
    // Keep the cache in sync so switching mid-typing preserves the current text.
-
 
733
    $(document).on('input', '#title', function () {
-
 
734
        var f = activeFormat();
-
 
735
        if (contentByFormat[f]) {
-
 
736
            contentByFormat[f].title = $(this).val();
-
 
737
        }
-
 
738
    });
-
 
739
    $(document).on('input', '#message', function () {
-
 
740
        var f = activeFormat();
-
 
741
        if (contentByFormat[f]) {
-
 
742
            contentByFormat[f].message = $(this).val();
-
 
743
        }
-
 
744
    });
-
 
745
 
-
 
746
    function activeFormat() {
-
 
747
        return $('.nm-format.active').data('format') || 'PUSH';
-
 
748
    }
-
 
749
 
-
 
750
    function getActiveCategorySelect() {
-
 
751
        var fmt = activeFormat();
-
 
752
        if (fmt === 'IN_APP_POPUP') return $('#nmCategoryPopup');
-
 
753
        if (fmt === 'STORY') return $('#nmCategoryStory');
-
 
754
        return $('#nmCategory');
-
 
755
    }
-
 
756
 
-
 
757
    // ─── Live preview updates ────────────────────────────────────────────
-
 
758
    $(document).on('input', '#title', function () {
-
 
759
        var v = $(this).val();
-
 
760
        $('#titleCount').text(v.length);
-
 
761
        var fallback = 'Restock offer: up to 40% margin this week';
-
 
762
        $('#nmPreviewTitlePush,#nmPreviewTitlePopup,#nmPreviewTitleStory').text(v || fallback);
-
 
763
    });
-
 
764
    $(document).on('input', '#message', function () {
-
 
765
        var v = $(this).val();
-
 
766
        $('#messageCount').text(v.length);
-
 
767
        var fallback = 'Order now and earn extra margin on staples. Free logistics on orders above ₹5,000. Tap to view the catalogue.';
-
 
768
        $('#nmPreviewBodyPush,#nmPreviewBodyPopup,#nmPreviewBodyStory').text(v || fallback);
-
 
769
    });
-
 
770
    $(document).on('input', '#buttonLabelPopup', function () {
-
 
771
        $('#nmPreviewCtaPopup').text($(this).val() || 'View offer');
-
 
772
    });
-
 
773
    $(document).on('input', '#buttonLabelStory', function () {
-
 
774
        $('#nmPreviewCtaStory').text(($(this).val() || 'View offer') + ' ↑');
-
 
775
    });
-
 
776
    $(document).on('change', '#showClose', function () {
-
 
777
        $('#nmPopupPreviewClose').toggle($(this).is(':checked'));
-
 
778
    });
-
 
779
    $(document).on('change', '#showCta', function () {
-
 
780
        $('#nmPreviewCtaPopup').toggle($(this).is(':checked'));
-
 
781
    });
-
 
782
    $(document).on('change', '#expiresAfterPolicy', function () {
-
 
783
        $('#storyExpiryOnDateField').toggle($(this).val() === 'ON_DATE');
-
 
784
        refreshExpirySummary();
-
 
785
    });
-
 
786
 
-
 
787
    // ─── Summary bindings ───────────────────────────────────────────────
-
 
788
    $(document).on('input', '#campaignName', function () {
-
 
789
        $('#nmSummaryCampaign').text($(this).val() || '—');
-
 
790
    });
-
 
791
    $(document).on('change', '#nmCategory,#nmCategoryPopup,#nmCategoryStory', refreshCategorySummary);
-
 
792
    $(document).on('change', '#expireSat,#popupLiveUntil,#storyExpiryOnDate,#storyLiveFrom,#popupLiveFrom', refreshExpirySummary);
-
 
793
    $(document).on('change', '#selectedUrl', function () {
-
 
794
        var v = $(this).val();
-
 
795
        $('#nmSummaryTap').text((v && v !== 'other') ? v : '—');
-
 
796
    });
-
 
797
    $(document).on('input', '#url', function () {
-
 
798
        if ($('#selectedUrl').val() === 'other') {
-
 
799
            $('#nmSummaryTap').text($(this).val() || '—');
-
 
800
        }
-
 
801
    });
-
 
802
    $(document).on('change', '#fofo-users, #state-users, #internal-users', updateRecipientSummary);
-
 
803
 
-
 
804
    function refreshCategorySummary() {
-
 
805
        $('#nmSummaryCategory').text(getActiveCategorySelect().find('option:selected').text() || 'Not set');
-
 
806
    }
-
 
807
 
-
 
808
    function refreshExpirySummary() {
-
 
809
        var v;
-
 
810
        var fmt = activeFormat();
-
 
811
        if (fmt === 'PUSH') {
-
 
812
            if ($('#nmHasExpiry').val() === 'no') {
-
 
813
                v = 'Never (lifetime)';
-
 
814
            } else {
-
 
815
                v = $('#expireSat').val();
-
 
816
            }
-
 
817
        } else if (fmt === 'IN_APP_POPUP') {
-
 
818
            v = $('#popupLiveUntil').val();
-
 
819
        } else if (fmt === 'STORY') {
-
 
820
            v = $('#expiresAfterPolicy').val() === 'ON_DATE'
-
 
821
                ? $('#storyExpiryOnDate').val()
-
 
822
                : '24 h from publish';
-
 
823
        }
-
 
824
        $('#nmSummaryExpires').text(v || '—');
-
 
825
    }
-
 
826
 
-
 
827
    // Show/hide expiry date input based on the lifetime toggle
-
 
828
    $(document).on('change', '#nmHasExpiry', function () {
-
 
829
        $('#nmExpiryDateField').toggle($(this).val() === 'yes');
-
 
830
        if ($(this).val() === 'no') {
-
 
831
            $('#expireSat').val('');
-
 
832
        }
-
 
833
        refreshExpirySummary();
-
 
834
    });
-
 
835
 
-
 
836
    function updateRecipientSummary() {
-
 
837
        var partners = ($('#fofo-users').val() || []).length;
-
 
838
        var states = ($('#state-users').val() || []).length;
-
 
839
        var users = ($('#internal-users').val() || []).length;
-
 
840
        var total = partners + states + users;
-
 
841
        if (total === 0) {
-
 
842
            $('#nmSummaryRecipients').text('Not selected');
-
 
843
            return;
-
 
844
        }
-
 
845
        var parts = [];
-
 
846
        if (partners) {
-
 
847
            parts.push(partners + ' partner' + (partners > 1 ? 's' : ''));
-
 
848
        }
-
 
849
        if (states) {
-
 
850
            parts.push(states + ' state' + (states > 1 ? 's' : ''));
-
 
851
        }
-
 
852
        if (users) {
-
 
853
            parts.push(users + ' user' + (users > 1 ? 's' : ''));
-
 
854
        }
-
 
855
        $('#nmSummaryRecipients').text(parts.join(' · '));
-
 
856
    }
-
 
857
 
-
 
858
    // ─── Image dropzone ─────────────────────────────────────────────────
-
 
859
    $(document).on('click', '.nm-dropzone', function (e) {
-
 
860
        if ($(e.target).is('.nm-file-input')) {
-
 
861
            return;
-
 
862
        }
-
 
863
        $(this).find('.nm-file-input').trigger('click');
-
 
864
    });
-
 
865
    $(document).on('dragover', '.nm-dropzone', function (e) {
-
 
866
        e.preventDefault();
-
 
867
        $(this).addClass('nm-dropzone-hover');
-
 
868
    });
-
 
869
    $(document).on('dragleave dragend drop', '.nm-dropzone', function () {
-
 
870
        $(this).removeClass('nm-dropzone-hover');
-
 
871
    });
-
 
872
    $(document).on('drop', '.nm-dropzone', function (e) {
-
 
873
        e.preventDefault();
-
 
874
        handleFiles(this, e.originalEvent.dataTransfer.files);
-
 
875
    });
-
 
876
    $(document).on('change', '.nm-file-input', function () {
-
 
877
        handleFiles($(this).closest('.nm-dropzone')[0], this.files);
-
 
878
        this.value = '';   // allow re-selecting the same file
-
 
879
    });
-
 
880
    $(document).on('click', '.nm-thumb-x', function (e) {
-
 
881
        e.stopPropagation();
-
 
882
        var $list = $(this).closest('.nm-thumb-list');
-
 
883
        $(this).closest('.nm-thumb').remove();
-
 
884
        if ($list.data('target') === 'popupImage') {
-
 
885
            refreshPopupPreviewImage();
-
 
886
        }
-
 
887
    });
-
 
888
 
-
 
889
    // Reflect the uploaded/removed popup image in the live preview card.
-
 
890
    function refreshPopupPreviewImage() {
-
 
891
        var $firstImg = $('.nm-thumb-list[data-target="popupImage"] .nm-thumb img').first();
-
 
892
        var $preview = $('#nmPreviewImagePopup');
-
 
893
        if ($firstImg.length && $firstImg.attr('src')) {
-
 
894
            $preview.attr('src', $firstImg.attr('src')).show();
-
 
895
        } else {
-
 
896
            $preview.attr('src', '').hide();
-
 
897
        }
-
 
898
    }
-
 
899
 
-
 
900
    function handleFiles(dropzone, fileList) {
-
 
901
        if (!fileList || fileList.length === 0) {
-
 
902
            return;
-
 
903
        }
-
 
904
        var $dz = $(dropzone);
-
 
905
        var target = $dz.data('target');
-
 
906
        var multi = $dz.data('multi') === true;
-
 
907
        var $list = $('.nm-thumb-list[data-target="' + target + '"]');
-
 
908
        if (!multi) {
-
 
909
            $list.empty();
-
 
910
        }
-
 
911
        for (var i = 0; i < fileList.length; i++) {
-
 
912
            uploadOne(fileList[i], $list);
-
 
913
            if (!multi) {
-
 
914
                break;
-
 
915
            }
-
 
916
        }
-
 
917
    }
-
 
918
 
-
 
919
    function uploadOne(file, $list) {
-
 
920
        if (!file.type || file.type.indexOf('image/') !== 0) {
-
 
921
            alert('Only image files are accepted.');
-
 
922
            return;
-
 
923
        }
-
 
924
        if (file.size > 5 * 1024 * 1024) {
-
 
925
            alert('Image is larger than 5 MB — please compress and retry.');
-
 
926
            return;
-
 
927
        }
-
 
928
        var $thumb = $('<li class="nm-thumb"></li>');
-
 
929
        var $spinner = $('<div class="nm-thumb-spinner"><i class="fa fa-spinner fa-spin"></i></div>');
-
 
930
        $thumb.append($spinner);
-
 
931
        $list.append($thumb);
-
 
932
 
-
 
933
        // Local preview via FileReader
-
 
934
        var reader = new FileReader();
-
 
935
        reader.onload = function (e) {
-
 
936
            $thumb.prepend('<img src="' + e.target.result + '">');
-
 
937
            if ($list.data('target') === 'popupImage') {
-
 
938
                refreshPopupPreviewImage();
-
 
939
            }
-
 
940
        };
-
 
941
        reader.readAsDataURL(file);
-
 
942
 
-
 
943
        // Upload to server; store the returned URL/id on the thumb.
-
 
944
        doAjaxUploadRequestHandler(context + '/document-upload', 'POST', file, function (response) {
-
 
945
            var docId = response && response.response && response.response.document_id;
-
 
946
            if (!docId) {
-
 
947
                $thumb.remove();
-
 
948
                alert('Upload failed. Try again.');
-
 
949
                return;
-
 
950
            }
-
 
951
            var url = context + '/document/' + docId;
-
 
952
            $thumb.data('doc-id', docId).data('url', url);
-
 
953
            $spinner.remove();
-
 
954
            $thumb.append('<span class="nm-thumb-x">&times;</span>');
-
 
955
        });
-
 
956
    }
-
 
957
 
-
 
958
    // ─── Linked entities: schemes / offers / products ────────────────────
-
 
959
    // Cache keyed by URL (which encodes recipient filters). Type + same recipients → cache hit.
-
 
960
    var LINKABLE_CACHE = {};
-
 
961
 
-
 
962
    function buildLinkableUrl(type) {
-
 
963
        var endpoint = type === 'SCHEME' ? 'schemes' : 'offers';
-
 
964
        var fofoIds = $('#fofo-users').val() || [];
-
 
965
        var stateIds = $('#state-users').val() || [];
-
 
966
        var qs = [];
-
 
967
        if (fofoIds.length) {
-
 
968
            qs.push('fofoIds=' + fofoIds.join(','));
-
 
969
        }
-
 
970
        if (stateIds.length) {
-
 
971
            qs.push('stateIds=' + stateIds.join(','));
-
 
972
        }
-
 
973
        return context + '/notificationLinkables/' + endpoint + (qs.length ? '?' + qs.join('&') : '');
-
 
974
    }
-
 
975
 
-
 
976
    // If recipients change while a scheme/offer picker is open, refetch with the new filter.
-
 
977
    $(document).on('change', '#fofo-users, #state-users', function () {
-
 
978
        LINKABLE_CACHE = {};   // invalidate the whole cache — filters affect every fetch
-
 
979
        var type = $('#linkableType').val();
-
 
980
        if (type === 'SCHEME' || type === 'OFFER') {
-
 
981
            $('#linkableType').trigger('change');   // reload the picker with new filters
-
 
982
        }
-
 
983
    });
-
 
984
 
-
 
985
    $(document).on('change', '#linkableType', function () {
-
 
986
        var type = $(this).val();
-
 
987
        var $picker = $('#linkablePicker');
-
 
988
        var $searchWrap = $('#linkableSearchWrap');
-
 
989
        if (!type) {
-
 
990
            $picker.hide();
-
 
991
            $searchWrap.hide();
-
 
992
            return;
-
 
993
        }
-
 
994
        if (type === 'PRODUCT') {
-
 
995
            $picker.hide();
-
 
996
            $searchWrap.show();
-
 
997
            initLinkableProductTypeahead();
-
 
998
            $('#linkableSearchInput').val('').focus();
-
 
999
            return;
-
 
1000
        }
-
 
1001
        // SCHEME / OFFER — build the URL from the current recipient selection.
-
 
1002
        $searchWrap.hide();
-
 
1003
        $picker.show().html('<option value="">— Loading —</option>');
-
 
1004
        var url = buildLinkableUrl(type);
-
 
1005
        if (LINKABLE_CACHE[url]) {
-
 
1006
            renderLinkableOptions($picker, LINKABLE_CACHE[url]);
-
 
1007
        } else {
-
 
1008
            $.getJSON(url, function (data) {
-
 
1009
                var parsed = typeof data === 'string' ? JSON.parse(data) : data;
-
 
1010
                LINKABLE_CACHE[url] = parsed;
-
 
1011
                renderLinkableOptions($picker, parsed);
-
 
1012
            }).fail(function () {
-
 
1013
                $picker.html('<option value="">— Failed to load —</option>');
-
 
1014
            });
-
 
1015
        }
-
 
1016
    });
-
 
1017
 
-
 
1018
    function renderLinkableOptions($sel, items) {
-
 
1019
        var opts = '<option value="">— Pick one —</option>';
-
 
1020
        items.forEach(function (it) {
-
 
1021
            opts += '<option value="' + it.id + '" data-label="' + escAttr(it.label) + '" data-type="' + it.type + '">'
-
 
1022
                + escHtml(it.label) + '</option>';
-
 
1023
        });
-
 
1024
        $sel.html(opts);
-
 
1025
    }
-
 
1026
 
-
 
1027
    $(document).on('change', '#linkablePicker', function () {
-
 
1028
        var $opt = $(this).find('option:selected');
-
 
1029
        if (!$opt.val()) {
-
 
1030
            return;
-
 
1031
        }
-
 
1032
        addLinkedEntity($opt.data('type'), parseInt($opt.val(), 10), $opt.data('label'));
-
 
1033
        $(this).val('');
-
 
1034
    });
-
 
1035
 
-
 
1036
    // Product typeahead — Bootstrap 3 typeahead widget against Solr-backed /entity.
-
 
1037
    // Same widget the rest of the app uses (getItemAheadOptions pattern) so behaviour and
-
 
1038
    // dropdown positioning stay consistent.
-
 
1039
    function initLinkableProductTypeahead() {
-
 
1040
        var $input = $('#linkableSearchInput');
-
 
1041
        if (!$input.length || $input.data('nm-typeahead-init')) {
-
 
1042
            return;
-
 
1043
        }
-
 
1044
        $input.data('nm-typeahead-init', true);
-
 
1045
        $input.typeahead('destroy').typeahead({
-
 
1046
            source: function (q, process) {
-
 
1047
                if (q && q.length >= 2) {
-
 
1048
                    return $.ajax(context + '/entity', {
-
 
1049
                        global: false,
-
 
1050
                        data: {query: q, limit: 30, activeOnly: true},
-
 
1051
                        success: function (data) {
-
 
1052
                            var parsed = typeof data === 'string' ? JSON.parse(data) : data;
-
 
1053
                            process(parsed || []);
-
 
1054
                        }
-
 
1055
                    });
-
 
1056
                }
-
 
1057
            },
-
 
1058
            delay: 250,
-
 
1059
            items: 20,
-
 
1060
            minLength: 2,
-
 
1061
            displayText: function (item) {
-
 
1062
                return item.title_s || ('Catalog ' + item.catalogId_i);
-
 
1063
            },
-
 
1064
            afterSelect: function (item) {
-
 
1065
                var label = item.title_s || ('Catalog ' + item.catalogId_i);
-
 
1066
                addLinkedEntity('PRODUCT', item.catalogId_i, label);
-
 
1067
                $input.val('');
-
 
1068
            }
-
 
1069
        });
-
 
1070
    }
-
 
1071
 
-
 
1072
    function addLinkedEntity(type, id, label) {
-
 
1073
        var key = type + ':' + id;
-
 
1074
        var duplicate = false;
-
 
1075
        $('#linkedEntitiesList li').each(function () {
-
 
1076
            if ($(this).data('key') === key) {
-
 
1077
                duplicate = true;
-
 
1078
            }
-
 
1079
        });
-
 
1080
        if (duplicate) {
-
 
1081
            return;
-
 
1082
        }
-
 
1083
        var $li = $('<li class="nm-chip"></li>')
-
 
1084
            .attr('data-key', key)
-
 
1085
            .data({type: type, id: id, label: label})
-
 
1086
            .text('[' + type.charAt(0) + '] ' + label)
-
 
1087
            .append(' <span class="nm-chip-x removeLinkedEntity">&times;</span>');
-
 
1088
        $('#linkedEntitiesList').append($li);
-
 
1089
    }
-
 
1090
 
-
 
1091
    $(document).on('click', '.removeLinkedEntity', function () {
-
 
1092
        $(this).closest('li').remove();
-
 
1093
    });
-
 
1094
 
-
 
1095
    function collectLinkedEntities() {
-
 
1096
        var out = [];
-
 
1097
        $('#linkedEntitiesList li').each(function () {
-
 
1098
            out.push({type: $(this).data('type'), id: $(this).data('id'), label: $(this).data('label')});
-
 
1099
        });
-
 
1100
        return out;
-
 
1101
    }
-
 
1102
 
-
 
1103
    function escHtml(s) {
-
 
1104
        return String(s || '').replace(/[&<>]/g, function (c) {
-
 
1105
            return {'&': '&amp;', '<': '&lt;', '>': '&gt;'}[c];
-
 
1106
        });
-
 
1107
    }
-
 
1108
 
-
 
1109
    function escAttr(s) {
-
 
1110
        return String(s || '').replace(/"/g, '&quot;');
-
 
1111
    }
-
 
1112
 
-
 
1113
    function collectImages(target) {
-
 
1114
        var out = [];
-
 
1115
        $('.nm-thumb-list[data-target="' + target + '"] .nm-thumb').each(function () {
-
 
1116
            var url = $(this).data('url');
-
 
1117
            if (url) {
-
 
1118
                out.push(url);
-
 
1119
            }
-
 
1120
        });
-
 
1121
        return out;
-
 
1122
    }
-
 
1123
 
-
 
1124
    // ─── Category → legacy message_type mapping ─────────────────────────
-
 
1125
    var CATEGORY_TO_MESSAGE_TYPE = {
-
 
1126
        'ANNOUNCE': 'scheme',       // was OFFER — same underlying route
-
 
1127
        'NOTIFICATION': 'notification', // was INFO
-
 
1128
        'REMINDER': 'reminder',
-
 
1129
        'ALERT': 'pricechange'
-
 
1130
    };
-
 
1131
 
-
 
1132
    // ─── Schedule modal ─────────────────────────────────────────────────
-
 
1133
    $(document).on('click', '#nmScheduleBtn', function () {
-
 
1134
        $('#nmScheduleModal').modal('show');
-
 
1135
        refreshScheduleButtonState();   // set initial disabled state on open
-
 
1136
    });
-
 
1137
 
-
 
1138
    // "Schedule & save" is only meaningful when at least one date has been added.
-
 
1139
    // Grey out until then so the admin can't submit an empty schedule.
-
 
1140
    function refreshScheduleButtonState() {
-
 
1141
        var hasDates = $('#scheduleDatesList li').length > 0;
-
 
1142
        $('#nmSubmitScheduleBtn')
-
 
1143
            .prop('disabled', !hasDates)
-
 
1144
            .css('opacity', hasDates ? 1 : 0.5)
-
 
1145
            .css('cursor', hasDates ? 'pointer' : 'not-allowed');
-
 
1146
    }
-
 
1147
 
-
 
1148
    $(document).on('click', '#addScheduleDate', function () {
-
 
1149
        var raw = $('#scheduleDateInput').val();
-
 
1150
        if (!raw) {
-
 
1151
            alert('Pick a date and time');
-
 
1152
            return;
-
 
1153
        }
-
 
1154
        var iso = raw.length === 16 ? raw + ':00' : raw;
-
 
1155
        var duplicate = false;
-
 
1156
        $('#scheduleDatesList li').each(function () {
-
 
1157
            if ($(this).data('ts') === iso) {
-
 
1158
                duplicate = true;
-
 
1159
            }
-
 
1160
        });
-
 
1161
        if (duplicate) {
-
 
1162
            return;
-
 
1163
        }
-
 
1164
        var $li = $('<li class="nm-chip"></li>').attr('data-ts', iso)
-
 
1165
            .text(raw.replace('T', ' '))
-
 
1166
            .append(' <span class="nm-chip-x removeScheduleDate">&times;</span>');
-
 
1167
        $('#scheduleDatesList').append($li);
-
 
1168
        $('#scheduleDateInput').val('');
-
 
1169
        refreshScheduleButtonState();
-
 
1170
    });
-
 
1171
    $(document).on('click', '.removeScheduleDate', function () {
-
 
1172
        $(this).closest('li').remove();
-
 
1173
        refreshScheduleButtonState();
-
 
1174
    });
-
 
1175
 
-
 
1176
    $(document).on('click', '#nmSubmitScheduleBtn', function () {
-
 
1177
        var scheduledTimestamps = collectScheduledTimestamps();
-
 
1178
        if (scheduledTimestamps.length === 0) {
-
 
1179
            alert('Add at least one date, or use "Send now" to send immediately.');
-
 
1180
            return;
-
 
1181
        }
-
 
1182
        if (!validateAndConfirm('Schedule for ' + scheduledTimestamps.length + ' date(s)?')) {
-
 
1183
            return;
-
 
1184
        }
-
 
1185
        submitNotification(scheduledTimestamps, function () {
-
 
1186
            $('#nmScheduleModal').modal('hide');
-
 
1187
            $('#scheduleDatesList').empty();
-
 
1188
        });
-
 
1189
    });
-
 
1190
 
-
 
1191
    function collectScheduledTimestamps() {
-
 
1192
        var list = [];
-
 
1193
        $('#scheduleDatesList li').each(function () {
-
 
1194
            list.push($(this).data('ts'));
-
 
1195
        });
-
 
1196
        return list;
-
 
1197
    }
-
 
1198
 
-
 
1199
    // ─── Send / Publish now ─────────────────────────────────────────────
-
 
1200
    $(document).on('click', '.create-notification-button', function () {
-
 
1201
        var confirmText = activeFormat() === 'PUSH' ? 'Send this notification now?' : 'Publish now?';
-
 
1202
        if (!validateAndConfirm(confirmText)) {
-
 
1203
            return;
-
 
1204
        }
-
 
1205
        submitNotification([], function () { /* stay on the form */
-
 
1206
        });
-
 
1207
    });
-
 
1208
 
-
 
1209
    function validateAndConfirm(confirmText) {
-
 
1210
        var campaignName = $('#campaignName').val();
-
 
1211
        var title = $('#title').val();
-
 
1212
        var message = $('#message').val();
-
 
1213
        var category = getActiveCategorySelect().val();
-
 
1214
        var type = $('#type').val();
-
 
1215
        var fmt = activeFormat();
-
 
1216
 
-
 
1217
        if (!campaignName) {
-
 
1218
            alert('Campaign name is required');
-
 
1219
            return false;
-
 
1220
        }
-
 
1221
        if (!title) {
-
 
1222
            alert('Title is required');
-
 
1223
            return false;
-
 
1224
        }
-
 
1225
        if (!message) {
-
 
1226
            alert('Message is required');
-
 
1227
            return false;
-
 
1228
        }
-
 
1229
        if (!category) {
-
 
1230
            alert('Category is required');
-
 
1231
            return false;
-
 
1232
        }
-
 
1233
        if (!type) {
-
 
1234
            alert('Tap action is required');
-
 
1235
            return false;
-
 
1236
        }
-
 
1237
 
-
 
1238
        if (fmt === 'PUSH' && $('#nmHasExpiry').val() === 'yes' && !$('#expireSat').val()) {
-
 
1239
            alert('Expiry date is required (or switch life span to "Lifetime")');
-
 
1240
            return false;
-
 
1241
        }
-
 
1242
        if (fmt === 'IN_APP_POPUP' && !$('#showOnPage').val()) {
-
 
1243
            alert('"Show on page (route)" is required — e.g. /dashboard');
-
 
1244
            return false;
-
 
1245
        }
-
 
1246
        if (fmt === 'IN_APP_POPUP' && !$('#popupLiveUntil').val()) {
-
 
1247
            alert('"Live until" date is required');
-
 
1248
            return false;
-
 
1249
        }
-
 
1250
        if (fmt === 'STORY' && $('#expiresAfterPolicy').val() === 'ON_DATE' && !$('#storyExpiryOnDate').val()) {
-
 
1251
            alert('"Expires on" date is required');
-
 
1252
            return false;
-
 
1253
        }
-
 
1254
 
-
 
1255
        if (type === 'url') {
-
 
1256
            var linkVal = $('#selectedUrl').val();
-
 
1257
            var effectiveUrl = (linkVal === 'other') ? $('#url').val() : linkVal;
-
 
1258
            if (!effectiveUrl) {
-
 
1259
                alert('Link to open is required');
-
 
1260
                return false;
-
 
1261
            }
-
 
1262
        }
-
 
1263
        return confirm(confirmText);
-
 
1264
    }
-
 
1265
 
-
 
1266
    function submitNotification(scheduledTimestamps, onSuccess) {
-
 
1267
        var fmt = activeFormat();
-
 
1268
        var linkVal = $('#selectedUrl').val();
-
 
1269
        var effectiveUrl = (linkVal === 'other') ? $('#url').val() : linkVal;
-
 
1270
        var category = getActiveCategorySelect().val();
-
 
1271
 
-
 
1272
        var selectedGroup = $('#nmCampaignGroup').val();
-
 
1273
        var campaignGroupId = (selectedGroup && selectedGroup !== '__new__') ? parseInt(selectedGroup, 10) : null;
-
 
1274
 
-
 
1275
        var payload = {
-
 
1276
            stateIds: $('#state-users').val(),
-
 
1277
            userIds: ($('#fofo-users').val() || []).concat($('#internal-users').val() || []),
-
 
1278
            campaignGroupId: campaignGroupId,
-
 
1279
            campaignName: $('#campaignName').val(),
-
 
1280
            title: $('#title').val(),
-
 
1281
            message: $('#message').val(),
-
 
1282
            type: $('#type').val(),
-
 
1283
            url: effectiveUrl,
-
 
1284
            messageType: CATEGORY_TO_MESSAGE_TYPE[category],
-
 
1285
            category: category,
-
 
1286
            format: fmt,
-
 
1287
            linkedEntities: collectLinkedEntities()
-
 
1288
        };
-
 
1289
 
-
 
1290
        if (fmt === 'PUSH') {
-
 
1291
            var pushImgs = collectImages('pushImages');
-
 
1292
            payload.images = pushImgs;
-
 
1293
            payload.imageUrl = pushImgs[0] || '';   // first as legacy imageUrl for older readers
-
 
1294
            payload.showImage = pushImgs.length > 0 ? 'true' : 'false';
-
 
1295
            payload.documentId = localStorage.getItem('notifyDocument');
-
 
1296
            // Lifetime toggle: only send an expiry when the admin explicitly chose one.
-
 
1297
            // null / omit → mobile treats as never-expiring.
-
 
1298
            payload.expiresat = $('#nmHasExpiry').val() === 'yes'
-
 
1299
                ? getDatesFromPicker('input[name="dateTime"]').startDate
-
 
1300
                : null;
-
 
1301
        } else if (fmt === 'IN_APP_POPUP') {
-
 
1302
            var popupImgs = collectImages('popupImage');
-
 
1303
            payload.imageUrl = popupImgs[0] || '';
-
 
1304
            payload.buttonLabel = $('#buttonLabelPopup').val();
-
 
1305
            payload.showCta = $('#showCta').is(':checked');
-
 
1306
            payload.showClose = $('#showClose').is(':checked');
-
 
1307
            payload.showOnPage = $('#showOnPage').val() || null;
-
 
1308
            payload.autoCloseSeconds = $('#autoCloseSeconds').val() ? parseInt($('#autoCloseSeconds').val(), 10) : null;
-
 
1309
            payload.frequencyCap = 'ALWAYS';
-
 
1310
            payload.liveFrom = normalizeDate($('#popupLiveFrom').val());
-
 
1311
            // liveUntil is a date input; admin intent is "still visible through end of that day",
-
 
1312
            // so anchor at 23:59:59 instead of the default midnight start-of-day.
-
 
1313
            payload.liveUntil = normalizeDate($('#popupLiveUntil').val(), true);
-
 
1314
            payload.expiresat = payload.liveUntil;
-
 
1315
        } else if (fmt === 'STORY') {
-
 
1316
            payload.buttonLabel = $('#buttonLabelStory').val();
-
 
1317
            payload.storyCards = collectImages('storyCards');
-
 
1318
            payload.postInCategory = $('#postInCategory').val();
-
 
1319
            payload.playDurationSeconds = parseInt($('#playDurationSeconds').val(), 10);
-
 
1320
            payload.expiresAfterPolicy = $('#expiresAfterPolicy').val();
-
 
1321
            payload.liveFrom = normalizeDate($('#storyLiveFrom').val());
-
 
1322
            // Anchor a concrete liveUntil + expiresat so downstream (React app, cron, expiry check)
-
 
1323
            // doesn't have to derive it. FIXED_HOURS = liveFrom + 24 h; ON_DATE = whatever the admin picks.
-
 
1324
            if (payload.expiresAfterPolicy === 'ON_DATE') {
-
 
1325
                payload.expiresat = normalizeDate($('#storyExpiryOnDate').val());
-
 
1326
                payload.liveUntil = payload.expiresat;
-
 
1327
            } else {
-
 
1328
                payload.liveUntil = addHoursIso(payload.liveFrom, 24);
-
 
1329
                payload.expiresat = payload.liveUntil;
-
 
1330
            }
-
 
1331
        }
-
 
1332
 
-
 
1333
        if (scheduledTimestamps && scheduledTimestamps.length > 0) {
-
 
1334
            payload.scheduledTimestamps = scheduledTimestamps;
-
 
1335
        }
-
 
1336
 
-
 
1337
        doPostAjaxRequestWithJsonHandler(
-
 
1338
            context + '/sendNotification',
-
 
1339
            JSON.stringify(payload),
-
 
1340
            function (response) {
-
 
1341
                if (response === 'true' || response === true) {
-
 
1342
                    alert(scheduledTimestamps.length > 0
-
 
1343
                        ? 'Scheduled successfully'
-
 
1344
                        : (fmt === 'PUSH' ? 'Sent successfully' : 'Published successfully'));
-
 
1345
                    localStorage.removeItem('notifyDocument');
-
 
1346
                    if (onSuccess) {
-
 
1347
                        onSuccess();
-
 
1348
                    }
-
 
1349
                } else {
-
 
1350
                    alert('Something went wrong. Check the campaign and try again.');
-
 
1351
                }
-
 
1352
            });
-
 
1353
    }
-
 
1354
 
-
 
1355
    function normalizeDate(v, endOfDay) {
-
 
1356
        if (!v) {
-
 
1357
            return null;
-
 
1358
        }
-
 
1359
        if (v.length === 10) {
-
 
1360
            return v + (endOfDay ? 'T23:59:59' : 'T00:00:00');
-
 
1361
        }
-
 
1362
        if (v.length === 16) {
-
 
1363
            return v + ':00';
-
 
1364
        }
-
 
1365
        return v;
-
 
1366
    }
-
 
1367
 
-
 
1368
    function addHoursIso(isoLocal, hours) {
-
 
1369
        if (!isoLocal) {
-
 
1370
            return null;
-
 
1371
        }
-
 
1372
        var d = new Date(isoLocal);
-
 
1373
        if (isNaN(d.getTime())) {
-
 
1374
            return null;
-
 
1375
        }
-
 
1376
        d.setHours(d.getHours() + hours);
-
 
1377
        // Back to local-ISO without the trailing Z.
-
 
1378
        var pad = function (n) {
-
 
1379
            return String(n).padStart(2, '0');
-
 
1380
        };
-
 
1381
        return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
-
 
1382
            + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
-
 
1383
    }
-
 
1384
 
-
 
1385
    // ─── Reset ──────────────────────────────────────────────────────────
-
 
1386
    $(document).on('click', '#nmResetBtn', function () {
-
 
1387
        if (!confirm('Clear all fields?')) {
-
 
1388
            return;
-
 
1389
        }
-
 
1390
        $('input[type="text"], input[type="date"], input[type="datetime-local"], textarea').val('');
-
 
1391
        $('#titleCount, #messageCount').text('0');
-
 
1392
        $('#showImage').prop('checked', false).val('false');
-
 
1393
        $('#showCta, #showClose').prop('checked', true);
-
 
1394
        $('#fofo-users, #state-users, #internal-users').val([]).multiselect('refresh');
-
 
1395
        $('#type').val('url');
-
 
1396
        $('#selectedUrl').val('').trigger('change');
-
 
1397
        $('.nm-select[name="category"]').val('').trigger('change');
-
 
1398
        $('.nm-thumb-list').empty();
-
 
1399
        $('#scheduleDatesList').empty();
-
 
1400
        $('#linkedEntitiesList').empty();
-
 
1401
        $('#linkableType').val('');
-
 
1402
        $('#linkablePicker').hide();
-
 
1403
        $('#linkableSearchWrap').hide();
-
 
1404
        $('#nmCampaignGroup').val('');
-
 
1405
        // Reset life span toggle to default (lifetime / no expiry)
-
 
1406
        $('#nmHasExpiry').val('no').trigger('change');
-
 
1407
        // Clear the per-format title/message cache too
-
 
1408
        contentByFormat.PUSH = {title: '', message: ''};
-
 
1409
        contentByFormat.IN_APP_POPUP = {title: '', message: ''};
-
 
1410
        contentByFormat.STORY = {title: '', message: ''};
-
 
1411
        // Clear popup preview image
-
 
1412
        refreshPopupPreviewImage();
-
 
1413
        $('#url').hide();
-
 
1414
        $('.nm-format').removeClass('active');
-
 
1415
        $('.nm-format[data-format="PUSH"]').addClass('active').trigger('click');
-
 
1416
        $('#nmSummaryCampaign, #nmSummaryTap, #nmSummaryExpires').text('—');
-
 
1417
        $('#nmSummaryRecipients').text('Not selected');
-
 
1418
        $('#nmSummaryCategory').text('Not set');
-
 
1419
        $('#nmSummarySchedule').text('Send now');
-
 
1420
        localStorage.removeItem('notifyDocument');
-
 
1421
    });
122
});
1422
});
123
 
1423
 
124
function loadNotificationPanel(domId) {
1424
function loadNotificationPanel(domId) {
125
	localStorage.removeItem("notifyDocument");
1425
    localStorage.removeItem('notifyDocument');
126
	doGetAjaxRequestHandler(context + "/sendNotificationPanel", function(
1426
    doGetAjaxRequestHandler(context + '/sendNotificationPanel', function (response) {
127
			response) {
-
 
128
		$('#' + domId).html(response);
1427
		$('#' + domId).html(response);
129
		$("#url").hide();
-
 
130
 
-
 
131
	});
1428
	});
132
 
-
 
133
}
1429
}
134
 
1430
 
135
$(document)
1431
// Attachment document upload (Push format only — separate from image dropzones)
136
		.on(
-
 
137
				'input',
-
 
138
				'#notifyDocument',
1432
$(document).on('input', '#notifyDocument', function () {
139
				function() {
-
 
140
					if (confirm('Document has been selected, Do you want to upload ?')) {
1433
    if (confirm('Document has been selected, do you want to upload?')) {
141
						var fileSelector = $('#notifyDocument')[0];
1434
        var fileSelector = $('#notifyDocument')[0];
142
						if (fileSelector != undefined
-
 
143
								&& fileSelector.files[0] != undefined) {
1435
        if (fileSelector !== undefined && fileSelector.files[0] !== undefined) {
144
							var url = context  + '/document-upload';
1436
            var uploadUrl = context + '/document-upload';
145
 
-
 
146
							console.log(url);
-
 
147
							var file = this.files[0];
1437
            var file = this.files[0];
148
							doAjaxUploadRequestHandler(
1438
            doAjaxUploadRequestHandler(uploadUrl, 'POST', file, function (response) {
149
									url,
-
 
150
									'POST',
-
 
151
									file,
-
 
152
									function(response) {
-
 
153
										console.log(response);
-
 
154
										var documentId = response.response.document_id;
1439
                var documentId = response.response.document_id;
155
										console.log("documentId : "
-
 
156
												+ documentId);
-
 
157
										localStorage.setItem("notifyDocument",
1440
                localStorage.setItem('notifyDocument', documentId);
158
												documentId);
-
 
159
										// $('#retailerDocument').attr("documentId",
-
 
160
										// localStorage.getItem("retailerDocument"));
-
 
161
										// return
-
 
162
										// documentId;
-
 
163
									});
1441
            });
164
							// alert("Retailer Shop Document
-
 
165
							// is required");
-
 
166
						}
-
 
167
					} else {
-
 
168
						// Do nothing!
-
 
169
					}
-
 
170
				});
-
 
171
 
-
 
172
function changeUrl() {
-
 
173
	var val = $("#selectedUrl").val();
-
 
174
	console.log(val);
-
 
175
	if (val == "other") {
-
 
176
		$("#url").show();
-
 
177
	} else {
1442
        }
178
		$("#url").hide();
-
 
179
	}
1443
    }
180
 
-
 
181
}
1444
});