Subversion Repositories SmartDukaan

Rev

Rev 37508 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
37508 ranu 1
$(function () {
2
    $(document).on('click', '.send-notification', function () {
3
        loadNotificationPanel('main-content');
4
    });
25759 tejbeer 5
 
37508 ranu 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) {
21
            $preview.appendTo('.nm-preview-col');
22
        }
24011 tejbeer 23
	});
25383 tejbeer 24
 
37508 ranu 25
    // ─── Campaign Group picker ───────────────────────────────────────────
26
    function loadCampaignGroups(preselectId) {
27
        $.getJSON(context + '/campaignGroups', function (data) {
28
            var parsed = typeof data === 'string' ? JSON.parse(data) : data;
29
            var $sel = $('#nmCampaignGroup');
30
            $sel.find('option.nm-group-opt').remove();
31
            var $createNew = $sel.find('option[value="__new__"]');
32
            (parsed || []).forEach(function (g) {
33
                var opt = $('<option class="nm-group-opt"></option>')
34
                    .attr('value', g.id).text(g.name);
35
                $createNew.before(opt);
36
            });
37
            if (preselectId) {
38
                $sel.val(String(preselectId));
39
            }
40
        });
41
    }
25651 tejbeer 42
 
37508 ranu 43
    loadCampaignGroups();
25759 tejbeer 44
 
37508 ranu 45
    $(document).on('change', '#nmCampaignGroup', function () {
46
        if ($(this).val() === '__new__') {
47
            $(this).val('');
48
            $('#nmGroupName, #nmGroupDesc, #nmGroupStarts, #nmGroupEnds').val('');
49
            $('#nmGroupModal').modal('show');
50
        }
51
    });
25759 tejbeer 52
 
37508 ranu 53
    $(document).on('click', '#nmCreateGroupBtn', function () {
54
        var name = $('#nmGroupName').val().trim();
55
        if (!name) {
56
            alert('Group name is required');
57
            return;
58
        }
59
        var body = {
60
            name: name,
61
            description: $('#nmGroupDesc').val() || null,
62
            startsOn: $('#nmGroupStarts').val() || null,
63
            endsOn: $('#nmGroupEnds').val() || null
64
        };
65
        doPostAjaxRequestWithJsonHandler(context + '/campaignGroups', JSON.stringify(body), function (response) {
66
            var parsed = typeof response === 'string' ? JSON.parse(response) : response;
67
            if (parsed && parsed.id) {
68
                $('#nmGroupModal').modal('hide');
69
                loadCampaignGroups(parsed.id);
70
            } else {
71
                alert('Could not create the group. Try again.');
72
            }
73
        });
74
    });
25685 tejbeer 75
 
37508 ranu 76
    // ─── Tab strip ───────────────────────────────────────────────────────
77
    $(document).on('click', '.nm-tab', function () {
78
        var tab = $(this).data('tab');
79
        $('.nm-tab').removeClass('active');
80
        $(this).addClass('active');
81
        $('.nm-tab-content').removeClass('active');
82
        $('.nm-tab-content[data-tab-content="' + tab + '"]').addClass('active');
25383 tejbeer 83
 
37508 ranu 84
        if (tab === 'calendar') {
85
            initCalendar();
86
        }
87
    });
25759 tejbeer 88
 
37508 ranu 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();
25685 tejbeer 620
							return;
621
						}
37508 ranu 622
                        $('#scheduleDetailModal').modal('hide');
623
                        renderCalendarMonth();
624
                        renderScheduledList();
625
                        renderRecentList();
626
                    });
627
            });
628
        }
25383 tejbeer 629
 
37508 ranu 630
        var $btn = $('#ncCancelBtn').off('click');
631
        if (kind === 'push-schedule') {
632
            if (event.status !== 'SCHEDULED') {
633
                $btn.prop('disabled', true).text('Not cancellable');
634
            } else {
635
                $btn.prop('disabled', false).text('Cancel Schedule').on('click', function () {
636
                    if (!confirm('Cancel this scheduled notification?')) return;
637
                    doPostAjaxRequestWithJsonHandler(
638
                        context + '/notificationSchedule/cancel?scheduleId=' + encodeURIComponent(event.id),
639
                        '{}',
640
                        function (response) {
641
                            $('#scheduleDetailModal').modal('hide');
642
                            if (response === 'true' || response === true) {
643
                                renderCalendarMonth();
644
                                renderScheduledList();
645
                                renderRecentList();
646
                            } else {
647
                                alert('Could not cancel — the schedule may already be dispatched or cancelled.');
648
                            }
649
                        });
650
                });
651
            }
652
        } else {
653
            var alreadyEnded = event.end && new Date(event.end) < new Date();
654
            if (alreadyEnded) {
655
                $btn.prop('disabled', true).text('Already ended');
656
            } else {
657
                $btn.prop('disabled', false).text('Deactivate now').on('click', function () {
658
                    var noun = kind === 'popup-window' ? 'pop-up' : 'story';
659
                    if (!confirm('Deactivate this ' + noun + ' immediately? Partners will stop seeing it.')) return;
660
                    doPostAjaxRequestWithJsonHandler(
661
                        context + '/notificationCampaign/deactivate?campaignId=' + encodeURIComponent(event.campaignId),
662
                        '{}',
663
                        function (response) {
664
                            $('#scheduleDetailModal').modal('hide');
665
                            if (response === 'true' || response === true) {
666
                                renderCalendarMonth();
667
                                renderScheduledList();
668
                                renderRecentList();
669
                            } else {
670
                                alert('Could not deactivate — the row may be missing or already ended.');
671
                            }
672
                        });
673
                });
674
            }
675
        }
676
        $('#scheduleDetailModal').modal('show');
677
    });
25383 tejbeer 678
 
37508 ranu 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
    }
25383 tejbeer 685
 
37508 ranu 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
    };
25383 tejbeer 692
 
37508 ranu 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
    };
25383 tejbeer 701
 
37508 ranu 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
        }
25383 tejbeer 710
 
37508 ranu 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();
24011 tejbeer 717
 
37508 ranu 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');
25383 tejbeer 724
 
37508 ranu 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
    });
25383 tejbeer 745
 
37508 ranu 746
    function activeFormat() {
747
        return $('.nm-format.active').data('format') || 'PUSH';
748
    }
25651 tejbeer 749
 
37508 ranu 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
    }
25651 tejbeer 756
 
37508 ranu 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
    });
25759 tejbeer 786
 
37508 ranu 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
    });
37559 ranu 802
    // "None" tap behaviour hides the Link picker entirely so the admin can ship
803
    // an info-only notification (no navigation, no CTA).
804
    $(document).on('change', '#type', function () {
805
        if ($(this).val() === 'none') {
806
            $('#nmLinkField').hide();
807
            $('#selectedUrl').val('');
808
            $('#url').val('').hide();
809
            $('#nmSummaryTap').text('None');
810
        } else {
811
            $('#nmLinkField').show();
812
        }
813
    });
37508 ranu 814
    $(document).on('change', '#fofo-users, #state-users, #internal-users', updateRecipientSummary);
25759 tejbeer 815
 
37508 ranu 816
    function refreshCategorySummary() {
817
        $('#nmSummaryCategory').text(getActiveCategorySelect().find('option:selected').text() || 'Not set');
818
    }
819
 
820
    function refreshExpirySummary() {
821
        var v;
822
        var fmt = activeFormat();
823
        if (fmt === 'PUSH') {
824
            if ($('#nmHasExpiry').val() === 'no') {
825
                v = 'Never (lifetime)';
826
            } else {
827
                v = $('#expireSat').val();
828
            }
829
        } else if (fmt === 'IN_APP_POPUP') {
830
            v = $('#popupLiveUntil').val();
831
        } else if (fmt === 'STORY') {
832
            v = $('#expiresAfterPolicy').val() === 'ON_DATE'
833
                ? $('#storyExpiryOnDate').val()
834
                : '24 h from publish';
835
        }
836
        $('#nmSummaryExpires').text(v || '—');
837
    }
838
 
839
    // Show/hide expiry date input based on the lifetime toggle
840
    $(document).on('change', '#nmHasExpiry', function () {
841
        $('#nmExpiryDateField').toggle($(this).val() === 'yes');
842
        if ($(this).val() === 'no') {
843
            $('#expireSat').val('');
844
        }
845
        refreshExpirySummary();
846
    });
847
 
848
    function updateRecipientSummary() {
849
        var partners = ($('#fofo-users').val() || []).length;
850
        var states = ($('#state-users').val() || []).length;
851
        var users = ($('#internal-users').val() || []).length;
852
        var total = partners + states + users;
853
        if (total === 0) {
854
            $('#nmSummaryRecipients').text('Not selected');
855
            return;
856
        }
857
        var parts = [];
858
        if (partners) {
859
            parts.push(partners + ' partner' + (partners > 1 ? 's' : ''));
860
        }
861
        if (states) {
862
            parts.push(states + ' state' + (states > 1 ? 's' : ''));
863
        }
864
        if (users) {
865
            parts.push(users + ' user' + (users > 1 ? 's' : ''));
866
        }
867
        $('#nmSummaryRecipients').text(parts.join(' · '));
868
    }
869
 
870
    // ─── Image dropzone ─────────────────────────────────────────────────
871
    $(document).on('click', '.nm-dropzone', function (e) {
872
        if ($(e.target).is('.nm-file-input')) {
873
            return;
874
        }
875
        $(this).find('.nm-file-input').trigger('click');
876
    });
877
    $(document).on('dragover', '.nm-dropzone', function (e) {
878
        e.preventDefault();
879
        $(this).addClass('nm-dropzone-hover');
880
    });
881
    $(document).on('dragleave dragend drop', '.nm-dropzone', function () {
882
        $(this).removeClass('nm-dropzone-hover');
883
    });
884
    $(document).on('drop', '.nm-dropzone', function (e) {
885
        e.preventDefault();
886
        handleFiles(this, e.originalEvent.dataTransfer.files);
887
    });
888
    $(document).on('change', '.nm-file-input', function () {
889
        handleFiles($(this).closest('.nm-dropzone')[0], this.files);
890
        this.value = '';   // allow re-selecting the same file
891
    });
892
    $(document).on('click', '.nm-thumb-x', function (e) {
893
        e.stopPropagation();
894
        var $list = $(this).closest('.nm-thumb-list');
895
        $(this).closest('.nm-thumb').remove();
896
        if ($list.data('target') === 'popupImage') {
897
            refreshPopupPreviewImage();
898
        }
899
    });
900
 
901
    // Reflect the uploaded/removed popup image in the live preview card.
902
    function refreshPopupPreviewImage() {
903
        var $firstImg = $('.nm-thumb-list[data-target="popupImage"] .nm-thumb img').first();
904
        var $preview = $('#nmPreviewImagePopup');
905
        if ($firstImg.length && $firstImg.attr('src')) {
906
            $preview.attr('src', $firstImg.attr('src')).show();
907
        } else {
908
            $preview.attr('src', '').hide();
909
        }
910
    }
911
 
912
    function handleFiles(dropzone, fileList) {
913
        if (!fileList || fileList.length === 0) {
914
            return;
915
        }
916
        var $dz = $(dropzone);
917
        var target = $dz.data('target');
918
        var multi = $dz.data('multi') === true;
919
        var $list = $('.nm-thumb-list[data-target="' + target + '"]');
920
        if (!multi) {
921
            $list.empty();
922
        }
923
        for (var i = 0; i < fileList.length; i++) {
924
            uploadOne(fileList[i], $list);
925
            if (!multi) {
926
                break;
927
            }
928
        }
929
    }
930
 
931
    function uploadOne(file, $list) {
932
        if (!file.type || file.type.indexOf('image/') !== 0) {
933
            alert('Only image files are accepted.');
934
            return;
935
        }
936
        if (file.size > 5 * 1024 * 1024) {
937
            alert('Image is larger than 5 MB — please compress and retry.');
938
            return;
939
        }
940
        var $thumb = $('<li class="nm-thumb"></li>');
941
        var $spinner = $('<div class="nm-thumb-spinner"><i class="fa fa-spinner fa-spin"></i></div>');
942
        $thumb.append($spinner);
943
        $list.append($thumb);
944
 
945
        // Local preview via FileReader
946
        var reader = new FileReader();
947
        reader.onload = function (e) {
948
            $thumb.prepend('<img src="' + e.target.result + '">');
949
            if ($list.data('target') === 'popupImage') {
950
                refreshPopupPreviewImage();
951
            }
952
        };
953
        reader.readAsDataURL(file);
954
 
955
        // Upload to server; store the returned URL/id on the thumb.
956
        doAjaxUploadRequestHandler(context + '/document-upload', 'POST', file, function (response) {
957
            var docId = response && response.response && response.response.document_id;
958
            if (!docId) {
959
                $thumb.remove();
960
                alert('Upload failed. Try again.');
961
                return;
962
            }
963
            var url = context + '/document/' + docId;
964
            $thumb.data('doc-id', docId).data('url', url);
965
            $spinner.remove();
966
            $thumb.append('<span class="nm-thumb-x">&times;</span>');
967
        });
968
    }
969
 
970
    // ─── Linked entities: schemes / offers / products ────────────────────
971
    // Cache keyed by URL (which encodes recipient filters). Type + same recipients → cache hit.
972
    var LINKABLE_CACHE = {};
973
 
974
    function buildLinkableUrl(type) {
975
        var endpoint = type === 'SCHEME' ? 'schemes' : 'offers';
976
        var fofoIds = $('#fofo-users').val() || [];
977
        var stateIds = $('#state-users').val() || [];
978
        var qs = [];
979
        if (fofoIds.length) {
980
            qs.push('fofoIds=' + fofoIds.join(','));
981
        }
982
        if (stateIds.length) {
983
            qs.push('stateIds=' + stateIds.join(','));
984
        }
985
        return context + '/notificationLinkables/' + endpoint + (qs.length ? '?' + qs.join('&') : '');
986
    }
987
 
988
    // If recipients change while a scheme/offer picker is open, refetch with the new filter.
989
    $(document).on('change', '#fofo-users, #state-users', function () {
990
        LINKABLE_CACHE = {};   // invalidate the whole cache — filters affect every fetch
991
        var type = $('#linkableType').val();
992
        if (type === 'SCHEME' || type === 'OFFER') {
993
            $('#linkableType').trigger('change');   // reload the picker with new filters
994
        }
995
    });
996
 
997
    $(document).on('change', '#linkableType', function () {
998
        var type = $(this).val();
999
        var $picker = $('#linkablePicker');
1000
        var $searchWrap = $('#linkableSearchWrap');
1001
        if (!type) {
1002
            $picker.hide();
1003
            $searchWrap.hide();
1004
            return;
1005
        }
1006
        if (type === 'PRODUCT') {
1007
            $picker.hide();
1008
            $searchWrap.show();
1009
            initLinkableProductTypeahead();
1010
            $('#linkableSearchInput').val('').focus();
1011
            return;
1012
        }
1013
        // SCHEME / OFFER — build the URL from the current recipient selection.
1014
        $searchWrap.hide();
1015
        $picker.show().html('<option value="">— Loading —</option>');
1016
        var url = buildLinkableUrl(type);
1017
        if (LINKABLE_CACHE[url]) {
1018
            renderLinkableOptions($picker, LINKABLE_CACHE[url]);
1019
        } else {
1020
            $.getJSON(url, function (data) {
1021
                var parsed = typeof data === 'string' ? JSON.parse(data) : data;
1022
                LINKABLE_CACHE[url] = parsed;
1023
                renderLinkableOptions($picker, parsed);
1024
            }).fail(function () {
1025
                $picker.html('<option value="">— Failed to load —</option>');
1026
            });
1027
        }
1028
    });
1029
 
1030
    function renderLinkableOptions($sel, items) {
1031
        var opts = '<option value="">— Pick one —</option>';
1032
        items.forEach(function (it) {
1033
            opts += '<option value="' + it.id + '" data-label="' + escAttr(it.label) + '" data-type="' + it.type + '">'
1034
                + escHtml(it.label) + '</option>';
1035
        });
1036
        $sel.html(opts);
1037
    }
1038
 
1039
    $(document).on('change', '#linkablePicker', function () {
1040
        var $opt = $(this).find('option:selected');
1041
        if (!$opt.val()) {
1042
            return;
1043
        }
1044
        addLinkedEntity($opt.data('type'), parseInt($opt.val(), 10), $opt.data('label'));
1045
        $(this).val('');
1046
    });
1047
 
1048
    // Product typeahead — Bootstrap 3 typeahead widget against Solr-backed /entity.
1049
    // Same widget the rest of the app uses (getItemAheadOptions pattern) so behaviour and
1050
    // dropdown positioning stay consistent.
1051
    function initLinkableProductTypeahead() {
1052
        var $input = $('#linkableSearchInput');
1053
        if (!$input.length || $input.data('nm-typeahead-init')) {
1054
            return;
1055
        }
1056
        $input.data('nm-typeahead-init', true);
1057
        $input.typeahead('destroy').typeahead({
1058
            source: function (q, process) {
1059
                if (q && q.length >= 2) {
1060
                    return $.ajax(context + '/entity', {
1061
                        global: false,
1062
                        data: {query: q, limit: 30, activeOnly: true},
1063
                        success: function (data) {
1064
                            var parsed = typeof data === 'string' ? JSON.parse(data) : data;
1065
                            process(parsed || []);
1066
                        }
1067
                    });
1068
                }
1069
            },
1070
            delay: 250,
1071
            items: 20,
1072
            minLength: 2,
1073
            displayText: function (item) {
1074
                return item.title_s || ('Catalog ' + item.catalogId_i);
1075
            },
1076
            afterSelect: function (item) {
1077
                var label = item.title_s || ('Catalog ' + item.catalogId_i);
1078
                addLinkedEntity('PRODUCT', item.catalogId_i, label);
1079
                $input.val('');
1080
            }
1081
        });
1082
    }
1083
 
1084
    function addLinkedEntity(type, id, label) {
1085
        var key = type + ':' + id;
1086
        var duplicate = false;
1087
        $('#linkedEntitiesList li').each(function () {
1088
            if ($(this).data('key') === key) {
1089
                duplicate = true;
1090
            }
1091
        });
1092
        if (duplicate) {
1093
            return;
1094
        }
1095
        var $li = $('<li class="nm-chip"></li>')
1096
            .attr('data-key', key)
1097
            .data({type: type, id: id, label: label})
1098
            .text('[' + type.charAt(0) + '] ' + label)
1099
            .append(' <span class="nm-chip-x removeLinkedEntity">&times;</span>');
1100
        $('#linkedEntitiesList').append($li);
1101
    }
1102
 
1103
    $(document).on('click', '.removeLinkedEntity', function () {
1104
        $(this).closest('li').remove();
1105
    });
1106
 
1107
    function collectLinkedEntities() {
1108
        var out = [];
1109
        $('#linkedEntitiesList li').each(function () {
1110
            out.push({type: $(this).data('type'), id: $(this).data('id'), label: $(this).data('label')});
1111
        });
1112
        return out;
1113
    }
1114
 
1115
    function escHtml(s) {
1116
        return String(s || '').replace(/[&<>]/g, function (c) {
1117
            return {'&': '&amp;', '<': '&lt;', '>': '&gt;'}[c];
1118
        });
1119
    }
1120
 
1121
    function escAttr(s) {
1122
        return String(s || '').replace(/"/g, '&quot;');
1123
    }
1124
 
1125
    function collectImages(target) {
1126
        var out = [];
1127
        $('.nm-thumb-list[data-target="' + target + '"] .nm-thumb').each(function () {
1128
            var url = $(this).data('url');
1129
            if (url) {
1130
                out.push(url);
1131
            }
1132
        });
1133
        return out;
1134
    }
1135
 
1136
    // ─── Category → legacy message_type mapping ─────────────────────────
1137
    var CATEGORY_TO_MESSAGE_TYPE = {
1138
        'ANNOUNCE': 'scheme',       // was OFFER — same underlying route
1139
        'NOTIFICATION': 'notification', // was INFO
1140
        'REMINDER': 'reminder',
1141
        'ALERT': 'pricechange'
1142
    };
1143
 
1144
    // ─── Schedule modal ─────────────────────────────────────────────────
1145
    $(document).on('click', '#nmScheduleBtn', function () {
1146
        $('#nmScheduleModal').modal('show');
1147
        refreshScheduleButtonState();   // set initial disabled state on open
1148
    });
1149
 
1150
    // "Schedule & save" is only meaningful when at least one date has been added.
1151
    // Grey out until then so the admin can't submit an empty schedule.
1152
    function refreshScheduleButtonState() {
1153
        var hasDates = $('#scheduleDatesList li').length > 0;
1154
        $('#nmSubmitScheduleBtn')
1155
            .prop('disabled', !hasDates)
1156
            .css('opacity', hasDates ? 1 : 0.5)
1157
            .css('cursor', hasDates ? 'pointer' : 'not-allowed');
1158
    }
1159
 
1160
    $(document).on('click', '#addScheduleDate', function () {
1161
        var raw = $('#scheduleDateInput').val();
1162
        if (!raw) {
1163
            alert('Pick a date and time');
1164
            return;
1165
        }
1166
        var iso = raw.length === 16 ? raw + ':00' : raw;
1167
        var duplicate = false;
1168
        $('#scheduleDatesList li').each(function () {
1169
            if ($(this).data('ts') === iso) {
1170
                duplicate = true;
1171
            }
1172
        });
1173
        if (duplicate) {
1174
            return;
1175
        }
1176
        var $li = $('<li class="nm-chip"></li>').attr('data-ts', iso)
1177
            .text(raw.replace('T', ' '))
1178
            .append(' <span class="nm-chip-x removeScheduleDate">&times;</span>');
1179
        $('#scheduleDatesList').append($li);
1180
        $('#scheduleDateInput').val('');
1181
        refreshScheduleButtonState();
1182
    });
1183
    $(document).on('click', '.removeScheduleDate', function () {
1184
        $(this).closest('li').remove();
1185
        refreshScheduleButtonState();
1186
    });
1187
 
1188
    $(document).on('click', '#nmSubmitScheduleBtn', function () {
1189
        var scheduledTimestamps = collectScheduledTimestamps();
1190
        if (scheduledTimestamps.length === 0) {
1191
            alert('Add at least one date, or use "Send now" to send immediately.');
1192
            return;
1193
        }
1194
        if (!validateAndConfirm('Schedule for ' + scheduledTimestamps.length + ' date(s)?')) {
1195
            return;
1196
        }
1197
        submitNotification(scheduledTimestamps, function () {
1198
            $('#nmScheduleModal').modal('hide');
1199
            $('#scheduleDatesList').empty();
1200
        });
1201
    });
1202
 
1203
    function collectScheduledTimestamps() {
1204
        var list = [];
1205
        $('#scheduleDatesList li').each(function () {
1206
            list.push($(this).data('ts'));
1207
        });
1208
        return list;
1209
    }
1210
 
1211
    // ─── Send / Publish now ─────────────────────────────────────────────
1212
    $(document).on('click', '.create-notification-button', function () {
1213
        var confirmText = activeFormat() === 'PUSH' ? 'Send this notification now?' : 'Publish now?';
1214
        if (!validateAndConfirm(confirmText)) {
1215
            return;
1216
        }
1217
        submitNotification([], function () { /* stay on the form */
1218
        });
1219
    });
1220
 
1221
    function validateAndConfirm(confirmText) {
1222
        var campaignName = $('#campaignName').val();
1223
        var title = $('#title').val();
1224
        var message = $('#message').val();
1225
        var category = getActiveCategorySelect().val();
1226
        var type = $('#type').val();
1227
        var fmt = activeFormat();
1228
 
1229
        if (!campaignName) {
1230
            alert('Campaign name is required');
1231
            return false;
1232
        }
1233
        if (!title) {
1234
            alert('Title is required');
1235
            return false;
1236
        }
1237
        if (!message) {
1238
            alert('Message is required');
1239
            return false;
1240
        }
1241
        if (!category) {
1242
            alert('Category is required');
1243
            return false;
1244
        }
1245
        if (!type) {
1246
            alert('Tap action is required');
1247
            return false;
1248
        }
1249
 
1250
        if (fmt === 'PUSH' && $('#nmHasExpiry').val() === 'yes' && !$('#expireSat').val()) {
1251
            alert('Expiry date is required (or switch life span to "Lifetime")');
1252
            return false;
1253
        }
1254
        if (fmt === 'IN_APP_POPUP' && !$('#showOnPage').val()) {
1255
            alert('"Show on page (route)" is required — e.g. /dashboard');
1256
            return false;
1257
        }
1258
        if (fmt === 'IN_APP_POPUP' && !$('#popupLiveUntil').val()) {
1259
            alert('"Live until" date is required');
1260
            return false;
1261
        }
1262
        if (fmt === 'STORY' && $('#expiresAfterPolicy').val() === 'ON_DATE' && !$('#storyExpiryOnDate').val()) {
1263
            alert('"Expires on" date is required');
1264
            return false;
1265
        }
1266
 
1267
        if (type === 'url') {
1268
            var linkVal = $('#selectedUrl').val();
1269
            var effectiveUrl = (linkVal === 'other') ? $('#url').val() : linkVal;
1270
            if (!effectiveUrl) {
1271
                alert('Link to open is required');
1272
                return false;
1273
            }
1274
        }
1275
        return confirm(confirmText);
1276
    }
1277
 
1278
    function submitNotification(scheduledTimestamps, onSuccess) {
1279
        var fmt = activeFormat();
37559 ranu 1280
        var typeVal = $('#type').val();
37508 ranu 1281
        var linkVal = $('#selectedUrl').val();
37559 ranu 1282
        var effectiveUrl = (typeVal === 'none')
1283
            ? ''
1284
            : (linkVal === 'other' ? $('#url').val() : linkVal);
37508 ranu 1285
        var category = getActiveCategorySelect().val();
1286
 
1287
        var selectedGroup = $('#nmCampaignGroup').val();
1288
        var campaignGroupId = (selectedGroup && selectedGroup !== '__new__') ? parseInt(selectedGroup, 10) : null;
1289
 
1290
        var payload = {
1291
            stateIds: $('#state-users').val(),
1292
            userIds: ($('#fofo-users').val() || []).concat($('#internal-users').val() || []),
1293
            campaignGroupId: campaignGroupId,
1294
            campaignName: $('#campaignName').val(),
1295
            title: $('#title').val(),
1296
            message: $('#message').val(),
1297
            type: $('#type').val(),
1298
            url: effectiveUrl,
1299
            messageType: CATEGORY_TO_MESSAGE_TYPE[category],
1300
            category: category,
1301
            format: fmt,
1302
            linkedEntities: collectLinkedEntities()
1303
        };
1304
 
1305
        if (fmt === 'PUSH') {
1306
            var pushImgs = collectImages('pushImages');
1307
            payload.images = pushImgs;
1308
            payload.imageUrl = pushImgs[0] || '';   // first as legacy imageUrl for older readers
1309
            payload.showImage = pushImgs.length > 0 ? 'true' : 'false';
1310
            payload.documentId = localStorage.getItem('notifyDocument');
1311
            // Lifetime toggle: only send an expiry when the admin explicitly chose one.
1312
            // null / omit → mobile treats as never-expiring.
1313
            payload.expiresat = $('#nmHasExpiry').val() === 'yes'
1314
                ? getDatesFromPicker('input[name="dateTime"]').startDate
1315
                : null;
1316
        } else if (fmt === 'IN_APP_POPUP') {
1317
            var popupImgs = collectImages('popupImage');
1318
            payload.imageUrl = popupImgs[0] || '';
1319
            payload.buttonLabel = $('#buttonLabelPopup').val();
1320
            payload.showCta = $('#showCta').is(':checked');
1321
            payload.showClose = $('#showClose').is(':checked');
1322
            payload.showOnPage = $('#showOnPage').val() || null;
1323
            payload.autoCloseSeconds = $('#autoCloseSeconds').val() ? parseInt($('#autoCloseSeconds').val(), 10) : null;
1324
            payload.frequencyCap = 'ALWAYS';
1325
            payload.liveFrom = normalizeDate($('#popupLiveFrom').val());
1326
            // liveUntil is a date input; admin intent is "still visible through end of that day",
1327
            // so anchor at 23:59:59 instead of the default midnight start-of-day.
1328
            payload.liveUntil = normalizeDate($('#popupLiveUntil').val(), true);
1329
            payload.expiresat = payload.liveUntil;
1330
        } else if (fmt === 'STORY') {
1331
            payload.buttonLabel = $('#buttonLabelStory').val();
1332
            payload.storyCards = collectImages('storyCards');
1333
            payload.postInCategory = $('#postInCategory').val();
1334
            payload.playDurationSeconds = parseInt($('#playDurationSeconds').val(), 10);
1335
            payload.expiresAfterPolicy = $('#expiresAfterPolicy').val();
1336
            payload.liveFrom = normalizeDate($('#storyLiveFrom').val());
1337
            // Anchor a concrete liveUntil + expiresat so downstream (React app, cron, expiry check)
1338
            // doesn't have to derive it. FIXED_HOURS = liveFrom + 24 h; ON_DATE = whatever the admin picks.
1339
            if (payload.expiresAfterPolicy === 'ON_DATE') {
1340
                payload.expiresat = normalizeDate($('#storyExpiryOnDate').val());
1341
                payload.liveUntil = payload.expiresat;
1342
            } else {
1343
                payload.liveUntil = addHoursIso(payload.liveFrom, 24);
1344
                payload.expiresat = payload.liveUntil;
1345
            }
1346
        }
1347
 
1348
        if (scheduledTimestamps && scheduledTimestamps.length > 0) {
1349
            payload.scheduledTimestamps = scheduledTimestamps;
1350
        }
1351
 
1352
        doPostAjaxRequestWithJsonHandler(
1353
            context + '/sendNotification',
1354
            JSON.stringify(payload),
1355
            function (response) {
1356
                if (response === 'true' || response === true) {
1357
                    alert(scheduledTimestamps.length > 0
1358
                        ? 'Scheduled successfully'
1359
                        : (fmt === 'PUSH' ? 'Sent successfully' : 'Published successfully'));
1360
                    localStorage.removeItem('notifyDocument');
1361
                    if (onSuccess) {
1362
                        onSuccess();
1363
                    }
1364
                } else {
1365
                    alert('Something went wrong. Check the campaign and try again.');
1366
                }
1367
            });
1368
    }
1369
 
1370
    function normalizeDate(v, endOfDay) {
1371
        if (!v) {
1372
            return null;
1373
        }
1374
        if (v.length === 10) {
1375
            return v + (endOfDay ? 'T23:59:59' : 'T00:00:00');
1376
        }
1377
        if (v.length === 16) {
1378
            return v + ':00';
1379
        }
1380
        return v;
1381
    }
1382
 
1383
    function addHoursIso(isoLocal, hours) {
1384
        if (!isoLocal) {
1385
            return null;
1386
        }
1387
        var d = new Date(isoLocal);
1388
        if (isNaN(d.getTime())) {
1389
            return null;
1390
        }
1391
        d.setHours(d.getHours() + hours);
1392
        // Back to local-ISO without the trailing Z.
1393
        var pad = function (n) {
1394
            return String(n).padStart(2, '0');
1395
        };
1396
        return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
1397
            + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
1398
    }
1399
 
1400
    // ─── Reset ──────────────────────────────────────────────────────────
1401
    $(document).on('click', '#nmResetBtn', function () {
1402
        if (!confirm('Clear all fields?')) {
1403
            return;
1404
        }
1405
        $('input[type="text"], input[type="date"], input[type="datetime-local"], textarea').val('');
1406
        $('#titleCount, #messageCount').text('0');
1407
        $('#showImage').prop('checked', false).val('false');
1408
        $('#showCta, #showClose').prop('checked', true);
1409
        $('#fofo-users, #state-users, #internal-users').val([]).multiselect('refresh');
1410
        $('#type').val('url');
1411
        $('#selectedUrl').val('').trigger('change');
1412
        $('.nm-select[name="category"]').val('').trigger('change');
1413
        $('.nm-thumb-list').empty();
1414
        $('#scheduleDatesList').empty();
1415
        $('#linkedEntitiesList').empty();
1416
        $('#linkableType').val('');
1417
        $('#linkablePicker').hide();
1418
        $('#linkableSearchWrap').hide();
1419
        $('#nmCampaignGroup').val('');
1420
        // Reset life span toggle to default (lifetime / no expiry)
1421
        $('#nmHasExpiry').val('no').trigger('change');
1422
        // Clear the per-format title/message cache too
1423
        contentByFormat.PUSH = {title: '', message: ''};
1424
        contentByFormat.IN_APP_POPUP = {title: '', message: ''};
1425
        contentByFormat.STORY = {title: '', message: ''};
1426
        // Clear popup preview image
1427
        refreshPopupPreviewImage();
1428
        $('#url').hide();
1429
        $('.nm-format').removeClass('active');
1430
        $('.nm-format[data-format="PUSH"]').addClass('active').trigger('click');
1431
        $('#nmSummaryCampaign, #nmSummaryTap, #nmSummaryExpires').text('—');
1432
        $('#nmSummaryRecipients').text('Not selected');
1433
        $('#nmSummaryCategory').text('Not set');
1434
        $('#nmSummarySchedule').text('Send now');
1435
        localStorage.removeItem('notifyDocument');
1436
    });
1437
});
1438
 
1439
function loadNotificationPanel(domId) {
1440
    localStorage.removeItem('notifyDocument');
1441
    doGetAjaxRequestHandler(context + '/sendNotificationPanel', function (response) {
1442
		$('#' + domId).html(response);
1443
	});
25759 tejbeer 1444
}
37508 ranu 1445
 
1446
// Attachment document upload (Push format only — separate from image dropzones)
1447
$(document).on('input', '#notifyDocument', function () {
1448
    if (confirm('Document has been selected, do you want to upload?')) {
1449
        var fileSelector = $('#notifyDocument')[0];
1450
        if (fileSelector !== undefined && fileSelector.files[0] !== undefined) {
1451
            var uploadUrl = context + '/document-upload';
1452
            var file = this.files[0];
1453
            doAjaxUploadRequestHandler(uploadUrl, 'POST', file, function (response) {
1454
                var documentId = response.response.document_id;
1455
                localStorage.setItem('notifyDocument', documentId);
1456
            });
1457
        }
1458
    }
1459
});