ultimate-jekyll-manager 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/CLAUDE.md +64 -1
  3. package/TODO.md +13 -0
  4. package/dist/assets/css/pages/admin/calendar/index.scss +212 -18
  5. package/dist/assets/js/pages/admin/calendar/calendar-core.js +535 -95
  6. package/dist/assets/js/pages/admin/calendar/calendar-events.js +631 -124
  7. package/dist/assets/js/pages/admin/calendar/calendar-renderer.js +238 -69
  8. package/dist/assets/js/pages/admin/calendar/campaign-preview.js +100 -0
  9. package/dist/assets/js/pages/admin/calendar/index.js +3 -16
  10. package/dist/assets/js/pages/contact/index.js +5 -1
  11. package/dist/defaults/dist/_includes/admin/sections/sidebar.json +0 -34
  12. package/dist/defaults/dist/_includes/admin/sections/topbar.json +0 -34
  13. package/dist/defaults/dist/_includes/themes/classy/backend/sections/topbar.html +1 -72
  14. package/dist/defaults/dist/_includes/themes/classy/frontend/sections/nav.html +7 -140
  15. package/dist/defaults/dist/_includes/themes/classy/global/sections/account.html +72 -0
  16. package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +442 -159
  17. package/dist/defaults/src/_includes/backend/sections/topbar.json +0 -34
  18. package/dist/defaults/src/_includes/frontend/sections/nav.json +0 -34
  19. package/dist/defaults/src/_includes/global/sections/account.json +36 -0
  20. package/package.json +2 -1
  21. package/dist/assets/js/pages/admin/notifications/index.js +0 -53
  22. package/dist/assets/js/pages/admin/notifications/new/index.js +0 -492
  23. package/dist/defaults/dist/_layouts/blueprint/admin/newsletters/index.html +0 -59
  24. package/dist/defaults/dist/_layouts/blueprint/admin/newsletters/new.html +0 -46
  25. package/dist/defaults/dist/_layouts/blueprint/admin/notifications/index.html +0 -103
  26. package/dist/defaults/dist/_layouts/blueprint/admin/notifications/new.html +0 -399
  27. package/dist/defaults/dist/pages/admin/newsletters/index.html +0 -7
  28. package/dist/defaults/dist/pages/admin/newsletters/new.html +0 -7
  29. package/dist/defaults/dist/pages/admin/notifications/index.html +0 -7
  30. package/dist/defaults/dist/pages/admin/notifications/new.html +0 -7
@@ -1,18 +1,24 @@
1
1
  /**
2
- * Calendar Events
3
- * Event CRUD via FormManager, modal management, and color swatch handling.
2
+ * Calendar Events (Campaign Editor)
3
+ * Campaign CRUD via FormManager + BEM API, modal management,
4
+ * type toggling (email/push), and results viewer.
4
5
  */
5
6
 
6
7
  import { FormManager } from '__main_assets__/js/libs/form-manager.js';
8
+ import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
7
9
  import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
10
+ import { escapeHtml } from '__main_assets__/js/libs/admin-helpers.js';
11
+ import { DISPLAY_TYPES } from './calendar-core.js';
12
+ import { renderEmailPreview, renderPushPreview } from './campaign-preview.js';
8
13
 
9
14
  export default class CalendarEvents {
10
15
  constructor(core, webManager) {
11
16
  this.core = core;
12
17
  this.webManager = webManager;
13
- this.editingEventId = null;
18
+ this.editingCampaignId = null;
14
19
  this.formManager = null;
15
- this.$modal = null;
20
+ this.$editorModal = null;
21
+ this.$resultsModal = null;
16
22
 
17
23
  this._init();
18
24
  }
@@ -21,67 +27,192 @@ export default class CalendarEvents {
21
27
  // Initialization
22
28
  // ============================================
23
29
  _init() {
24
- this._initModal();
30
+ this._initEditorModal();
31
+ this._initResultsModal();
25
32
  this._initForm();
26
- this._initColorSwatches();
33
+ this._initTypeToggle();
34
+ this._initRecurrenceToggle();
35
+ this._initSendNow();
27
36
  this._initDeleteButton();
37
+ this._initCreateButton();
38
+ this._initPreview();
28
39
  }
29
40
 
30
- _initModal() {
31
- this.$modal = document.getElementById('calendar-event-modal');
41
+ _initEditorModal() {
42
+ this.$editorModal = document.getElementById('campaign-editor-modal');
32
43
 
33
- // Reset form when modal closes
34
- this.$modal.addEventListener('hidden.bs.modal', () => {
35
- this.editingEventId = null;
44
+ this.$editorModal.addEventListener('hidden.bs.modal', () => {
45
+ this.editingCampaignId = null;
36
46
  this.formManager.reset();
37
- this._setColor('#4CAF50');
38
47
  this._toggleDeleteButton(false);
39
- document.getElementById('event-modal-title-text').textContent = 'Create Event';
48
+ this._setType('email');
49
+ this._resetRecurrence();
50
+ document.getElementById('campaign-modal-title-text').textContent = 'Create Campaign';
40
51
  });
41
52
  }
42
53
 
43
- _getModal() {
44
- return bootstrap.Modal.getOrCreateInstance(this.$modal);
54
+ _initResultsModal() {
55
+ this.$resultsModal = document.getElementById('campaign-results-modal');
56
+
57
+ document.getElementById('btn-retry-campaign').addEventListener('click', () => {
58
+ const campaign = this._retrySource;
59
+ if (!campaign) {
60
+ return;
61
+ }
62
+
63
+ this._getResultsModal().hide();
64
+
65
+ // Open editor with same settings but no ID (creates new)
66
+ this._populateFormFromCampaign(campaign, false);
67
+ document.getElementById('campaign-modal-title-text').textContent = 'Retry Campaign';
68
+ this._getEditorModal().show();
69
+ });
70
+ }
71
+
72
+ _getEditorModal() {
73
+ return bootstrap.Modal.getOrCreateInstance(this.$editorModal);
74
+ }
75
+
76
+ _getResultsModal() {
77
+ return bootstrap.Modal.getOrCreateInstance(this.$resultsModal);
45
78
  }
46
79
 
47
80
  _initForm() {
48
- this.formManager = new FormManager('#calendar-event-form', {
81
+ this.formManager = new FormManager('#campaign-editor-form', {
49
82
  autoReady: true,
50
83
  allowResubmit: true,
51
84
  });
52
85
 
53
- this.formManager.on('submit', ({ data }) => {
54
- const eventData = this._extractEventData(data);
86
+ this.formManager.on('submit', async ({ data }) => {
87
+ const payload = this._buildPayload(data);
55
88
 
56
- if (this.editingEventId) {
57
- this.core.updateEvent(this.editingEventId, eventData);
89
+ if (this.editingCampaignId) {
90
+ await this._updateCampaign(this.editingCampaignId, payload);
58
91
  } else {
59
- this.core.addEvent(eventData);
92
+ await this._createCampaign(payload);
60
93
  }
61
94
 
62
- this._getModal().hide();
95
+ this._getEditorModal().hide();
63
96
  });
64
97
  }
65
98
 
66
- _initColorSwatches() {
67
- document.getElementById('color-swatches').addEventListener('click', (e) => {
68
- const $swatch = e.target.closest('.color-swatch');
69
- if (!$swatch) {
99
+ _initTypeToggle() {
100
+ const $emailFields = document.getElementById('email-fields');
101
+ const $pushFields = document.getElementById('push-fields');
102
+ const $subjectHint = document.getElementById('campaign-subject-hint');
103
+
104
+ document.querySelectorAll('input[name="campaign.type"]').forEach(($radio) => {
105
+ $radio.addEventListener('change', () => {
106
+ const isEmail = $radio.value === 'email';
107
+ $emailFields.classList.toggle('d-none', !isEmail);
108
+ $pushFields.classList.toggle('d-none', isEmail);
109
+ $subjectHint.textContent = isEmail ? '(email subject line)' : '(notification body text)';
110
+ });
111
+ });
112
+ }
113
+
114
+ _initCreateButton() {
115
+ const $btn = document.getElementById('btn-create-campaign');
116
+ if (!$btn) {
117
+ return;
118
+ }
119
+ $btn.addEventListener('click', () => {
120
+ const today = this.core.formatDate(new Date());
121
+ this.openCreateModal(today, '09:00');
122
+ });
123
+ }
124
+
125
+ _initPreview() {
126
+ const $previewTab = document.getElementById('tab-preview');
127
+ const $previewContainer = document.getElementById('campaign-preview-container');
128
+
129
+ // Capture form data BEFORE tab switch (Edit pane still visible), render AFTER
130
+ let pendingData = null;
131
+ $previewTab.addEventListener('show.bs.tab', () => {
132
+ pendingData = this.formManager.getData();
133
+ });
134
+ $previewTab.addEventListener('shown.bs.tab', () => {
135
+ this._renderPreview($previewContainer, pendingData);
136
+ pendingData = null;
137
+ });
138
+
139
+ // Handle click-to-test on push notification preview
140
+ $previewContainer.addEventListener('click', (e) => {
141
+ const $notification = e.target.closest('[data-click-action]');
142
+ if (!$notification) {
70
143
  return;
71
144
  }
145
+ const url = $notification.dataset.clickAction;
146
+ if (url) {
147
+ window.open(url, '_blank', 'noopener,noreferrer');
148
+ }
149
+ });
150
+ }
151
+
152
+ async _renderPreview($container, data) {
153
+ data = data || this.formManager.getData();
154
+ const type = (data.campaign && data.campaign.type) || 'email';
155
+
156
+ try {
157
+ if (type === 'email') {
158
+ $container.innerHTML = await renderEmailPreview(data);
159
+ } else {
160
+ $container.innerHTML = renderPushPreview(data);
161
+ }
162
+ } catch (error) {
163
+ $container.innerHTML = `<div class="text-danger small">Preview failed: ${error.message}</div>`;
164
+ }
165
+ }
166
+
167
+ _initRecurrenceToggle() {
168
+ const $checkbox = document.getElementById('campaign-recurring');
169
+ const $fields = document.getElementById('recurrence-fields');
170
+ const $patternSelect = document.getElementById('campaign-recurrence-pattern');
171
+ const $dayHint = document.getElementById('recurrence-day-hint');
172
+ const $monthRow = document.getElementById('recurrence-month-row');
173
+
174
+ $checkbox.addEventListener('change', () => {
175
+ $fields.classList.toggle('d-none', !$checkbox.checked);
176
+ });
177
+
178
+ $patternSelect.addEventListener('change', () => {
179
+ const pattern = $patternSelect.value;
180
+ // Update day hint based on pattern
181
+ if (pattern === 'weekly') {
182
+ $dayHint.textContent = '(of week, 0=Sun)';
183
+ } else {
184
+ $dayHint.textContent = '(of month)';
185
+ }
186
+ // Show month field only for yearly
187
+ $monthRow.classList.toggle('d-none', pattern !== 'yearly');
188
+ });
189
+ }
72
190
 
73
- this._setColor($swatch.dataset.color);
191
+ _initSendNow() {
192
+ document.getElementById('btn-send-now').addEventListener('click', () => {
193
+ const now = new Date();
194
+ document.getElementById('campaign-date').value = this.core.formatDate(now);
195
+ document.getElementById('campaign-time').value =
196
+ String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
74
197
  });
75
198
  }
76
199
 
77
200
  _initDeleteButton() {
78
- document.getElementById('btn-delete-event').addEventListener('click', () => {
79
- if (!this.editingEventId) {
201
+ document.getElementById('btn-delete-campaign').addEventListener('click', async () => {
202
+ if (!this.editingCampaignId) {
203
+ return;
204
+ }
205
+
206
+ if (!confirm('Delete this campaign? This cannot be undone.')) {
80
207
  return;
81
208
  }
82
209
 
83
- this.core.removeEvent(this.editingEventId);
84
- this._getModal().hide();
210
+ try {
211
+ await this._deleteCampaign(this.editingCampaignId);
212
+ this._getEditorModal().hide();
213
+ } catch (error) {
214
+ this.formManager.showError(`Delete failed: ${error.message}`);
215
+ }
85
216
  });
86
217
  }
87
218
 
@@ -89,125 +220,501 @@ export default class CalendarEvents {
89
220
  // Modal Operations
90
221
  // ============================================
91
222
  openCreateModal(date, time) {
92
- this.editingEventId = null;
223
+ this.editingCampaignId = null;
93
224
  this.formManager.reset();
94
225
  this._toggleDeleteButton(false);
95
- document.getElementById('event-modal-title-text').textContent = 'Create Event';
226
+ this._setType('email');
227
+ document.getElementById('campaign-modal-title-text').textContent = 'Create Campaign';
96
228
 
97
229
  // Pre-fill date and time
98
- document.getElementById('event-date').value = date || '';
99
- document.getElementById('event-time').value = time || '09:00';
100
- document.getElementById('event-duration').value = '60';
101
-
102
- // Reset type to newsletter
103
- document.getElementById('event-type-newsletter').checked = true;
230
+ document.getElementById('campaign-date').value = date || '';
231
+ document.getElementById('campaign-time').value = time || '09:00';
104
232
 
105
- // Reset audience
106
- document.getElementById('audience-all').checked = true;
233
+ this._getEditorModal().show();
234
+ }
107
235
 
108
- // Reset channels
109
- document.getElementById('channel-push').checked = true;
110
- document.getElementById('channel-email').checked = false;
111
- document.getElementById('channel-sms').checked = false;
112
- document.getElementById('channel-inapp').checked = false;
236
+ openEditModal(campaignId) {
237
+ const campaign = this.core.getCampaign(campaignId);
238
+ if (!campaign) {
239
+ return;
240
+ }
113
241
 
114
- // Reset status
115
- document.getElementById('event-status').value = 'draft';
242
+ const displayType = this.core.getCampaignDisplayType(campaign);
116
243
 
117
- // Reset color
118
- this._setColor('#4CAF50');
244
+ // Virtual recurring occurrences: open the recurring template editor
245
+ if (campaign._virtual) {
246
+ const template = this.core.getCampaign(campaign._recurringSourceId);
247
+ if (!template) {
248
+ return;
249
+ }
250
+ this.editingCampaignId = template.id;
251
+ this._toggleDeleteButton(true);
252
+ document.getElementById('campaign-modal-title-text').textContent = 'Edit Recurring Campaign';
253
+ this._populateFormFromCampaign(template, true);
254
+ this._getEditorModal().show();
255
+ return;
256
+ }
119
257
 
120
- this._getModal().show();
121
- }
258
+ // History records and sent/failed: read-only results modal
259
+ if (displayType === DISPLAY_TYPES.RECURRING_HISTORY
260
+ || campaign.status === 'sent'
261
+ || campaign.status === 'failed') {
262
+ this._openResultsModal(campaign);
263
+ return;
264
+ }
122
265
 
123
- openEditModal(eventId) {
124
- const event = this.core.getEvent(eventId);
125
- if (!event) {
266
+ // Recurring template: editable (changes apply to all future sends)
267
+ if (displayType === DISPLAY_TYPES.RECURRING_TEMPLATE) {
268
+ this.editingCampaignId = campaignId;
269
+ this._toggleDeleteButton(true);
270
+ document.getElementById('campaign-modal-title-text').textContent = 'Edit Recurring Campaign';
271
+ this._populateFormFromCampaign(campaign, true);
272
+ this._getEditorModal().show();
126
273
  return;
127
274
  }
128
275
 
129
- this.editingEventId = eventId;
276
+ // One-off pending: fully editable
277
+ this.editingCampaignId = campaignId;
130
278
  this._toggleDeleteButton(true);
131
- document.getElementById('event-modal-title-text').textContent = 'Edit Event';
279
+ document.getElementById('campaign-modal-title-text').textContent = 'Edit Campaign';
280
+ this._populateFormFromCampaign(campaign, true);
281
+ this._getEditorModal().show();
282
+ }
283
+
284
+ _openResultsModal(campaign) {
285
+ this._retrySource = campaign;
286
+ const settings = campaign.settings || {};
287
+
288
+ document.getElementById('campaign-results-title-text').textContent =
289
+ `${settings.name || 'Campaign'} — ${campaign.status === 'sent' ? 'Sent' : 'Failed'}`;
132
290
 
133
- // Populate form fields
134
- document.getElementById('event-title').value = event.title || '';
135
- document.getElementById('event-body').value = (event.data && event.data.body) || '';
136
- document.getElementById('event-date').value = event.date || '';
137
- document.getElementById('event-time').value = event.time || '09:00';
138
- document.getElementById('event-duration').value = event.duration || 60;
291
+ // Show retry button for failed campaigns
292
+ document.getElementById('btn-retry-campaign').classList.toggle('d-none', campaign.status !== 'failed');
293
+
294
+ const $body = document.getElementById('campaign-results-body');
295
+ $body.innerHTML = this._renderResultsBody(campaign);
296
+
297
+ this._getResultsModal().show();
298
+ }
299
+
300
+ // ============================================
301
+ // Form Population
302
+ // ============================================
303
+ _populateFormFromCampaign(campaign, isEditing) {
304
+ const settings = campaign.settings || {};
305
+ const d = new Date(campaign.sendAt * 1000);
139
306
 
140
307
  // Type
141
- const typeRadio = document.getElementById(`event-type-${event.type}`);
142
- if (typeRadio) {
143
- typeRadio.checked = true;
144
- }
145
-
146
- // Audience
147
- const audienceType = (event.data && event.data.audience && event.data.audience.type) || 'all';
148
- const audienceRadio = document.getElementById(`audience-${audienceType}`);
149
- if (audienceRadio) {
150
- audienceRadio.checked = true;
151
- }
152
-
153
- // Channels
154
- const channels = (event.data && event.data.channels) || {};
155
- document.getElementById('channel-push').checked = !!channels.push;
156
- document.getElementById('channel-email').checked = !!channels.email;
157
- document.getElementById('channel-sms').checked = !!channels.sms;
158
- document.getElementById('channel-inapp').checked = !!channels.inapp;
159
-
160
- // Status
161
- document.getElementById('event-status').value = event.status || 'draft';
162
-
163
- // Color
164
- this._setColor(event.color || '#4CAF50');
165
-
166
- this._getModal().show();
167
- }
168
-
169
- // ============================================
170
- // Data Extraction
171
- // ============================================
172
- _extractEventData(data) {
173
- const event = data.event || {};
174
- const eventData = event.data || {};
175
-
176
- return {
177
- title: event.title || 'Untitled',
178
- type: event.type || 'newsletter',
179
- date: event.date || '',
180
- time: event.time || '09:00',
181
- duration: parseInt(event.duration, 10) || 60,
182
- status: event.status || 'draft',
183
- color: event.color || '#4CAF50',
184
- data: {
185
- body: eventData.body || '',
186
- audience: {
187
- type: (eventData.audience && eventData.audience.type) || 'all',
188
- },
189
- channels: {
190
- push: !!(eventData.channels && eventData.channels.push),
191
- email: !!(eventData.channels && eventData.channels.email),
192
- sms: !!(eventData.channels && eventData.channels.sms),
193
- inapp: !!(eventData.channels && eventData.channels.inapp),
194
- },
195
- },
308
+ this._setType(campaign.type || 'email');
309
+
310
+ // Shared fields
311
+ document.getElementById('campaign-name').value = settings.name || '';
312
+ document.getElementById('campaign-subject').value = settings.subject || '';
313
+ document.getElementById('campaign-date').value = this.core.formatDate(d);
314
+ document.getElementById('campaign-time').value =
315
+ String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
316
+ document.getElementById('campaign-discount-code').value = settings.discountCode || '';
317
+ document.getElementById('campaign-test').checked = !!settings.test;
318
+
319
+ // Targeting
320
+ document.getElementById('campaign-all').checked = !!settings.all;
321
+ document.getElementById('campaign-segments').value = (settings.segments || []).join(', ');
322
+ document.getElementById('campaign-exclude-segments').value = (settings.excludeSegments || []).join(', ');
323
+
324
+ // Email fields
325
+ document.getElementById('campaign-preheader').value = settings.preheader || '';
326
+ document.getElementById('campaign-content').value = settings.content || '';
327
+ document.getElementById('campaign-template').value = settings.template || 'default';
328
+ document.getElementById('campaign-sender').value = settings.sender || 'marketing';
329
+
330
+ // Push fields
331
+ document.getElementById('campaign-icon').value = settings.icon || '';
332
+ document.getElementById('campaign-click-action').value = settings.clickAction || '';
333
+ document.getElementById('campaign-push-tags').value =
334
+ (settings.filters && settings.filters.tags) ? settings.filters.tags.join(', ') : '';
335
+ document.getElementById('campaign-push-owner').value =
336
+ (settings.filters && settings.filters.owner) || '';
337
+
338
+ // Lists
339
+ document.getElementById('campaign-lists').value = (settings.lists || []).join(', ');
340
+
341
+ // Providers (checkboxes)
342
+ const providers = settings.providers || [];
343
+ document.getElementById('campaign-provider-sendgrid').checked = providers.includes('sendgrid');
344
+ document.getElementById('campaign-provider-beehiiv').checked = providers.includes('beehiiv');
345
+
346
+ // Advanced
347
+ document.getElementById('campaign-group').value = settings.group || '';
348
+ document.getElementById('campaign-categories').value = (settings.categories || []).join(', ');
349
+
350
+ // UTM
351
+ const utm = settings.utm || {};
352
+ const utmFields = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
353
+ utmFields.forEach((key) => {
354
+ const $input = document.querySelector(`input[name="campaign.utm.${key}"]`);
355
+ if ($input) {
356
+ $input.value = utm[key] || '';
357
+ }
358
+ });
359
+
360
+ // Recurrence
361
+ const recurrence = campaign.recurrence;
362
+ const $recurringCheckbox = document.getElementById('campaign-recurring');
363
+ const $recurrenceFields = document.getElementById('recurrence-fields');
364
+
365
+ if (recurrence) {
366
+ $recurringCheckbox.checked = true;
367
+ $recurrenceFields.classList.remove('d-none');
368
+ document.getElementById('campaign-recurrence-pattern').value = recurrence.pattern || 'monthly';
369
+ document.getElementById('campaign-recurrence-hour').value = recurrence.hour || 0;
370
+ document.getElementById('campaign-recurrence-day').value = recurrence.day || 1;
371
+ document.getElementById('campaign-recurrence-month').value = recurrence.month || 1;
372
+
373
+ // Show month row if yearly
374
+ document.getElementById('recurrence-month-row').classList.toggle('d-none', recurrence.pattern !== 'yearly');
375
+
376
+ // Update day hint
377
+ const $dayHint = document.getElementById('recurrence-day-hint');
378
+ $dayHint.textContent = recurrence.pattern === 'weekly' ? '(of week, 0=Sun)' : '(of month)';
379
+ } else {
380
+ $recurringCheckbox.checked = false;
381
+ $recurrenceFields.classList.add('d-none');
382
+ }
383
+ }
384
+
385
+ _setType(type) {
386
+ const $radio = document.getElementById(`campaign-type-${type}`);
387
+ if ($radio) {
388
+ $radio.checked = true;
389
+ $radio.dispatchEvent(new Event('change'));
390
+ }
391
+ }
392
+
393
+ // ============================================
394
+ // Payload Building
395
+ // ============================================
396
+ _buildPayload(formData) {
397
+ const c = formData.campaign || {};
398
+ const type = c.type || 'email';
399
+
400
+ // Convert date + time to sendAt
401
+ const sendAt = this._dateTimeToISO(c.date, c.time);
402
+
403
+ const payload = {
404
+ type,
405
+ name: (c.name || '').trim(),
406
+ subject: (c.subject || '').trim(),
407
+ sendAt,
196
408
  };
409
+
410
+ // Config
411
+ if (c.discountCode) {
412
+ payload.discountCode = c.discountCode.trim();
413
+ }
414
+ if (c.test) {
415
+ payload.test = true;
416
+ }
417
+
418
+ // Providers (checkboxes → array)
419
+ const providersCb = c.providers || {};
420
+ const providers = Object.keys(providersCb).filter((p) => providersCb[p]);
421
+ if (providers.length) {
422
+ payload.providers = providers;
423
+ }
424
+
425
+ // Targeting
426
+ if (c.all) {
427
+ payload.all = true;
428
+ }
429
+ const lists = this._csvToArray(c.lists);
430
+ if (lists.length) {
431
+ payload.lists = lists;
432
+ }
433
+ const segments = this._csvToArray(c.segments);
434
+ if (segments.length) {
435
+ payload.segments = segments;
436
+ }
437
+ const excludeSegments = this._csvToArray(c.excludeSegments);
438
+ if (excludeSegments.length) {
439
+ payload.excludeSegments = excludeSegments;
440
+ }
441
+
442
+ // Email-specific
443
+ if (type === 'email') {
444
+ if (c.preheader) {
445
+ payload.preheader = c.preheader.trim();
446
+ }
447
+ if (c.content) {
448
+ payload.content = c.content;
449
+ }
450
+ if (c.template && c.template !== 'default') {
451
+ payload.template = c.template;
452
+ }
453
+ if (c.sender) {
454
+ payload.sender = c.sender;
455
+ }
456
+ }
457
+
458
+ // Push-specific
459
+ if (type === 'push') {
460
+ if (c.icon) {
461
+ payload.icon = c.icon.trim();
462
+ }
463
+ if (c.clickAction) {
464
+ payload.clickAction = c.clickAction.trim();
465
+ }
466
+ const filters = c.filters || {};
467
+ const tags = this._csvToArray(filters.tags);
468
+ if (tags.length || filters.owner) {
469
+ payload.filters = {};
470
+ if (tags.length) {
471
+ payload.filters.tags = tags;
472
+ }
473
+ if (filters.owner) {
474
+ payload.filters.owner = filters.owner.trim();
475
+ }
476
+ }
477
+ }
478
+
479
+ // Advanced
480
+ if (c.group) {
481
+ payload.group = c.group.trim();
482
+ }
483
+ const categories = this._csvToArray(c.categories);
484
+ if (categories.length) {
485
+ payload.categories = categories;
486
+ }
487
+
488
+ // UTM
489
+ const utm = c.utm || {};
490
+ const utmClean = {};
491
+ let hasUtm = false;
492
+ Object.entries(utm).forEach(([key, val]) => {
493
+ if (val && val.trim()) {
494
+ utmClean[key] = val.trim();
495
+ hasUtm = true;
496
+ }
497
+ });
498
+ if (hasUtm) {
499
+ payload.utm = utmClean;
500
+ }
501
+
502
+ // Recurrence
503
+ if (c.recurring) {
504
+ const rec = c.recurrence || {};
505
+ payload.recurrence = {
506
+ pattern: rec.pattern || 'monthly',
507
+ hour: parseInt(rec.hour, 10) || 0,
508
+ day: parseInt(rec.day, 10) || 1,
509
+ };
510
+ if (rec.pattern === 'yearly') {
511
+ payload.recurrence.month = parseInt(rec.month, 10) || 1;
512
+ }
513
+ }
514
+
515
+ return payload;
516
+ }
517
+
518
+ _dateTimeToISO(date, time) {
519
+ if (!date || !time) {
520
+ return 'now';
521
+ }
522
+ return `${date}T${time}:00`;
523
+ }
524
+
525
+ _csvToArray(str) {
526
+ if (!str) {
527
+ return [];
528
+ }
529
+ // Handle both string and array inputs
530
+ if (Array.isArray(str)) {
531
+ return str;
532
+ }
533
+ return str.split(',').map((s) => s.trim()).filter(Boolean);
197
534
  }
198
535
 
199
536
  // ============================================
200
- // UI Helpers
537
+ // BEM API Calls
201
538
  // ============================================
202
- _setColor(color) {
203
- document.getElementById('event-color').value = color;
539
+ async _createCampaign(payload) {
540
+ const url = `${this.webManager.getApiUrl()}/backend-manager/marketing/campaign`;
541
+ const response = await authorizedFetch(url, {
542
+ method: 'POST',
543
+ timeout: 60000,
544
+ response: 'json',
545
+ tries: 1,
546
+ log: true,
547
+ body: payload,
548
+ });
549
+
550
+ this.formManager.showSuccess('Campaign created');
551
+
552
+ // Track
553
+ this._trackCampaignAction('create', payload);
554
+
555
+ return response;
556
+ }
557
+
558
+ async _updateCampaign(id, payload) {
559
+ payload.id = id;
560
+ const url = `${this.webManager.getApiUrl()}/backend-manager/marketing/campaign`;
561
+ const response = await authorizedFetch(url, {
562
+ method: 'PUT',
563
+ timeout: 60000,
564
+ response: 'json',
565
+ tries: 1,
566
+ log: true,
567
+ body: payload,
568
+ });
569
+
570
+ this.formManager.showSuccess('Campaign updated');
571
+
572
+ // Track
573
+ this._trackCampaignAction('update', payload);
204
574
 
205
- document.querySelectorAll('.color-swatch').forEach(($swatch) => {
206
- $swatch.classList.toggle('active', $swatch.dataset.color === color);
575
+ return response;
576
+ }
577
+
578
+ async _deleteCampaign(id) {
579
+ const url = `${this.webManager.getApiUrl()}/backend-manager/marketing/campaign`;
580
+ const response = await authorizedFetch(url, {
581
+ method: 'DELETE',
582
+ timeout: 60000,
583
+ response: 'json',
584
+ tries: 1,
585
+ log: true,
586
+ body: { id },
207
587
  });
588
+
589
+ // Track
590
+ this._trackCampaignAction('delete', { id });
591
+
592
+ return response;
208
593
  }
209
594
 
595
+ /**
596
+ * Reschedule a campaign (drag-and-drop)
597
+ */
598
+ async rescheduleCampaign(id, newSendAt) {
599
+ const url = `${this.webManager.getApiUrl()}/backend-manager/marketing/campaign`;
600
+ return authorizedFetch(url, {
601
+ method: 'PUT',
602
+ timeout: 60000,
603
+ response: 'json',
604
+ tries: 1,
605
+ log: true,
606
+ body: { id, sendAt: newSendAt },
607
+ });
608
+ }
609
+
610
+ // ============================================
611
+ // Results Rendering
612
+ // ============================================
613
+ _renderResultsBody(campaign) {
614
+ const settings = campaign.settings || {};
615
+ const results = campaign.results || {};
616
+ const d = new Date(campaign.sendAt * 1000);
617
+
618
+ let html = '';
619
+
620
+ // Status badge
621
+ const statusBadge = campaign.status === 'sent'
622
+ ? `<span class="badge bg-success">${getPrerenderedIcon('circle-check', 'fa-xs me-1')} Sent</span>`
623
+ : `<span class="badge bg-danger">${getPrerenderedIcon('triangle-exclamation', 'fa-xs me-1')} Failed</span>`;
624
+
625
+ // Overview
626
+ html += '<div class="mb-4">';
627
+ html += `<div class="d-flex align-items-center gap-2 mb-2">`;
628
+ html += statusBadge;
629
+ html += `<span class="badge bg-${campaign.type === 'email' ? 'primary' : 'success'}">${campaign.type === 'email' ? 'Email' : 'Push'}</span>`;
630
+ html += `</div>`;
631
+ html += `<table class="table table-sm table-borderless mb-0">`;
632
+ html += `<tr><td class="text-muted" style="width:120px">Name</td><td>${escapeHtml(settings.name || '')}</td></tr>`;
633
+ html += `<tr><td class="text-muted">Subject</td><td>${escapeHtml(settings.subject || '')}</td></tr>`;
634
+ html += `<tr><td class="text-muted">Sent At</td><td>${d.toLocaleString()}</td></tr>`;
635
+
636
+ if (campaign.type === 'email') {
637
+ if (settings.preheader) {
638
+ html += `<tr><td class="text-muted">Preheader</td><td>${escapeHtml(settings.preheader)}</td></tr>`;
639
+ }
640
+ if (settings.sender) {
641
+ html += `<tr><td class="text-muted">Sender</td><td>${escapeHtml(settings.sender)}</td></tr>`;
642
+ }
643
+ if (settings.template) {
644
+ html += `<tr><td class="text-muted">Template</td><td>${escapeHtml(settings.template)}</td></tr>`;
645
+ }
646
+ }
647
+
648
+ if (campaign.type === 'push') {
649
+ if (settings.icon) {
650
+ html += `<tr><td class="text-muted">Icon</td><td><a href="${escapeHtml(settings.icon)}" target="_blank" rel="noopener">${escapeHtml(settings.icon)}</a></td></tr>`;
651
+ }
652
+ if (settings.clickAction) {
653
+ html += `<tr><td class="text-muted">Click URL</td><td><a href="${escapeHtml(settings.clickAction)}" target="_blank" rel="noopener">${escapeHtml(settings.clickAction)}</a></td></tr>`;
654
+ }
655
+ }
656
+
657
+ if (campaign.recurringId) {
658
+ html += `<tr><td class="text-muted">Recurring</td><td>${escapeHtml(campaign.recurringId)}</td></tr>`;
659
+ }
660
+
661
+ html += `</table>`;
662
+ html += '</div>';
663
+
664
+ // Content (email only)
665
+ if (campaign.type === 'email' && settings.content) {
666
+ html += '<div class="mb-4">';
667
+ html += '<h6>Content</h6>';
668
+ html += `<pre class="bg-body-tertiary p-3 rounded small" style="white-space:pre-wrap;max-height:200px;overflow-y:auto">${escapeHtml(settings.content)}</pre>`;
669
+ html += '</div>';
670
+ }
671
+
672
+ // Results
673
+ if (Object.keys(results).length > 0) {
674
+ html += '<div class="mb-3">';
675
+ html += '<h6>Results</h6>';
676
+ html += `<pre class="bg-body-tertiary p-3 rounded small" style="white-space:pre-wrap;max-height:300px;overflow-y:auto">${escapeHtml(JSON.stringify(results, null, 2))}</pre>`;
677
+ html += '</div>';
678
+ }
679
+
680
+ return html;
681
+ }
682
+
683
+ // ============================================
684
+ // UI Helpers
685
+ // ============================================
210
686
  _toggleDeleteButton(show) {
211
- document.getElementById('btn-delete-event').classList.toggle('d-none', !show);
687
+ document.getElementById('btn-delete-campaign').classList.toggle('d-none', !show);
688
+ }
689
+
690
+ _resetRecurrence() {
691
+ document.getElementById('campaign-recurring').checked = false;
692
+ document.getElementById('recurrence-fields').classList.add('d-none');
693
+ document.getElementById('campaign-recurrence-pattern').value = 'monthly';
694
+ document.getElementById('campaign-recurrence-hour').value = '14';
695
+ document.getElementById('campaign-recurrence-day').value = '1';
696
+ document.getElementById('campaign-recurrence-month').value = '1';
697
+ document.getElementById('recurrence-month-row').classList.add('d-none');
698
+ }
699
+
700
+ // ============================================
701
+ // Tracking
702
+ // ============================================
703
+ _trackCampaignAction(action, payload) {
704
+ gtag('event', `campaign_${action}`, {
705
+ campaign_type: payload.type || 'unknown',
706
+ campaign_name: payload.name || '',
707
+ });
708
+
709
+ fbq('trackCustom', `AdminCampaign${action.charAt(0).toUpperCase() + action.slice(1)}`, {
710
+ type: payload.type || 'unknown',
711
+ name: payload.name || '',
712
+ });
713
+
714
+ ttq.track('SubmitForm', {
715
+ content_id: `admin-campaign-${action}`,
716
+ content_type: 'product',
717
+ content_name: `Admin Campaign ${action}`,
718
+ });
212
719
  }
213
720
  }