@o2vend/theme-cli 1.0.36 → 1.0.38

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 (90) hide show
  1. package/README.md +4 -0
  2. package/lib/lib/dev-server.js +344 -48
  3. package/lib/lib/liquid-engine.js +3 -1
  4. package/lib/lib/mock-data.js +473 -119
  5. package/lib/lib/widget-service.js +12 -4
  6. package/package.json +2 -2
  7. package/test-theme/assets/async-sections.js +32 -24
  8. package/test-theme/assets/cart-drawer.js +20 -22
  9. package/test-theme/assets/cart-manager.js +1 -15
  10. package/test-theme/assets/checkout-price-handler.js +12 -11
  11. package/test-theme/assets/checkout.css +1415 -0
  12. package/test-theme/assets/checkout.js +3174 -0
  13. package/test-theme/assets/components.css +178 -29
  14. package/test-theme/assets/delivery-zone.js +1 -1
  15. package/test-theme/assets/product-detail.css +1050 -0
  16. package/test-theme/assets/product-detail.js +2940 -0
  17. package/test-theme/assets/theme.css +95 -120
  18. package/test-theme/assets/theme.js +781 -186
  19. package/test-theme/layout/theme.liquid +91 -17
  20. package/test-theme/sections/content.liquid +64 -57
  21. package/test-theme/sections/footer-fallback.liquid +57 -7
  22. package/test-theme/sections/footer.liquid +63 -12
  23. package/test-theme/sections/header-fallback.liquid +41 -41
  24. package/test-theme/sections/header.liquid +41 -51
  25. package/test-theme/sections/hero-fallback.liquid +1 -1
  26. package/test-theme/sections/hero.liquid +159 -136
  27. package/test-theme/snippets/account-sidebar.liquid +121 -29
  28. package/test-theme/snippets/add-to-cart-modal.liquid +258 -206
  29. package/test-theme/snippets/breadcrumbs.liquid +98 -11
  30. package/test-theme/snippets/cart-drawer.liquid +93 -0
  31. package/test-theme/snippets/delivery-zone-city-selector.liquid +101 -15
  32. package/test-theme/snippets/delivery-zone-modal.liquid +529 -84
  33. package/test-theme/snippets/delivery-zone-search.liquid +104 -18
  34. package/test-theme/snippets/login-modal.liquid +269 -82
  35. package/test-theme/snippets/mega-menu.liquid +130 -43
  36. package/test-theme/snippets/news-thumbnail.liquid +120 -28
  37. package/test-theme/snippets/pagination.liquid +1 -1
  38. package/test-theme/snippets/price.liquid +100 -9
  39. package/test-theme/snippets/product-card-related.liquid +22 -4
  40. package/test-theme/snippets/product-card-simple.liquid +521 -25
  41. package/test-theme/snippets/product-card.liquid +145 -232
  42. package/test-theme/snippets/rating.liquid +100 -9
  43. package/test-theme/snippets/skeleton-collection-grid.liquid +94 -8
  44. package/test-theme/snippets/skeleton-product-card.liquid +102 -16
  45. package/test-theme/snippets/skeleton-product-grid.liquid +87 -1
  46. package/test-theme/snippets/social-sharing.liquid +133 -32
  47. package/test-theme/templates/account/dashboard.liquid +30 -0
  48. package/test-theme/templates/account/loyalty-redemption.liquid +29 -28
  49. package/test-theme/templates/account/loyalty.liquid +45 -43
  50. package/test-theme/templates/account/order-detail.liquid +15 -8
  51. package/test-theme/templates/account/orders.liquid +189 -35
  52. package/test-theme/templates/account/profile.liquid +509 -114
  53. package/test-theme/templates/account/register.liquid +18 -8
  54. package/test-theme/templates/account/return-orders.liquid +31 -30
  55. package/test-theme/templates/account/store-credit.liquid +27 -26
  56. package/test-theme/templates/account/subscriptions.liquid +22 -5
  57. package/test-theme/templates/account/wishlist.liquid +88 -19
  58. package/test-theme/templates/address-book.liquid +166 -69
  59. package/test-theme/templates/categories.liquid +90 -30
  60. package/test-theme/templates/checkout.liquid +137 -3834
  61. package/test-theme/templates/error.liquid +23 -21
  62. package/test-theme/templates/index.liquid +29 -0
  63. package/test-theme/templates/login.liquid +33 -6
  64. package/test-theme/templates/order-confirmation.liquid +67 -9
  65. package/test-theme/templates/page.liquid +418 -206
  66. package/test-theme/templates/product-detail.liquid +124 -3878
  67. package/test-theme/templates/products.liquid +155 -30
  68. package/test-theme/templates/search.liquid +739 -225
  69. package/test-theme/widgets/brand-carousel.liquid +102 -82
  70. package/test-theme/widgets/brand.liquid +78 -50
  71. package/test-theme/widgets/carousel.liquid +253 -121
  72. package/test-theme/widgets/category-list-carousel.liquid +32 -8
  73. package/test-theme/widgets/category-list.liquid +21 -6
  74. package/test-theme/widgets/category.liquid +104 -37
  75. package/test-theme/widgets/discount-time.liquid +326 -119
  76. package/test-theme/widgets/footer-menu.liquid +115 -23
  77. package/test-theme/widgets/footer.liquid +118 -5
  78. package/test-theme/widgets/gallery.liquid +29 -5
  79. package/test-theme/widgets/header-menu.liquid +25 -13
  80. package/test-theme/widgets/header.liquid +64 -26
  81. package/test-theme/widgets/html.liquid +29 -6
  82. package/test-theme/widgets/news.liquid +6 -0
  83. package/test-theme/widgets/product-canvas.liquid +20 -12
  84. package/test-theme/widgets/product-carousel.liquid +118 -56
  85. package/test-theme/widgets/shared/product-grid.liquid +12 -0
  86. package/test-theme/widgets/single-product.liquid +688 -250
  87. package/test-theme/widgets/spacebar-carousel.liquid +39 -10
  88. package/test-theme/widgets/spacebar.liquid +77 -6
  89. package/test-theme/widgets/splash.liquid +40 -30
  90. package/test-theme/widgets/testimonial-carousel.liquid +111 -67
@@ -0,0 +1,2940 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ // Get product data - prefer global PRODUCT_DATA, fallback to DOM element
5
+ let productData = {};
6
+ if (typeof PRODUCT_DATA !== 'undefined' && PRODUCT_DATA) {
7
+ productData = PRODUCT_DATA;
8
+ } else {
9
+ const productDataEl = document.getElementById('productData');
10
+ productData = productDataEl ? JSON.parse(productDataEl.textContent) : {};
11
+ }
12
+
13
+ // Ensure productData has required arrays initialized
14
+ if (!productData.combinations) {
15
+ productData.combinations = [];
16
+ }
17
+ if (!productData.variants && !productData.variations) {
18
+ // Try to get from global PRODUCT_VARIANTS
19
+ if (typeof PRODUCT_VARIANTS !== 'undefined' && PRODUCT_VARIANTS) {
20
+ productData.variants = PRODUCT_VARIANTS;
21
+ productData.variations = PRODUCT_VARIANTS;
22
+ } else {
23
+ productData.variants = [];
24
+ productData.variations = [];
25
+ }
26
+ } else if (!productData.variants && productData.variations) {
27
+ productData.variants = productData.variations;
28
+ } else if (!productData.variations && productData.variants) {
29
+ productData.variations = productData.variants;
30
+ }
31
+ if (!productData.subscriptions) {
32
+ productData.subscriptions = [];
33
+ }
34
+
35
+ // Process combinations to add group metadata
36
+ if (productData.combinations && Array.isArray(productData.combinations)) {
37
+ productData.combinations.forEach(c => {
38
+ try {
39
+ const gd = c.group?.groupDetail;
40
+
41
+ c.groupName = gd?.groupName || "Default";
42
+ c.minimumSelectable = gd?.minimumSelectable || 1;
43
+ c.maximumSelectable = gd?.maximumSelectable || 1;
44
+ c.isOptional = gd?.isOptional || false;
45
+ } catch (err) {
46
+ c.groupName = "Default";
47
+ c.minimumSelectable = 1;
48
+ c.maximumSelectable = 1;
49
+ c.isOptional = false;
50
+ }
51
+ });
52
+ }
53
+ // Product state
54
+ let selectedOptions = {};
55
+ let currentVariant = null;
56
+ let currentImageIndex = 0;
57
+ let baseMainImageUrls = [];
58
+ let baseThumbnailImageUrls = [];
59
+ let moneyFormat = typeof SHOP_CURRENCY_SYMBOL !== 'undefined' ? SHOP_CURRENCY_SYMBOL : '₹';
60
+ // DOM elements
61
+ let mainImages, thumbnails, productForm, addToCartBtn, quantityInput, priceElement, priceCompareElement;
62
+ let optionsContainer, galleryModal, galleryModalImage, galleryModalClose;
63
+ let galleryModalPrev, galleryModalNext, galleryModalCounter, galleryZoomBtn, cartMessage;
64
+
65
+ // Helper function to format money
66
+ function formatMoney(cents) {
67
+ return moneyFormat + (cents).toFixed(2);
68
+ }
69
+
70
+ function isCallForPricingEnabled(item) {
71
+ if (!item) return false;
72
+ const value = item.showCallForPricing;
73
+ return value === true || value === 'true' || value === 1 || value === '1';
74
+ }
75
+
76
+ function normalizeImageUrl(image) {
77
+ if (!image) return '';
78
+ if (typeof image === 'string') return image;
79
+ if (typeof image !== 'object') return '';
80
+ return image.url || image.Url || image.src || image.Src || image.imageUrl || image.ImageUrl || '';
81
+ }
82
+
83
+ function refreshGalleryElements() {
84
+ mainImages = document.querySelectorAll('.gallery-main-image');
85
+ thumbnails = document.querySelectorAll('.gallery-thumbnail');
86
+ }
87
+
88
+ function renderGalleryImages(imageUrls) {
89
+ const urls = (imageUrls || []).filter(Boolean);
90
+ if (urls.length === 0) return;
91
+
92
+ const galleryMain = document.querySelector('.gallery-main');
93
+ if (!galleryMain) return;
94
+
95
+ // Remove existing gallery images/placeholder and rebuild from the active image set.
96
+ galleryMain.querySelectorAll('.gallery-main-image, .gallery-placeholder').forEach(node => node.remove());
97
+
98
+ const zoomBtn = document.getElementById('galleryZoomBtn');
99
+ const firstChildAfterImages = zoomBtn || null;
100
+ const productName = productData?.name || productData?.title || 'Product';
101
+
102
+ urls.forEach((url, index) => {
103
+ const img = document.createElement('img');
104
+ img.src = url;
105
+ img.alt = `${productName} - ${index + 1}`;
106
+ img.className = `gallery-main-image${index === 0 ? ' active' : ''}`;
107
+ img.dataset.index = String(index);
108
+ img.loading = index === 0 ? 'eager' : 'lazy';
109
+ img.decoding = 'async';
110
+ if (index === 0) {
111
+ img.setAttribute('fetchpriority', 'high');
112
+ }
113
+ if (index === 0) {
114
+ img.id = 'mainProductImage';
115
+ }
116
+ galleryMain.insertBefore(img, firstChildAfterImages);
117
+ });
118
+
119
+ let thumbnailsContainer = document.getElementById('galleryThumbnails');
120
+ if (urls.length > 1) {
121
+ if (!thumbnailsContainer) {
122
+ thumbnailsContainer = document.createElement('div');
123
+ thumbnailsContainer.id = 'galleryThumbnails';
124
+ thumbnailsContainer.className = 'gallery-thumbnails';
125
+ const galleryParent = galleryMain.parentElement;
126
+ if (galleryParent) {
127
+ galleryParent.appendChild(thumbnailsContainer);
128
+ }
129
+ }
130
+
131
+ thumbnailsContainer.innerHTML = '';
132
+ urls.slice(0, 8).forEach((url, index) => {
133
+ const thumb = document.createElement('button');
134
+ thumb.type = 'button';
135
+ thumb.className = `gallery-thumbnail${index === 0 ? ' active' : ''}`;
136
+ thumb.dataset.image = url;
137
+ thumb.dataset.index = String(index);
138
+ thumb.setAttribute('aria-label', `View image ${index + 1}`);
139
+
140
+ const thumbImg = document.createElement('img');
141
+ thumbImg.src = url;
142
+ thumbImg.alt = `${productName} - ${index + 1}`;
143
+ thumbImg.loading = 'lazy';
144
+
145
+ thumb.appendChild(thumbImg);
146
+ thumbnailsContainer.appendChild(thumb);
147
+ });
148
+ } else if (thumbnailsContainer) {
149
+ thumbnailsContainer.remove();
150
+ }
151
+
152
+ refreshGalleryElements();
153
+ switchToImage(0);
154
+ }
155
+
156
+ function restoreBaseGalleryImages() {
157
+ const baseUrls = baseMainImageUrls.length > 0 ? baseMainImageUrls : baseThumbnailImageUrls;
158
+ if (baseUrls.length === 0) return;
159
+ renderGalleryImages(baseUrls);
160
+ }
161
+
162
+ function applyVariantGalleryImages(images) {
163
+ const variantUrls = (images || []).map(normalizeImageUrl).filter(Boolean);
164
+ if (variantUrls.length === 0) {
165
+ restoreBaseGalleryImages();
166
+ return;
167
+ }
168
+ renderGalleryImages(variantUrls);
169
+ }
170
+
171
+ // Consolidated helper: Parse additionalData
172
+ function parseAdditionalData(additionalData) {
173
+ if (!additionalData) return {};
174
+ try {
175
+ return typeof additionalData === "string"
176
+ ? JSON.parse(additionalData)
177
+ : additionalData;
178
+ } catch (e) {
179
+ console.warn("Error parsing additionalData:", e);
180
+ return {};
181
+ }
182
+ }
183
+
184
+ // Consolidated helper: Format date
185
+ function formatDate(date) {
186
+ const year = date.getFullYear();
187
+ const month = String(date.getMonth() + 1).padStart(2, '0');
188
+ const day = String(date.getDate()).padStart(2, '0');
189
+ return `${year}-${month}-${day}`;
190
+ }
191
+
192
+ // Consolidated helper: Get subscription settings
193
+ function getSubscriptionSettings(addData) {
194
+ const isScheduleByCustomer = addData.settings?.isScheduleByCustomer ?? true;
195
+ const isProductChoiceEnabled = addData.settings?.isProductChoiceEnabled ?? true;
196
+ const allProductsRequired = !isScheduleByCustomer || !isProductChoiceEnabled;
197
+ const hasPredefinedFrequency = !isScheduleByCustomer && (addData.frequency || addData.frequencyData);
198
+
199
+ return {
200
+ isScheduleByCustomer,
201
+ isProductChoiceEnabled,
202
+ allProductsRequired,
203
+ hasPredefinedFrequency,
204
+ predefinedFrequency: addData.frequency || {},
205
+ predefinedFrequencyData: addData.frequencyData || {}
206
+ };
207
+ }
208
+
209
+ let bundleSelections = {};
210
+ let selectedSubscription = null;
211
+
212
+ // Helper function to show subscription validation errors
213
+ function showSubscriptionError(message) {
214
+ const errorContainer = document.getElementById("subscriptionValidationMsg");
215
+ if (errorContainer) {
216
+ errorContainer.textContent = message;
217
+ errorContainer.style.display = message ? "block" : "none";
218
+ }
219
+ }
220
+
221
+ // Helper function to clear subscription errors
222
+ function clearSubscriptionError() {
223
+ showSubscriptionError("");
224
+ }
225
+
226
+ // Helper function to show combination validation errors
227
+ function showCombinationError(message) {
228
+ const errorContainer = document.getElementById("combinationValidationMsg");
229
+ if (errorContainer) {
230
+ errorContainer.textContent = message;
231
+ errorContainer.style.display = message ? "block" : "none";
232
+ // Auto-hide after 5 seconds
233
+ if (message) {
234
+ setTimeout(() => {
235
+ showCombinationError("");
236
+ }, 5000);
237
+ }
238
+ }
239
+ }
240
+
241
+ // Helper function to calculate shipping start date based on shippingDays only
242
+ function calculateShippingStartDate(shippingDays) {
243
+ const now = new Date();
244
+ const shippingStartDate = new Date();
245
+
246
+ // Simply add shippingDays to today's date
247
+ // If shippingDays = 1, start date = tomorrow
248
+ // If shippingDays = 2, start date = today + 2 days, etc.
249
+ shippingStartDate.setDate(now.getDate() + shippingDays);
250
+ shippingStartDate.setHours(0, 0, 0, 0);
251
+
252
+ return shippingStartDate;
253
+ }
254
+
255
+ // Helper function to calculate correct end date based on start date, expected orders count, and frequency
256
+ function calculateCorrectEndDate(startDateStr, expectedOrdersCount, addData) {
257
+ if (!startDateStr || !expectedOrdersCount) return null;
258
+
259
+ const startDate = new Date(startDateStr);
260
+ startDate.setHours(0, 0, 0, 0);
261
+
262
+ const predefinedFrequencyData = addData.frequencyData || {};
263
+ const predefinedFrequency = addData.frequency || {};
264
+
265
+ // Get frequency from predefined data
266
+ const freqOption = predefinedFrequency.selectedOption || predefinedFrequencyData.selectedOption || 'daily';
267
+ let freqOptionNormalized = freqOption.toLowerCase();
268
+ let selectedFreq = 'Daily';
269
+
270
+ if (predefinedFrequencyData.isDailyFrequency) {
271
+ selectedFreq = 'Daily';
272
+ } else if (predefinedFrequencyData.isWeeklyFrequency) {
273
+ selectedFreq = 'Weekly';
274
+ } else if (predefinedFrequencyData.isMonthlyFrequency) {
275
+ selectedFreq = 'Monthly';
276
+ } else if (predefinedFrequencyData.isAlterNativeFrequency) {
277
+ selectedFreq = 'Alternate Days';
278
+ }
279
+
280
+ // Get frequency details
281
+ let freqDetails = {};
282
+ if (selectedFreq === 'Weekly') {
283
+ freqDetails.days = predefinedFrequencyData.weeklyFreVals || ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
284
+ } else if (selectedFreq === 'Alternate Days') {
285
+ freqDetails.days = predefinedFrequencyData.weeklyFreVal || 2;
286
+ } else if (selectedFreq === 'Monthly') {
287
+ freqDetails.day = predefinedFrequencyData.day || 1;
288
+ }
289
+
290
+ // Calculate end date based on frequency and expected orders count
291
+ let endDate = new Date(startDate);
292
+
293
+ if (selectedFreq === 'Daily') {
294
+ // Daily: end date = start date + (expectedOrdersCount - 1) days
295
+ endDate.setDate(startDate.getDate() + (expectedOrdersCount - 1));
296
+ } else if (selectedFreq === 'Alternate Days') {
297
+ // Alternate Days: calculate backwards from expected count
298
+ const step = freqDetails.days || 2;
299
+ let currentDate = new Date(startDate);
300
+ let count = 0;
301
+ while (count < expectedOrdersCount) {
302
+ count++;
303
+ if (count < expectedOrdersCount) {
304
+ currentDate.setDate(currentDate.getDate() + step);
305
+ }
306
+ }
307
+ endDate = currentDate;
308
+ } else if (selectedFreq === 'Weekly') {
309
+ // Weekly: find the date that gives us exactly expectedOrdersCount occurrences
310
+ const selectedDays = freqDetails.days || ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
311
+ let currentDate = new Date(startDate);
312
+ let count = 0;
313
+
314
+ // Find the end date that gives us exactly expectedOrdersCount
315
+ while (count < expectedOrdersCount) {
316
+ let day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][currentDate.getDay()];
317
+ if (selectedDays.includes(day)) {
318
+ count++;
319
+ if (count === expectedOrdersCount) {
320
+ endDate = new Date(currentDate);
321
+ break;
322
+ }
323
+ }
324
+ currentDate.setDate(currentDate.getDate() + 1);
325
+
326
+ // Safety check to prevent infinite loop
327
+ if (currentDate.getTime() - startDate.getTime() > 365 * 24 * 60 * 60 * 1000) {
328
+ return null;
329
+ }
330
+ }
331
+ } else if (selectedFreq === 'Monthly') {
332
+ // Monthly: calculate based on day of month
333
+ const dom = freqDetails.day || 1;
334
+ let currentDate = new Date(startDate);
335
+
336
+ // Set to first occurrence of the day in start month
337
+ currentDate.setDate(dom);
338
+ if (currentDate < startDate) {
339
+ currentDate.setMonth(currentDate.getMonth() + 1);
340
+ currentDate.setDate(dom);
341
+ }
342
+
343
+ let count = 0;
344
+ while (count < expectedOrdersCount) {
345
+ const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
346
+ const validDay = Math.min(dom, lastDayOfMonth);
347
+ const validDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), validDay);
348
+
349
+ if (validDate >= startDate) {
350
+ count++;
351
+ if (count === expectedOrdersCount) {
352
+ endDate = validDate;
353
+ break;
354
+ }
355
+ }
356
+
357
+ currentDate.setMonth(currentDate.getMonth() + 1);
358
+ currentDate.setDate(validDay);
359
+
360
+ // Safety check
361
+ if (currentDate.getTime() - startDate.getTime() > 365 * 24 * 60 * 60 * 1000) {
362
+ return null;
363
+ }
364
+ }
365
+ }
366
+
367
+ return endDate;
368
+ }
369
+
370
+ // Helper function to parse time string (e.g., "14:30", "2:30 PM", or ISO date string)
371
+ function parseTimeString(timeStr) {
372
+ if (!timeStr) return null;
373
+
374
+ // Handle ISO date strings (e.g., "1970-01-01T07:32:00+00:00" or "1970-01-01T07:32:00.000Z")
375
+ if (timeStr.includes('T') || timeStr.includes('Z') || timeStr.includes('+')) {
376
+ try {
377
+ const date = new Date(timeStr);
378
+ if (!isNaN(date.getTime())) {
379
+ return {
380
+ hours: date.getUTCHours(),
381
+ minutes: date.getUTCMinutes()
382
+ };
383
+ }
384
+ } catch (e) {
385
+ // Fall through to other parsing methods
386
+ }
387
+ }
388
+
389
+ // Try HH:MM format first
390
+ const match24 = timeStr.match(/^(\d{1,2}):(\d{2})$/);
391
+ if (match24) {
392
+ return {
393
+ hours: parseInt(match24[1], 10),
394
+ minutes: parseInt(match24[2], 10)
395
+ };
396
+ }
397
+
398
+ // Try 12-hour format with AM/PM
399
+ const match12 = timeStr.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
400
+ if (match12) {
401
+ let hours = parseInt(match12[1], 10);
402
+ const minutes = parseInt(match12[2], 10);
403
+ const ampm = match12[3].toUpperCase();
404
+
405
+ if (ampm === 'PM' && hours !== 12) hours += 12;
406
+ if (ampm === 'AM' && hours === 12) hours = 0;
407
+
408
+ return { hours, minutes };
409
+ }
410
+
411
+ return null;
412
+ }
413
+
414
+ // Helper function to format time for display
415
+ function formatTimeDisplay(timeStr) {
416
+ const time = parseTimeString(timeStr);
417
+ if (!time) return timeStr;
418
+
419
+ const hours = time.hours;
420
+ const minutes = time.minutes;
421
+ const ampm = hours >= 12 ? 'PM' : 'AM';
422
+ const displayHours = hours % 12 || 12;
423
+ const displayMinutes = minutes.toString().padStart(2, '0');
424
+
425
+ return `${displayHours}:${displayMinutes} ${ampm}`;
426
+ }
427
+
428
+ // Initialize shipping methods dropdown
429
+ function initializeShippingMethods() {
430
+ const shippingSelect = document.getElementById('shippingMethod');
431
+ const shippingDetailsDiv = document.getElementById('shippingMethodDetails');
432
+
433
+ if (!shippingSelect) return;
434
+
435
+ // Clear existing options except "Select"
436
+ while (shippingSelect.options.length > 1) {
437
+ shippingSelect.remove(1);
438
+ }
439
+
440
+ // Get shipping methods from productData
441
+ const shippingMethods = productData.shippingMethods || [];
442
+
443
+ if (shippingMethods.length === 0) {
444
+ // No shipping methods available
445
+ shippingSelect.innerHTML = '<option value="">No shipping methods available</option>';
446
+ return;
447
+ }
448
+
449
+ // Populate dropdown with shipping methods
450
+ shippingMethods.forEach((method, index) => {
451
+ try {
452
+ // Parse the data JSON string if it's a string
453
+ let methodData = {};
454
+ if (method.data) {
455
+ if (typeof method.data === 'string') {
456
+ methodData = JSON.parse(method.data);
457
+ } else {
458
+ methodData = method.data;
459
+ }
460
+ }
461
+
462
+ const description = methodData.description || `Shipping Method ${index + 1}`;
463
+ const shippingDays = methodData.shippingDays || 1;
464
+ const deliveryDays = methodData.deliveryDays || 0;
465
+ const shippingTiming = methodData.shippingTiming || '';
466
+ const deliveryTiming = methodData.deliveryTiming || '';
467
+ const orderCutOffTiming = methodData.orderCutOffTiming || '';
468
+
469
+ // Create option element
470
+ const option = document.createElement('option');
471
+ option.value = method.shippingClassId || index;
472
+ option.textContent = description;
473
+ option.dataset.shippingDays = shippingDays;
474
+ option.dataset.deliveryDays = deliveryDays;
475
+ option.dataset.shippingTiming = shippingTiming;
476
+ option.dataset.deliveryTiming = deliveryTiming;
477
+ option.dataset.orderCutOffTiming = orderCutOffTiming;
478
+ option.dataset.methodIndex = index;
479
+
480
+ shippingSelect.appendChild(option);
481
+ } catch (error) {
482
+ console.warn('Error parsing shipping method data:', error, method);
483
+ }
484
+ });
485
+
486
+ // Add change handler to update start date and end date
487
+ shippingSelect.addEventListener('change', () => {
488
+ const selectedOption = this.options[this.selectedIndex];
489
+
490
+ if (!selectedOption || !selectedOption.value) {
491
+ if (shippingDetailsDiv) {
492
+ shippingDetailsDiv.style.display = 'none';
493
+ }
494
+ return;
495
+ }
496
+
497
+ const shippingDays = parseInt(selectedOption.dataset.shippingDays) || 1;
498
+
499
+ // Calculate shipping start date (only based on shippingDays)
500
+ const shippingStartDate = calculateShippingStartDate(shippingDays);
501
+
502
+ // Update start date input field
503
+ const startDateInput = document.getElementById('startDate');
504
+ const endDateInput = document.getElementById('endDate');
505
+
506
+ if (startDateInput) {
507
+ // Temporarily enable if disabled to set value (for predefined subscriptions)
508
+ const wasStartDisabled = startDateInput.disabled;
509
+ const wasEndDisabled = endDateInput ? endDateInput.disabled : false;
510
+
511
+ if (wasStartDisabled) {
512
+ startDateInput.disabled = false;
513
+ }
514
+ startDateInput.value = formatDate(shippingStartDate);
515
+ if (wasStartDisabled) {
516
+ startDateInput.disabled = true;
517
+ }
518
+
519
+ // Check if this is a predefined subscription - if so, calculate end date
520
+ const addData = parseAdditionalData(productData.additionalData);
521
+ const subSettings = getSubscriptionSettings(addData);
522
+ const expectedOrdersCount = subSettings.hasPredefinedFrequency ? (subSettings.predefinedFrequency.ordersCount) : null;
523
+
524
+ // For predefined subscriptions, calculate end date based on start date and ordersCount
525
+ if (expectedOrdersCount !== null && expectedOrdersCount !== undefined && endDateInput) {
526
+ const correctEndDate = calculateCorrectEndDate(
527
+ startDateInput.value,
528
+ expectedOrdersCount,
529
+ addData
530
+ );
531
+
532
+ if (correctEndDate) {
533
+ if (wasEndDisabled) {
534
+ endDateInput.disabled = false;
535
+ }
536
+ endDateInput.value = formatDate(correctEndDate);
537
+ if (wasEndDisabled) {
538
+ endDateInput.disabled = true;
539
+ }
540
+ }
541
+ }
542
+
543
+ // Trigger change event to recalculate deliverables if it's a subscription
544
+ if (typeof calculateDeliverables === 'function') {
545
+ setTimeout(() => {
546
+ calculateDeliverables();
547
+ }, 100);
548
+ }
549
+ }
550
+
551
+ // Hide shipping details div (no need to show delivery info)
552
+ if (shippingDetailsDiv) {
553
+ shippingDetailsDiv.style.display = 'none';
554
+ }
555
+ });
556
+
557
+ // Auto-select shipping method based on shippingClassId for predefined subscriptions
558
+ const addData = parseAdditionalData(productData.additionalData);
559
+ const subSettings = getSubscriptionSettings(addData);
560
+ const shippingClassId = addData.settings?.shippingClassId;
561
+
562
+ // If predefined subscription and shippingClassId exists, auto-select matching shipping method
563
+ if (!subSettings.isScheduleByCustomer && shippingClassId !== null && shippingClassId !== undefined) {
564
+ // Find matching shipping method
565
+ for (let i = 0; i < shippingSelect.options.length; i++) {
566
+ const option = shippingSelect.options[i];
567
+ if (option.value == shippingClassId || parseInt(option.value) === parseInt(shippingClassId)) {
568
+ shippingSelect.selectedIndex = i;
569
+ // Trigger change event to auto-set dates
570
+ shippingSelect.dispatchEvent(new Event('change'));
571
+ break;
572
+ }
573
+ }
574
+ }
575
+ }
576
+
577
+ // Helper function to show user-friendly success/error messages
578
+ // Delegate to the global Theme notification system so that
579
+ // product detail uses the same bottom-center toast as other entry points.
580
+ function showUserMessage(message, type = 'success') {
581
+ if (window.Theme && typeof window.Theme.showNotification === 'function') {
582
+ window.Theme.showNotification(message, type, 3000);
583
+ return;
584
+ }
585
+
586
+ // Fallback to inline cartMessage banner if Theme is not available
587
+ const cartMessage = document.getElementById('cartMessage');
588
+ const cartMessageText = cartMessage ? cartMessage.querySelector('.cart-message-text') : null;
589
+
590
+ if (cartMessage && cartMessageText) {
591
+ cartMessageText.textContent = message;
592
+
593
+ // Update styling based on message type
594
+ if (type === 'success') {
595
+ cartMessage.style.color = '#059669';
596
+ cartMessage.style.backgroundColor = 'transparent';
597
+ cartMessage.style.border = 'none';
598
+ } else if (type === 'error') {
599
+ cartMessage.style.color = '#b70000';
600
+ cartMessage.style.backgroundColor = '#ffe6e6';
601
+ cartMessage.style.border = '1px solid #b70000';
602
+ cartMessage.style.borderRadius = '4px';
603
+ cartMessage.style.padding = '12px';
604
+ }
605
+
606
+ cartMessage.style.display = 'block';
607
+
608
+ // Auto-hide after appropriate time
609
+ const hideDelay = type === 'success' ? 3000 : 5000;
610
+ setTimeout(() => {
611
+ cartMessage.style.display = 'none';
612
+ }, hideDelay);
613
+ }
614
+ }
615
+
616
+ function openLoginModal() {
617
+ if (window.Theme && typeof window.Theme.openLoginModal === 'function') {
618
+ window.Theme.openLoginModal();
619
+ return true;
620
+ }
621
+ if (window.CartManager && typeof window.CartManager.openLoginModal === 'function') {
622
+ window.CartManager.openLoginModal();
623
+ return true;
624
+ }
625
+ const loginTrigger = document.querySelector('[data-login-modal-trigger]');
626
+ if (loginTrigger) {
627
+ loginTrigger.click();
628
+ return true;
629
+ }
630
+ return false;
631
+ }
632
+
633
+ // Validate subscription before submission
634
+ function validateSubscription() {
635
+ // Check if this is a subscription product
636
+ if (productData.productType != 90) {
637
+ return { valid: true };
638
+ }
639
+
640
+ // Check if at least one subscription item is selected (only if customer can choose)
641
+ const addData = parseAdditionalData(productData.additionalData);
642
+ const subSettings = getSubscriptionSettings(addData);
643
+
644
+ // If customer can choose products, validate selection
645
+ if (!subSettings.allProductsRequired) {
646
+ const selectedSubs = document.querySelectorAll(".subscription-item.selected");
647
+ if (!selectedSubs || selectedSubs.length === 0) {
648
+ return {
649
+ valid: false,
650
+ message: "Please select at least one subscription item"
651
+ };
652
+ }
653
+
654
+ // Check minimum subscription items requirement
655
+ const minSelectable = addData.minimumSelectable || addData.settings?.minimumSelectable || addData.settings?.minSubscriptionProducts || 1;
656
+ if (selectedSubs.length < minSelectable) {
657
+ return {
658
+ valid: false,
659
+ message: `Please select at least ${minSelectable} subscription item(s)`
660
+ };
661
+ }
662
+ }
663
+
664
+ // Check if frequency is selected (skip if predefined)
665
+ if (!subSettings.hasPredefinedFrequency) {
666
+ if (!selectedFrequency) {
667
+ return {
668
+ valid: false,
669
+ message: "Please select a subscription frequency"
670
+ };
671
+ }
672
+
673
+ // Check if weekly frequency has at least one day selected
674
+ if (selectedFrequency === "Weekly") {
675
+ if (!frequencyDetails.days || frequencyDetails.days.length === 0) {
676
+ return {
677
+ valid: false,
678
+ message: "Please select at least one day for weekly delivery"
679
+ };
680
+ }
681
+ }
682
+ }
683
+
684
+ // Validate dates
685
+ const dateValidation = validateDates();
686
+ if (!dateValidation.valid) {
687
+ return dateValidation;
688
+ }
689
+
690
+ // If there's a predefined frequency with ordersCount, validate that calculated count matches
691
+ const expectedOrdersCount = subSettings.hasPredefinedFrequency ? (subSettings.predefinedFrequency.ordersCount) : null;
692
+ if (expectedOrdersCount !== null && expectedOrdersCount !== undefined) {
693
+ const liveOrderCount = document.querySelector(".liveOrderCount");
694
+ const calculatedCount = liveOrderCount ? parseInt(liveOrderCount.textContent) : 0;
695
+ if (calculatedCount !== expectedOrdersCount) {
696
+ return {
697
+ valid: false,
698
+ message: `The selected dates result in ${calculatedCount} deliveries, but this subscription requires exactly ${expectedOrdersCount} deliveries. Please adjust your dates.`
699
+ };
700
+ }
701
+ }
702
+
703
+ return { valid: true };
704
+ }
705
+
706
+ // Helper function to validate dates
707
+ function validateDates() {
708
+ const startDateInput = document.getElementById("startDate");
709
+ const endDateInput = document.getElementById("endDate");
710
+
711
+ if (!startDateInput || !endDateInput) return { valid: true };
712
+
713
+ const startDate = startDateInput.value;
714
+ const endDate = endDateInput.value;
715
+ const today = new Date();
716
+ today.setHours(0, 0, 0, 0);
717
+
718
+ // Check if start date is provided
719
+ if (!startDate) {
720
+ return { valid: false, message: "Please select a start date" };
721
+ }
722
+
723
+ // Check if end date is provided
724
+ if (!endDate) {
725
+ return { valid: false, message: "Please select an end date" };
726
+ }
727
+
728
+ const start = new Date(startDate);
729
+ start.setHours(0, 0, 0, 0);
730
+ const end = new Date(endDate);
731
+ end.setHours(0, 0, 0, 0);
732
+
733
+ // Check if start date is in the past
734
+ if (start < today) {
735
+ return { valid: false, message: "Start date cannot be in the past" };
736
+ }
737
+
738
+ // Check if end date is before start date
739
+ if (end < start) {
740
+ return { valid: false, message: "End date must be after start date" };
741
+ }
742
+
743
+ return { valid: true };
744
+ }
745
+
746
+ // Set minimum date to today for date inputs
747
+ function initializeDateInputs() {
748
+ const startDateInput = document.getElementById("startDate");
749
+ const endDateInput = document.getElementById("endDate");
750
+
751
+ // Check if we have predefined frequency data with dates
752
+ const addData = parseAdditionalData(productData.additionalData);
753
+ const subSettings = getSubscriptionSettings(addData);
754
+ const ordersCount = subSettings.predefinedFrequencyData.ordersCount || subSettings.predefinedFrequency.ordersCount;
755
+
756
+ // If predefined subscription, disable date inputs
757
+ if (!subSettings.isScheduleByCustomer) {
758
+ if (startDateInput) {
759
+ startDateInput.disabled = true;
760
+ startDateInput.style.backgroundColor = '#f5f5f5';
761
+ startDateInput.style.cursor = 'not-allowed';
762
+ startDateInput.setAttribute('readonly', 'readonly');
763
+ }
764
+ if (endDateInput) {
765
+ endDateInput.disabled = true;
766
+ endDateInput.style.backgroundColor = '#f5f5f5';
767
+ endDateInput.style.cursor = 'not-allowed';
768
+ endDateInput.setAttribute('readonly', 'readonly');
769
+ }
770
+ }
771
+
772
+ // For predefined subscriptions, dates will be auto-set when shipping method is selected
773
+ // No need to set dates here - they will be set by the shipping method change handler
774
+
775
+ if (startDateInput && !startDateInput.disabled) {
776
+ // Use local date formatting to avoid timezone issues
777
+ const today = new Date();
778
+ today.setHours(0, 0, 0, 0);
779
+ const todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0');
780
+ startDateInput.setAttribute('min', todayStr);
781
+ startDateInput.addEventListener('change', () => {
782
+ const validation = validateDates();
783
+ if (!validation.valid) {
784
+ showSubscriptionError(validation.message);
785
+ return;
786
+ }
787
+
788
+ // Update end date min to be start date
789
+ if (endDateInput && this.value) {
790
+ endDateInput.setAttribute('min', this.value);
791
+ }
792
+
793
+ // Check if this is a predefined subscription - if so, recalculate end date
794
+ const addData = parseAdditionalData(productData.additionalData);
795
+ const subSettings = getSubscriptionSettings(addData);
796
+ const expectedOrdersCount = subSettings.hasPredefinedFrequency ? (subSettings.predefinedFrequency.ordersCount) : null;
797
+
798
+ // For predefined subscriptions, recalculate end date to match deliverables
799
+ if (expectedOrdersCount !== null && expectedOrdersCount !== undefined && endDateInput) {
800
+ const correctEndDate = calculateCorrectEndDate(
801
+ this.value,
802
+ expectedOrdersCount,
803
+ addData
804
+ );
805
+
806
+ if (correctEndDate) {
807
+ endDateInput.value = formatDate(correctEndDate);
808
+ }
809
+ }
810
+
811
+ clearSubscriptionError();
812
+ calculateDeliverables();
813
+ });
814
+ }
815
+
816
+ if (endDateInput && !endDateInput.disabled) {
817
+ endDateInput.addEventListener('change', () => {
818
+ const validation = validateDates();
819
+ if (!validation.valid) {
820
+ showSubscriptionError(validation.message);
821
+ return;
822
+ }
823
+
824
+ // Check if this is a predefined subscription that requires exact deliverables
825
+ const addData = parseAdditionalData(productData.additionalData);
826
+ const subSettings = getSubscriptionSettings(addData);
827
+ const expectedOrdersCount = subSettings.hasPredefinedFrequency ? (subSettings.predefinedFrequency.ordersCount) : null;
828
+
829
+ // For predefined subscriptions, enforce exact deliverables count
830
+ if (expectedOrdersCount !== null && expectedOrdersCount !== undefined) {
831
+ // Calculate deliverables with current dates
832
+ calculateDeliverables();
833
+
834
+ // Wait a moment for calculation to complete, then check
835
+ setTimeout(() => {
836
+ const liveOrderCount = document.querySelector(".liveOrderCount");
837
+ const calculatedCount = liveOrderCount ? parseInt(liveOrderCount.textContent) : 0;
838
+
839
+ if (calculatedCount !== expectedOrdersCount) {
840
+ // End date doesn't match required deliverables - reset it
841
+ const correctEndDate = calculateCorrectEndDate(
842
+ startDateInput.value,
843
+ expectedOrdersCount,
844
+ addData
845
+ );
846
+
847
+ if (correctEndDate) {
848
+ endDateInput.value = formatDate(correctEndDate);
849
+
850
+ // Recalculate with correct end date
851
+ calculateDeliverables();
852
+
853
+ // Show validation message
854
+ showSubscriptionError(`End Date must match ${expectedOrdersCount} deliveries. The date has been adjusted.`);
855
+ } else {
856
+ showSubscriptionError(`End Date must match ${expectedOrdersCount} deliveries. Please adjust your dates.`);
857
+ }
858
+ } else {
859
+ clearSubscriptionError();
860
+ }
861
+ }, 50);
862
+ } else {
863
+ // Not a predefined subscription, allow normal calculation
864
+ clearSubscriptionError();
865
+ calculateDeliverables();
866
+ }
867
+ });
868
+ }
869
+ }
870
+
871
+ function renderSubscriptionUI() {
872
+ const container = document.getElementById("subscriptionPlanContainer");
873
+ if (!container || !productData.subscriptions) {
874
+ container.innerHTML = "<p>No subscription items available.</p>";
875
+ return;
876
+ }
877
+
878
+ const subscriptions = productData.subscriptions;
879
+
880
+ const addData = parseAdditionalData(productData.additionalData);
881
+
882
+ // Check for minimumSelectable in additionalSettings (try root level first, then settings)
883
+ const minSelectable = addData.minimumSelectable || addData.settings?.minimumSelectable || addData.settings?.minSubscriptionProducts || 1;
884
+ const maxSelectable = addData.maximumSelectable || addData.settings?.maximumSelectable || addData.settings?.maxSubscriptionProducts || 1;
885
+ const quantityChangeAllowed = addData.settings?.isQuantityChangeAllowed ?? true;
886
+ const subSettings = getSubscriptionSettings(addData);
887
+
888
+ container.innerHTML = "";
889
+ let selectedCount = 0;
890
+
891
+ // Show info message if all products are required (will be updated with frequency details later)
892
+ const infoMessageEl = document.getElementById("subscriptionInfoMessage");
893
+ if (infoMessageEl && subSettings.allProductsRequired) {
894
+ // Build product list with quantities
895
+ let productList = subscriptions.map(sub => {
896
+ const qty = sub.quantity || minSelectable;
897
+ return `${sub.name || 'Product'} (Qty: ${qty})`;
898
+ }).join(', ');
899
+
900
+ // Initial message (will be updated with frequency details)
901
+ infoMessageEl.innerHTML = `
902
+ <div class="info-message-content">
903
+ <strong>You will get the below products in this subscription with their respective quantities:</strong><br>
904
+ ${productList}
905
+ </div>
906
+ `;
907
+ infoMessageEl.style.display = "block";
908
+ } else if (infoMessageEl) {
909
+ infoMessageEl.style.display = "none";
910
+ }
911
+
912
+ subscriptions.forEach((sub) => {
913
+ let quantity = sub.quantity || minSelectable;
914
+
915
+ const subDiv = document.createElement("div");
916
+ subDiv.className = "subscription-item";
917
+ subDiv.dataset.subId = sub.productId;
918
+
919
+ // If all products are required, checkbox should be checked and disabled
920
+ const isRequired = subSettings.allProductsRequired;
921
+
922
+ const checkboxClass = isRequired ? 'sub-select sub-select-required' : 'sub-select';
923
+ subDiv.innerHTML = `
924
+ <div class="subscription-item-content">
925
+ <input
926
+ type="checkbox"
927
+ class="${checkboxClass}"
928
+ aria-label="${isRequired ? 'Included' : 'Select'} ${sub.name || 'subscription item'}"
929
+ id="sub-checkbox-${sub.productId}"
930
+ ${isRequired ? 'checked disabled' : ''}
931
+ >
932
+ <div>
933
+ <input type="hidden" class="sub-id" value="${sub.productId}">
934
+ <h4 class="subscription-item-title">${sub.name}${isRequired ? ' <span class="subscription-included">(Included)</span>' : ''}</h4>
935
+ <div class="subscription-spec">${sub.specification || ''}</div>
936
+ <div class="subscription-item-meta">
937
+ Price: <span class="sub-price">${formatMoney(sub.prices.price)}</span>
938
+ </div>
939
+ </div>
940
+ </div>
941
+
942
+ <div class="subscription-item-right">
943
+ <div class="subscription-item-actions">
944
+ <button
945
+ type="button"
946
+ class="qty-btn minus"
947
+ aria-label="Decrease quantity for ${sub.name || 'item'}"
948
+ >&minus;</button>
949
+ <span class="qty-value" aria-live="polite">${quantity}</span>
950
+ <button
951
+ type="button"
952
+ class="qty-btn plus"
953
+ aria-label="Increase quantity for ${sub.name || 'item'}"
954
+ >+</button>
955
+ </div>
956
+ <div class="subscription-item-meta">
957
+ Total: <span class="sub-total">${formatMoney(sub.prices.price * quantity)}</span>
958
+ </div>
959
+ </div>
960
+ `;
961
+
962
+ const qtyValueEl = subDiv.querySelector(".qty-value");
963
+ const totalEl = subDiv.querySelector(".sub-total");
964
+ const plusBtn = subDiv.querySelector(".plus");
965
+ const minusBtn = subDiv.querySelector(".minus");
966
+ const checkbox = subDiv.querySelector(".sub-select");
967
+
968
+ const updateTotal = () => {
969
+ totalEl.textContent = formatMoney(sub.prices.price * quantity);
970
+ updateSubscriptionPriceUI();
971
+ };
972
+
973
+
974
+
975
+ //=====================================
976
+ // QUANTITY BEHAVIOR
977
+ //=====================================
978
+ if (quantityChangeAllowed === false) {
979
+ // ❗ disable buttons completely
980
+ plusBtn.disabled = true;
981
+ minusBtn.disabled = true;
982
+ }
983
+ else {
984
+ plusBtn.addEventListener("click", () => {
985
+ // Sanitize: ensure quantity is a positive integer
986
+ quantity = Math.max(1, Math.floor(quantity) + 1);
987
+ qtyValueEl.textContent = quantity;
988
+ updateTotal();
989
+ });
990
+
991
+ minusBtn.addEventListener("click", () => {
992
+ // Sanitize: ensure quantity doesn't go below minimum
993
+ const newQuantity = Math.max(minSelectable, Math.floor(quantity) - 1);
994
+ if (newQuantity < quantity) {
995
+ quantity = newQuantity;
996
+ qtyValueEl.textContent = quantity;
997
+ updateTotal();
998
+ }
999
+ });
1000
+ }
1001
+ //=====================================
1002
+ // CHECKBOX SELECTION (line items)
1003
+ //=====================================
1004
+
1005
+ // If all products are required, mark as selected and don't allow changes
1006
+ if (isRequired) {
1007
+ checkbox.checked = true;
1008
+ subDiv.classList.add("selected");
1009
+ selectedCount++;
1010
+ } else {
1011
+ checkbox.addEventListener("change", () => {
1012
+ if (checkbox.checked) {
1013
+ if (selectedCount >= maxSelectable) {
1014
+ checkbox.checked = false;
1015
+ showSubscriptionError(`You can select only ${maxSelectable} subscription item${maxSelectable > 1 ? 's' : ''}.`);
1016
+ return;
1017
+ }
1018
+ selectedCount++;
1019
+ subDiv.classList.add("selected");
1020
+ clearSubscriptionError();
1021
+ } else {
1022
+ selectedCount--;
1023
+ subDiv.classList.remove("selected");
1024
+ }
1025
+ updateSubscriptionPriceUI();
1026
+ });
1027
+ }
1028
+
1029
+ container.appendChild(subDiv);
1030
+ });
1031
+
1032
+ // Auto-select items based on isScheduleByCustomer
1033
+ if (subSettings.allProductsRequired) {
1034
+ // If isScheduleByCustomer is false, select ALL items
1035
+ const subscriptionItems = container.querySelectorAll(".subscription-item");
1036
+ subscriptionItems.forEach(item => {
1037
+ const checkbox = item.querySelector(".sub-select");
1038
+ if (checkbox && !checkbox.checked) {
1039
+ checkbox.checked = true;
1040
+ item.classList.add("selected");
1041
+ selectedCount++;
1042
+ }
1043
+ });
1044
+ if (subscriptionItems.length > 0) {
1045
+ updateSubscriptionPriceUI();
1046
+ }
1047
+ } else if (minSelectable > 0 && subscriptions.length > 0) {
1048
+ // Otherwise, auto-select minimum required items
1049
+ const subscriptionItems = container.querySelectorAll(".subscription-item");
1050
+ let selected = 0;
1051
+ for (let i = 0; i < subscriptionItems.length && selected < minSelectable; i++) {
1052
+ const checkbox = subscriptionItems[i].querySelector(".sub-select");
1053
+ if (checkbox && !checkbox.checked) {
1054
+ checkbox.checked = true;
1055
+ subscriptionItems[i].classList.add("selected");
1056
+ selected++;
1057
+ selectedCount++;
1058
+ }
1059
+ }
1060
+ if (selected > 0) {
1061
+ updateSubscriptionPriceUI();
1062
+ }
1063
+ }
1064
+
1065
+ // Check if frequency is predefined (isScheduleByCustomer is false and frequency exists)
1066
+ if (subSettings.hasPredefinedFrequency) {
1067
+ // Use predefined frequency data
1068
+ const predefinedFrequency = subSettings.predefinedFrequency;
1069
+ const predefinedFrequencyData = subSettings.predefinedFrequencyData;
1070
+
1071
+ // Set frequency from predefined data
1072
+ const freqOption = predefinedFrequency.selectedOption || predefinedFrequencyData.selectedOption || 'daily';
1073
+ let freqOptionNormalized = freqOption.toLowerCase();
1074
+
1075
+ // Map frequency options
1076
+ if (freqOptionNormalized === 'daily' || predefinedFrequencyData.isDailyFrequency) {
1077
+ selectedFrequency = 'Daily';
1078
+ } else if (freqOptionNormalized === 'weekly' || predefinedFrequencyData.isWeeklyFrequency) {
1079
+ selectedFrequency = 'Weekly';
1080
+ } else if (freqOptionNormalized === 'monthly' || predefinedFrequencyData.isMonthlyFrequency) {
1081
+ selectedFrequency = 'Monthly';
1082
+ } else if (freqOptionNormalized === 'alternate' || freqOptionNormalized === 'alternate days' || predefinedFrequencyData.isAlterNativeFrequency) {
1083
+ selectedFrequency = 'Alternate Days';
1084
+ } else {
1085
+ selectedFrequency = freqOption.charAt(0).toUpperCase() + freqOption.slice(1);
1086
+ }
1087
+
1088
+ // Use ordersCount from frequencyData if available
1089
+ const ordersCount = predefinedFrequencyData.ordersCount || predefinedFrequency.ordersCount;
1090
+
1091
+ // Set frequencyDetails for calculation
1092
+ // Handle Weekly frequency - ensure days array is properly set
1093
+ let weeklyDays = null;
1094
+ if (selectedFrequency === 'Weekly') {
1095
+ // weeklyFreVals should be an array of day names, or we default to all days
1096
+ if (predefinedFrequencyData.weeklyFreVals && Array.isArray(predefinedFrequencyData.weeklyFreVals) && predefinedFrequencyData.weeklyFreVals.length > 0) {
1097
+ weeklyDays = predefinedFrequencyData.weeklyFreVals;
1098
+ } else {
1099
+ // Default to all days if not specified (once per week)
1100
+ weeklyDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1101
+ }
1102
+ }
1103
+
1104
+ frequencyDetails = {
1105
+ type: selectedFrequency,
1106
+ days: weeklyDays || (selectedFrequency === 'Alternate Days' ? (predefinedFrequencyData.weeklyFreVal || 2) : null),
1107
+ day: predefinedFrequencyData.day || (selectedFrequency === 'Monthly' ? (predefinedFrequencyData.day || 1) : null)
1108
+ };
1109
+
1110
+ // Display frequency UI with predefined selection
1111
+ // Render frequency UI with default selection from predefined data
1112
+ renderFrequencyUI(selectedFrequency, frequencyDetails);
1113
+
1114
+ // Update info message with frequency details
1115
+ updateSubscriptionInfoMessage(selectedFrequency, ordersCount, frequencyDetails);
1116
+
1117
+ // Calculate deliverables based on dates (will validate against ordersCount)
1118
+ setTimeout(() => {
1119
+ calculateDeliverables();
1120
+ }, 100);
1121
+ } else {
1122
+ // Render frequency UI for customer selection
1123
+ renderFrequencyUI();
1124
+ }
1125
+
1126
+ initializeDateInputs();
1127
+ }
1128
+
1129
+ // Helper function to update subscription info message with frequency details
1130
+ function updateSubscriptionInfoMessage(frequency, ordersCount, frequencyDetails) {
1131
+ const infoMessageEl = document.getElementById("subscriptionInfoMessage");
1132
+ if (!infoMessageEl || infoMessageEl.style.display === "none") return;
1133
+
1134
+ const addData = parseAdditionalData(productData.additionalData);
1135
+
1136
+ const subSettings = getSubscriptionSettings(addData);
1137
+
1138
+ if (subSettings.allProductsRequired && productData.subscriptions) {
1139
+ // Build product list with quantities
1140
+ const minSelectable = addData.minimumSelectable || addData.settings?.minimumSelectable || addData.settings?.minSubscriptionProducts || 1;
1141
+ let productList = productData.subscriptions.map(sub => {
1142
+ const qty = sub.quantity || minSelectable;
1143
+ return `${sub.name || 'Product'} (Qty: ${qty})`;
1144
+ }).join(', ');
1145
+
1146
+ // Build frequency text with details
1147
+ let frequencyText = '';
1148
+ if (frequency) {
1149
+ let frequencyDetail = '';
1150
+ if (frequencyDetails) {
1151
+ if (frequency === 'Alternate Days' && frequencyDetails.days) {
1152
+ frequencyDetail = ` (every ${frequencyDetails.days} days)`;
1153
+ } else if (frequency === 'Weekly' && frequencyDetails.days && Array.isArray(frequencyDetails.days) && frequencyDetails.days.length > 0) {
1154
+ frequencyDetail = ` (${frequencyDetails.days.join(', ')} each week)`;
1155
+ } else if (frequency === 'Monthly' && frequencyDetails.day) {
1156
+ frequencyDetail = ` (day ${frequencyDetails.day} of each month)`;
1157
+ }
1158
+ }
1159
+
1160
+ }
1161
+
1162
+ infoMessageEl.innerHTML = `
1163
+ <div class="info-message-content">
1164
+ <strong>You will get the below products in this subscription with their respective quantities:</strong><br>
1165
+ </div>
1166
+ `;
1167
+ }
1168
+ }
1169
+
1170
+
1171
+
1172
+ let selectedFrequency = null;
1173
+ let frequencyDetails = {};
1174
+
1175
+ function renderFrequencyUI(defaultFrequency, defaultFrequencyDetails) {
1176
+ const container = document.getElementById("frequencyContainer");
1177
+ if (!container) return;
1178
+ container.innerHTML = "";
1179
+
1180
+ const frequencyOptions = ["Daily", "Alternate Days", "Weekly", "Monthly"];
1181
+
1182
+ const title = document.createElement("h4");
1183
+ title.innerHTML = "Select Subscription Frequency: <span class='required-indicator'>*</span>";
1184
+ title.style.marginBottom = "10px";
1185
+ container.appendChild(title);
1186
+
1187
+ const frequencyOptionsContainer = document.createElement("div");
1188
+ frequencyOptionsContainer.style.display = "flex";
1189
+ frequencyOptionsContainer.style.flexWrap = "wrap";
1190
+ frequencyOptionsContainer.style.gap = "10px";
1191
+ container.appendChild(frequencyOptionsContainer);
1192
+
1193
+ const detailsContainer = document.createElement("div");
1194
+ detailsContainer.id = "frequencyDetailsContainer";
1195
+ detailsContainer.style.marginTop = "15px";
1196
+ container.appendChild(detailsContainer);
1197
+
1198
+ // Check if this is a predefined frequency (read-only)
1199
+ const isPredefined = defaultFrequency !== null && defaultFrequency !== undefined;
1200
+
1201
+ frequencyOptions.forEach(option => {
1202
+ const btn = document.createElement("button");
1203
+ btn.type = "button";
1204
+ btn.textContent = option;
1205
+ btn.className = "freq-ui";
1206
+ btn.setAttribute("aria-label", `Select ${option} frequency`);
1207
+
1208
+ // Pre-select if this is the default frequency
1209
+ if (isPredefined && option === defaultFrequency) {
1210
+ btn.classList.add("selected");
1211
+ selectedFrequency = option;
1212
+ // Set frequencyDetails from predefined data
1213
+ if (defaultFrequencyDetails) {
1214
+ frequencyDetails = JSON.parse(JSON.stringify(defaultFrequencyDetails));
1215
+ }
1216
+ }
1217
+
1218
+ // If predefined, disable buttons (read-only display)
1219
+ if (isPredefined) {
1220
+ btn.disabled = true;
1221
+ btn.style.opacity = option === defaultFrequency ? "1" : "0.5";
1222
+ btn.style.cursor = "not-allowed";
1223
+ } else {
1224
+ // Allow selection for non-predefined frequencies
1225
+ btn.addEventListener("click", () => {
1226
+ selectedFrequency = option;
1227
+ frequencyDetails = {};
1228
+
1229
+ frequencyOptionsContainer.querySelectorAll("button").forEach(b => b.classList.remove("selected"));
1230
+ btn.classList.add("selected");
1231
+
1232
+ renderFrequencyDetails(option, detailsContainer);
1233
+ calculateDeliverables();
1234
+ });
1235
+ }
1236
+
1237
+ frequencyOptionsContainer.appendChild(btn);
1238
+ });
1239
+
1240
+ // Render frequency details if default frequency is set
1241
+ if (isPredefined && defaultFrequency) {
1242
+ renderFrequencyDetails(defaultFrequency, detailsContainer, defaultFrequencyDetails, isPredefined);
1243
+ }
1244
+ }
1245
+
1246
+ function renderFrequencyDetails(frequency, container, predefinedDetails, isPredefined) {
1247
+ container.innerHTML = "";
1248
+
1249
+ if (frequency === "Daily") {
1250
+ container.innerHTML = `<p>Occurs every day.</p>`;
1251
+ if (!isPredefined) {
1252
+ frequencyDetails.type = "Daily";
1253
+ }
1254
+
1255
+ } else if (frequency === "Alternate Days") {
1256
+ const wrapper = document.createElement("div");
1257
+ wrapper.style.display = "flex";
1258
+ wrapper.style.alignItems = "center";
1259
+ wrapper.style.gap = "10px";
1260
+
1261
+ const label = document.createElement("span");
1262
+ label.textContent = "Every";
1263
+
1264
+ const input = document.createElement("input");
1265
+ input.type = "number";
1266
+ input.min = 1;
1267
+ input.value = predefinedDetails?.days || 2;
1268
+ input.className = "freq-ui";
1269
+ input.style.width = "60px";
1270
+ input.style.textAlign = "center";
1271
+ input.setAttribute("aria-label", "Number of days between deliveries");
1272
+
1273
+ if (isPredefined) {
1274
+ input.disabled = true;
1275
+ input.style.backgroundColor = "#f5f5f5";
1276
+ input.style.cursor = "not-allowed";
1277
+ } else {
1278
+ input.addEventListener("input", () => {
1279
+ frequencyDetails.type = "Alternate Days";
1280
+ frequencyDetails.days = parseInt(input.value) || 2;
1281
+ calculateDeliverables();
1282
+ });
1283
+ }
1284
+
1285
+ const text = document.createElement("span");
1286
+ text.textContent = "days";
1287
+
1288
+ wrapper.appendChild(label);
1289
+ wrapper.appendChild(input);
1290
+ wrapper.appendChild(text);
1291
+ container.appendChild(wrapper);
1292
+
1293
+ if (!isPredefined) {
1294
+ frequencyDetails.type = "Alternate Days";
1295
+ frequencyDetails.days = parseInt(input.value) || 2;
1296
+ }
1297
+
1298
+ } else if (frequency === "Weekly") {
1299
+ const days = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
1300
+ const wrapper = document.createElement("div");
1301
+ wrapper.className = "weekly-days";
1302
+
1303
+ // Use predefined days if available, otherwise empty array
1304
+ const predefinedDays = predefinedDetails?.days || [];
1305
+ if (!isPredefined) {
1306
+ frequencyDetails.days = [];
1307
+ }
1308
+
1309
+ days.forEach(day => {
1310
+ const btn = document.createElement("button");
1311
+ btn.type = "button";
1312
+ btn.textContent = day;
1313
+ btn.className = "freq-ui";
1314
+ btn.setAttribute("aria-label", `Select ${day} for weekly delivery`);
1315
+
1316
+ // Pre-select days from predefined data
1317
+ if (isPredefined && predefinedDays.includes(day)) {
1318
+ btn.classList.add("selected");
1319
+ if (!frequencyDetails.days) frequencyDetails.days = [];
1320
+ if (!frequencyDetails.days.includes(day)) {
1321
+ frequencyDetails.days.push(day);
1322
+ }
1323
+ }
1324
+
1325
+ if (isPredefined) {
1326
+ btn.disabled = true;
1327
+ btn.style.opacity = predefinedDays.includes(day) ? "1" : "0.5";
1328
+ btn.style.cursor = "not-allowed";
1329
+ } else {
1330
+ btn.addEventListener("click", () => {
1331
+ if (frequencyDetails.days.includes(day)) {
1332
+ frequencyDetails.days = frequencyDetails.days.filter(d => d !== day);
1333
+ btn.classList.remove("selected");
1334
+ } else {
1335
+ frequencyDetails.days.push(day);
1336
+ btn.classList.add("selected");
1337
+ }
1338
+ calculateDeliverables();
1339
+ });
1340
+ }
1341
+
1342
+ wrapper.appendChild(btn);
1343
+ });
1344
+
1345
+ container.appendChild(wrapper);
1346
+
1347
+ } else if (frequency === "Monthly") {
1348
+ const wrapper = document.createElement("div");
1349
+ wrapper.style.display = "flex";
1350
+ wrapper.style.alignItems = "center";
1351
+ wrapper.style.gap = "10px";
1352
+
1353
+ const label = document.createElement("span");
1354
+ label.textContent = "Day of month:";
1355
+
1356
+ const input = document.createElement("input");
1357
+ input.type = "number";
1358
+ input.min = 1;
1359
+ input.max = 31;
1360
+ input.value = predefinedDetails?.day || 1;
1361
+ input.className = "freq-ui";
1362
+ input.style.width = "60px";
1363
+ input.style.textAlign = "center";
1364
+ input.setAttribute("aria-label", "Day of month for monthly delivery");
1365
+
1366
+ if (isPredefined) {
1367
+ input.disabled = true;
1368
+ input.style.backgroundColor = "#f5f5f5";
1369
+ input.style.cursor = "not-allowed";
1370
+ } else {
1371
+ input.addEventListener("input", () => {
1372
+ frequencyDetails.type = "Monthly";
1373
+ frequencyDetails.day = parseInt(input.value) || 1;
1374
+ calculateDeliverables();
1375
+ });
1376
+ }
1377
+
1378
+ wrapper.appendChild(label);
1379
+ wrapper.appendChild(input);
1380
+ container.appendChild(wrapper);
1381
+
1382
+ if (!isPredefined) {
1383
+ frequencyDetails.type = "Monthly";
1384
+ frequencyDetails.day = parseInt(input.value) || 1;
1385
+ }
1386
+ }
1387
+ }
1388
+
1389
+ // -------------------------------------
1390
+ // DELIVERY COUNT CALCULATOR
1391
+ // -------------------------------------
1392
+ function calculateDeliverables() {
1393
+ try {
1394
+ // Check if we have predefined frequency with ordersCount
1395
+ const addData = parseAdditionalData(productData.additionalData);
1396
+
1397
+ const subSettings = getSubscriptionSettings(addData);
1398
+
1399
+ // Check if there's a predefined ordersCount to validate against
1400
+ const expectedOrdersCount = subSettings.hasPredefinedFrequency ? (subSettings.predefinedFrequency.ordersCount) : null;
1401
+
1402
+ const startDateInput = document.getElementById("startDate");
1403
+ const endDateInput = document.getElementById("endDate");
1404
+
1405
+ if (!startDateInput || !endDateInput) return;
1406
+
1407
+ const start = startDateInput.value;
1408
+ const end = endDateInput.value;
1409
+
1410
+ // Validate dates before calculating
1411
+ const dateValidation = validateDates();
1412
+ if (!dateValidation.valid) {
1413
+ showSubscriptionError(dateValidation.message);
1414
+ const liveOrderCount = document.querySelector(".liveOrderCount");
1415
+ if (liveOrderCount) liveOrderCount.textContent = "0";
1416
+ return;
1417
+ }
1418
+
1419
+ clearSubscriptionError();
1420
+
1421
+ // If predefined frequency, always get selectedFrequency from predefined data
1422
+ let currentSelectedFrequency = selectedFrequency;
1423
+ if (subSettings.hasPredefinedFrequency) {
1424
+ // Always use predefined frequency data to ensure accuracy
1425
+ const predefinedFrequencyData = subSettings.predefinedFrequencyData;
1426
+ const freqOption = subSettings.predefinedFrequency.selectedOption || predefinedFrequencyData.selectedOption || 'daily';
1427
+ let freqOptionNormalized = freqOption.toLowerCase();
1428
+ if (predefinedFrequencyData.isDailyFrequency) {
1429
+ currentSelectedFrequency = 'Daily';
1430
+ } else if (predefinedFrequencyData.isWeeklyFrequency) {
1431
+ currentSelectedFrequency = 'Weekly';
1432
+ } else if (predefinedFrequencyData.isMonthlyFrequency) {
1433
+ currentSelectedFrequency = 'Monthly';
1434
+ } else if (predefinedFrequencyData.isAlterNativeFrequency) {
1435
+ currentSelectedFrequency = 'Alternate Days';
1436
+ } else {
1437
+ // Fallback to option name
1438
+ currentSelectedFrequency = freqOption.charAt(0).toUpperCase() + freqOption.slice(1);
1439
+ }
1440
+ }
1441
+
1442
+ if (!start || !end || !currentSelectedFrequency) {
1443
+ const liveOrderCount = document.querySelector(".liveOrderCount");
1444
+ if (liveOrderCount) liveOrderCount.textContent = "0";
1445
+ return;
1446
+ }
1447
+
1448
+ let count = 0;
1449
+ let startDate = new Date(start);
1450
+ let endDate = new Date(end);
1451
+
1452
+ // Normalize to midnight
1453
+ startDate.setHours(0,0,0,0);
1454
+ endDate.setHours(0,0,0,0);
1455
+
1456
+ // Get frequencyDetails for predefined frequencies if not set globally
1457
+ let currentFrequencyDetails = frequencyDetails || {};
1458
+ if (subSettings.hasPredefinedFrequency && (!currentFrequencyDetails || Object.keys(currentFrequencyDetails).length === 0)) {
1459
+ const predefinedFrequencyData = subSettings.predefinedFrequencyData;
1460
+ // Handle Weekly frequency - ensure days array is properly set
1461
+ let weeklyDays = null;
1462
+ if (currentSelectedFrequency === 'Weekly') {
1463
+ // weeklyFreVals should be an array of day names, or we default to all days
1464
+ if (predefinedFrequencyData.weeklyFreVals && Array.isArray(predefinedFrequencyData.weeklyFreVals) && predefinedFrequencyData.weeklyFreVals.length > 0) {
1465
+ weeklyDays = predefinedFrequencyData.weeklyFreVals;
1466
+ } else {
1467
+ // Default to all days if not specified
1468
+ weeklyDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1469
+ }
1470
+ }
1471
+
1472
+ currentFrequencyDetails = {
1473
+ type: currentSelectedFrequency,
1474
+ days: weeklyDays || (currentSelectedFrequency === 'Alternate Days' ? (predefinedFrequencyData.weeklyFreVal || 2) : null),
1475
+ day: predefinedFrequencyData.day || (currentSelectedFrequency === 'Monthly' ? (predefinedFrequencyData.day || 1) : null)
1476
+ };
1477
+ }
1478
+
1479
+ // Also ensure frequencyDetails is set globally for future calculations
1480
+ if (subSettings.hasPredefinedFrequency && (!frequencyDetails || Object.keys(frequencyDetails).length === 0)) {
1481
+ frequencyDetails = currentFrequencyDetails;
1482
+ }
1483
+
1484
+ // ----------------------------------------
1485
+ // DAILY = every single day including both ends
1486
+ // ----------------------------------------
1487
+ if (currentSelectedFrequency === "Daily") {
1488
+ count = Math.floor((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
1489
+ }
1490
+
1491
+ // ----------------------------------------
1492
+ // ALTERNATE DAYS
1493
+ // Deliver every X days from start
1494
+ // Including starting day
1495
+ // ----------------------------------------
1496
+ else if (currentSelectedFrequency === "Alternate Days") {
1497
+ let step = currentFrequencyDetails.days || 2;
1498
+
1499
+ for (let d = new Date(startDate), i = 0; d <= endDate; d.setDate(d.getDate() + step), i++) {
1500
+ count++;
1501
+ }
1502
+ }
1503
+
1504
+ // ----------------------------------------
1505
+ // WEEKLY
1506
+ // Selected specific days of week
1507
+ // ----------------------------------------
1508
+ else if (currentSelectedFrequency === "Weekly") {
1509
+ let selectedDays = currentFrequencyDetails.days || [];
1510
+
1511
+ // If no days specified, default to all days (once per week = 7 days)
1512
+ if (!Array.isArray(selectedDays) || selectedDays.length === 0) {
1513
+ selectedDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1514
+ }
1515
+
1516
+ // Count occurrences of selected days within the date range
1517
+ for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
1518
+ let day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()];
1519
+ if (selectedDays.includes(day)) count++;
1520
+ }
1521
+ }
1522
+
1523
+ // ----------------------------------------
1524
+ // MONTHLY
1525
+ // Day of month (e.g. 5 → 5th every month)
1526
+ // Handles invalid dates (e.g., Feb 31) by using last day of month
1527
+ // ----------------------------------------
1528
+ else if (currentSelectedFrequency === "Monthly") {
1529
+ let dom = currentFrequencyDetails.day || 1;
1530
+
1531
+ // Start from the first occurrence of the day in the start month
1532
+ let currentDate = new Date(startDate);
1533
+ currentDate.setDate(dom);
1534
+
1535
+ // If the date is before start date, move to next month
1536
+ if (currentDate < startDate) {
1537
+ currentDate.setMonth(currentDate.getMonth() + 1);
1538
+ currentDate.setDate(dom);
1539
+ }
1540
+
1541
+ // Count occurrences until we exceed end date
1542
+ while (currentDate <= endDate) {
1543
+ // Check if date is valid (handles cases like Feb 31)
1544
+ const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
1545
+ const validDay = Math.min(dom, lastDayOfMonth);
1546
+
1547
+ // Create valid date
1548
+ const validDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), validDay);
1549
+
1550
+ if (validDate >= startDate && validDate <= endDate) {
1551
+ count++;
1552
+ }
1553
+
1554
+ // Move to next month
1555
+ currentDate.setMonth(currentDate.getMonth() + 1);
1556
+ currentDate.setDate(validDay);
1557
+ }
1558
+ }
1559
+
1560
+ const liveOrderCount = document.querySelector(".liveOrderCount");
1561
+ if (liveOrderCount) {
1562
+ liveOrderCount.textContent = count;
1563
+ }
1564
+
1565
+ // If there's an expected order count, validate against it
1566
+ if (expectedOrdersCount !== null && expectedOrdersCount !== undefined) {
1567
+ if (count !== expectedOrdersCount) {
1568
+ showSubscriptionError(`The selected dates result in ${count} deliveries, but this subscription requires exactly ${expectedOrdersCount} deliveries. Please adjust your dates.`);
1569
+ } else {
1570
+ // Clear any previous errors when count matches
1571
+ clearSubscriptionError();
1572
+ }
1573
+ } else {
1574
+ // If no expected count, clear any errors
1575
+ clearSubscriptionError();
1576
+ }
1577
+
1578
+ updateSubscriptionPriceUI();
1579
+ } catch (error) {
1580
+ console.error("Error calculating deliverables:", error);
1581
+ showSubscriptionError("Error calculating delivery count. Please check your dates.");
1582
+ const liveOrderCount = document.querySelector(".liveOrderCount");
1583
+ if (liveOrderCount) liveOrderCount.textContent = "0";
1584
+ }
1585
+ }
1586
+
1587
+
1588
+ function updateSubscriptionPriceUI() {
1589
+ try {
1590
+ let total = 0;
1591
+ const liveOrderCountEl = document.querySelector(".liveOrderCount");
1592
+ let deliverables = liveOrderCountEl ? parseInt(liveOrderCountEl.textContent) || 1 : 1;
1593
+
1594
+ // Parse additionalData if needed
1595
+ const additionalData = parseAdditionalData(productData.additionalData);
1596
+
1597
+ let items = [];
1598
+ // Must explicitly check IsSubscriptionPrice
1599
+ if (additionalData.IsSubscriptionPrice === true) {
1600
+ const subSettings = getSubscriptionSettings(additionalData);
1601
+
1602
+ // If isScheduleByCustomer is false, include ALL subscription products
1603
+ if (subSettings.allProductsRequired) {
1604
+ // Get all subscription products from productData
1605
+ const allSubscriptions = productData.subscriptions || [];
1606
+ allSubscriptions.forEach(sub => {
1607
+ const quantity = sub.quantity || 1;
1608
+ const price = parseFloat(sub.prices?.price || sub.price || 0);
1609
+ const subId = parseInt(sub.productId || sub.id || 0);
1610
+
1611
+ if (subId > 0 && price >= 0) {
1612
+ items.push({
1613
+ id: subId,
1614
+ name: sub.name || '',
1615
+ quantity: quantity,
1616
+ price: price,
1617
+ orderTotal: price * quantity
1618
+ });
1619
+ total += price * quantity;
1620
+ }
1621
+ });
1622
+
1623
+ // For predefined subscriptions, check if orderTotal is already provided
1624
+ // If orderTotal exists, it's already the total for all deliveries, don't multiply
1625
+ const predefinedOrderTotal = additionalData.subscriptionDetails?.orderTotal;
1626
+ if (predefinedOrderTotal) {
1627
+ // Use the predefined orderTotal directly (already includes all deliveries)
1628
+ deliverables = Math.max(1, deliverables);
1629
+ total = total * deliverables;
1630
+ total = total;
1631
+ } else {
1632
+ // If no predefined orderTotal, calculate from perOrderPrice if available
1633
+ const perOrderPrice = additionalData.settings?.perOrderPrice;
1634
+ if (perOrderPrice) {
1635
+ total = parseFloat(perOrderPrice) || total;
1636
+ // Multiply by deliverables only if using perOrderPrice
1637
+ deliverables = Math.max(1, deliverables);
1638
+ total = total * deliverables;
1639
+ } else {
1640
+ // Fallback: multiply calculated total by deliverables
1641
+ deliverables = Math.max(1, deliverables);
1642
+ total = total * deliverables;
1643
+ }
1644
+ }
1645
+ } else {
1646
+ // ⛳ find ALL selected subscriptions (customer can choose)
1647
+ const selectedSubs = document.querySelectorAll(".subscription-item.selected");
1648
+
1649
+ selectedSubs.forEach(subItem => {
1650
+ const idEl = subItem.querySelector(".sub-id");
1651
+ const priceEl = subItem.querySelector(".sub-price");
1652
+ const qtyEl = subItem.querySelector(".qty-value");
1653
+ const nameEl = subItem.querySelector("h4");
1654
+
1655
+ // Sanitize and validate inputs
1656
+ let price = parseFloat(priceEl.textContent.replace(/[^\d.-]/g, '')) || 0;
1657
+ let qty = Math.max(1, parseInt(qtyEl.textContent) || 1); // Ensure positive integer
1658
+ let subId = parseInt(idEl.value) || 0;
1659
+ let name = nameEl ? nameEl.textContent.replace(/\s*\(Included\)/g, '').trim() : '';
1660
+
1661
+ if (subId > 0 && price >= 0 && qty > 0) {
1662
+ items.push({
1663
+ id: subId,
1664
+ name: name,
1665
+ quantity: qty,
1666
+ price: price,
1667
+ orderTotal: price * qty
1668
+ });
1669
+ total += price * qty;
1670
+ }
1671
+ });
1672
+
1673
+ // For customer-selectable subscriptions, multiply by deliverables
1674
+ deliverables = Math.max(1, deliverables);
1675
+ total = total * deliverables;
1676
+ }
1677
+
1678
+ } else {
1679
+ total = productData.price || 0;
1680
+ // For non-subscription products, multiply by deliverables
1681
+ deliverables = Math.max(1, deliverables);
1682
+ total = total * deliverables;
1683
+ }
1684
+
1685
+ if (priceElement) {
1686
+ priceElement.textContent = formatMoney(total);
1687
+ }
1688
+ updateSubscriptionData(items, deliverables, total);
1689
+ } catch (error) {
1690
+ console.error("Error updating subscription price UI:", error);
1691
+ showSubscriptionError("Error calculating price. Please refresh the page.");
1692
+ }
1693
+ }
1694
+
1695
+ function updateSubscriptionData(items, deliverables, priceTotal) {
1696
+ try {
1697
+ const startDateInput = document.getElementById("startDate");
1698
+ const endDateInput = document.getElementById("endDate");
1699
+
1700
+ if (!startDateInput || !endDateInput) return;
1701
+
1702
+ let startDate = startDateInput.value;
1703
+ let endDate = endDateInput.value;
1704
+
1705
+ // Validate and sanitize dates
1706
+ if (!startDate || !endDate) return;
1707
+
1708
+ // ensure ISO dates
1709
+ let isoStart = startDate ? new Date(startDate).toISOString() : null;
1710
+ let isoEnd = endDate ? new Date(endDate).toISOString() : null;
1711
+
1712
+ // Validate dates are valid
1713
+ if (isoStart && isNaN(new Date(isoStart).getTime())) {
1714
+ console.warn("Invalid start date");
1715
+ return;
1716
+ }
1717
+ if (isoEnd && isNaN(new Date(isoEnd).getTime())) {
1718
+ console.warn("Invalid end date");
1719
+ return;
1720
+ }
1721
+
1722
+ // Get original settings from additionalData to preserve isScheduleByCustomer and other settings
1723
+ let originalAddData = {};
1724
+ try {
1725
+ const originalDataEl = document.getElementById('productData');
1726
+ if (originalDataEl) {
1727
+ const originalProductData = JSON.parse(originalDataEl.textContent);
1728
+ originalAddData = parseAdditionalData(originalProductData.additionalData);
1729
+ }
1730
+ } catch (e) {
1731
+ console.warn("Error parsing original additionalData:", e);
1732
+ }
1733
+
1734
+ let subscriptionPayload = {
1735
+ QuantityVariation: [],
1736
+ IsCombinationPrice: false,
1737
+
1738
+ settings: {
1739
+ isScheduleByCustomer: originalAddData.settings?.isScheduleByCustomer ?? true,
1740
+ typeOfOrder: originalAddData.settings?.typeOfOrder ?? 10,
1741
+ // Set shipping class based on order count (deliverables)
1742
+ // Use original shippingClassId if available, otherwise calculate based on order count
1743
+ // Default to 3 if no order count, otherwise use order count (capped at reasonable max)
1744
+ shippingClassId: originalAddData.settings?.shippingClassId || (deliverables > 0 ? Math.max(3, Math.min(deliverables, 50)) : 3),
1745
+ offersSetting: originalAddData.settings?.offersSetting || [],
1746
+ perOrderPrice: deliverables > 0 ? priceTotal / deliverables : priceTotal,
1747
+ nextBillDate: originalAddData.settings?.nextBillDate || null,
1748
+ startDate: isoStart,
1749
+ endDate: isoEnd,
1750
+ isProductChoiceEnabled: originalAddData.settings?.isProductChoiceEnabled ?? true,
1751
+ isQuantityChangeAllowed: originalAddData.settings?.isQuantityChangeAllowed ?? true,
1752
+ minSubscriptionProducts: originalAddData.settings?.minSubscriptionProducts ?? 1,
1753
+ maxSubscriptionProducts: originalAddData.settings?.maxSubscriptionProducts ?? 100
1754
+ },
1755
+
1756
+ subscriptionDetails: {
1757
+ shippingFeeAmount: originalAddData.subscriptionDetails?.shippingFeeAmount || 0,
1758
+ paymentFeeAmount: originalAddData.subscriptionDetails?.paymentFeeAmount || 0,
1759
+ roundOff: originalAddData.subscriptionDetails?.roundOff || 0,
1760
+ discountAmount: originalAddData.subscriptionDetails?.discountAmount || 0,
1761
+ subscriptionCoupon: originalAddData.subscriptionDetails?.subscriptionCoupon || {},
1762
+ orderTotal: priceTotal.toFixed(2)
1763
+ },
1764
+
1765
+ IsSubscriptionPrice: true,
1766
+
1767
+ paymentSettings: {
1768
+ detectPaymentFromLP: true,
1769
+ createInvoiceWithoutPayment: true
1770
+ },
1771
+
1772
+ QuantityManualInput: false,
1773
+
1774
+ items: items,
1775
+
1776
+ frequency: originalAddData.frequency || {
1777
+ selectedOption: selectedFrequency?.toLowerCase(),
1778
+ timeFre: originalAddData.frequency?.timeFre || null,
1779
+ ordersCount: deliverables
1780
+ },
1781
+ frequencyData: {
1782
+ ...(originalAddData.frequencyData || {}),
1783
+ selectedOption: selectedFrequency?.toLowerCase() || originalAddData.frequencyData?.selectedOption,
1784
+ ordersCount: deliverables || originalAddData.frequencyData?.ordersCount || originalAddData.frequency?.ordersCount,
1785
+ startDate: isoStart || originalAddData.frequencyData?.startDate,
1786
+ endDate: isoEnd || originalAddData.frequencyData?.endDate,
1787
+ isDailyFrequency: (selectedFrequency == "Daily") || originalAddData.frequencyData?.isDailyFrequency || false,
1788
+ isWeeklyFrequency: (selectedFrequency == "Weekly") || originalAddData.frequencyData?.isWeeklyFrequency || false,
1789
+ isAlterNativeFrequency: (selectedFrequency == "Alternate Days") || originalAddData.frequencyData?.isAlterNativeFrequency || false,
1790
+ isMonthlyFrequency: (selectedFrequency == "Monthly") || originalAddData.frequencyData?.isMonthlyFrequency || false,
1791
+ isYearlyFrequency: originalAddData.frequencyData?.isYearlyFrequency || false
1792
+ }
1793
+ };
1794
+
1795
+ // 🧠 replace original product additional data
1796
+ productData.additionalData = subscriptionPayload;
1797
+
1798
+ console.log("UPDATED additionalData:", subscriptionPayload);
1799
+ } catch (error) {
1800
+ console.error("Error updating subscription data:", error);
1801
+ showSubscriptionError("Error updating subscription settings. Please try again.");
1802
+ }
1803
+ }
1804
+
1805
+
1806
+ function toggleComboSelection(btn) {
1807
+ const group = btn.dataset.group;
1808
+ const productId = btn.dataset.productId;
1809
+ const item = productData.combinations.find(x => x.productId == productId);
1810
+
1811
+ if (!bundleSelections[group]) bundleSelections[group] = [];
1812
+
1813
+ const selected = bundleSelections[group];
1814
+
1815
+ // deselect
1816
+ if (selected.includes(productId)) {
1817
+ bundleSelections[group] = selected.filter(x => x !== productId);
1818
+ btn.classList.remove("selected");
1819
+ updateBundlePriceUI();
1820
+ return;
1821
+ }
1822
+
1823
+ // ENFORCE MAX RULE
1824
+ if (selected.length >= item.maximumSelectable) {
1825
+ showCombinationError(`You can select only ${item.maximumSelectable} item${item.maximumSelectable > 1 ? 's' : ''} from "${item.groupName}".`);
1826
+ return;
1827
+ }
1828
+
1829
+ // select
1830
+ bundleSelections[group].push(productId);
1831
+ btn.classList.add("selected");
1832
+
1833
+ updateBundlePriceUI();
1834
+ }
1835
+
1836
+ //-------------------------------
1837
+ // VALIDATE BEFORE CART ADDING
1838
+ //-------------------------------
1839
+ function validateBundle() {
1840
+ for (const group in bundleSelections) {
1841
+ const items = productData.combinations.filter(x => x.groupName == group);
1842
+ const min = items[0].minimumSelectable;
1843
+ const selectedCount = bundleSelections[group].length;
1844
+
1845
+ if (selectedCount < min) {
1846
+ showCombinationError(`Please select at least ${min} item${min > 1 ? 's' : ''} from "${group}".`);
1847
+ return false;
1848
+ }
1849
+ }
1850
+ return true;
1851
+ }
1852
+
1853
+ function updateBundlePriceUI() {
1854
+ let total = 0;
1855
+ const items = [];
1856
+
1857
+ // Don't interfere with subscription products (productType == 90)
1858
+ const isSubscriptionProduct = productData.productType == 90;
1859
+
1860
+ // Parse additionalData (string → JSON)
1861
+ const additionalData = parseAdditionalData(productData.additionalData);
1862
+
1863
+ // Check if this is a combination product
1864
+ const hasCombinations = productData.combinations && productData.combinations.length > 0;
1865
+
1866
+ if (hasCombinations && Object.keys(bundleSelections).length > 0) {
1867
+ // Combination-based pricing - build Items array
1868
+ for (const g in bundleSelections) {
1869
+ bundleSelections[g].forEach(productId => {
1870
+ const item = productData.combinations.find(c => c.productId == productId);
1871
+ if (item) {
1872
+ const quantity = item.quantity || 1;
1873
+ total += item.prices.price * quantity;
1874
+
1875
+ // Add to Items array for additionalSettings
1876
+ items.push({
1877
+ Id: parseInt(item.productId),
1878
+ Name: item.name || '',
1879
+ Price: parseFloat(item.prices.price || 0),
1880
+ Quantity: parseInt(quantity)
1881
+ });
1882
+ }
1883
+ });
1884
+ }
1885
+
1886
+ // Only update additionalData for combination products (not subscription products)
1887
+ if (!isSubscriptionProduct) {
1888
+ // Update additionalData with only Items structure
1889
+ productData.additionalData = {
1890
+ Items: items
1891
+ };
1892
+ }
1893
+ } else if (hasCombinations && !isSubscriptionProduct) {
1894
+ // Use main product price
1895
+ total = productData.prices.price || 0;
1896
+
1897
+ // Clear additionalData if no combinations selected (only for non-subscription products)
1898
+ productData.additionalData = {
1899
+ Items: []
1900
+ };
1901
+ } else {
1902
+ // Use main product price or existing pricing logic
1903
+ if (additionalData.IsCombinationPrice === true && hasCombinations) {
1904
+ // Combination-based pricing from existing data
1905
+ for (const g in bundleSelections) {
1906
+ bundleSelections[g].forEach(productId => {
1907
+ const item = productData.combinations.find(c => c.productId == productId);
1908
+ if (item) {
1909
+ total += item.prices.price * (item.quantity || 1);
1910
+ }
1911
+ });
1912
+ }
1913
+ } else {
1914
+ // Use main product price
1915
+ total = productData.prices.price || 0;
1916
+ }
1917
+ }
1918
+
1919
+ if (priceElement) {
1920
+ priceElement.textContent = formatMoney(total);
1921
+ }
1922
+ }
1923
+
1924
+ // Build option groups from variants
1925
+ function buildOptionGroups() {
1926
+ const variants = productData.variations || productData.variants || [];
1927
+ const combinations = productData.combinations || [];
1928
+ const subscriptions = productData.subscriptions || [];
1929
+ if ((!variants || variants.length === 0) && (!combinations || combinations.length === 0) && (!subscriptions || subscriptions.length === 0)) return null;
1930
+
1931
+ if(combinations && combinations.length > 0){
1932
+ renderCombinationUI();
1933
+ }
1934
+
1935
+ if(subscriptions && subscriptions.length > 0){
1936
+ renderSubscriptionUI();
1937
+ }
1938
+ if (variants || variants.length > 0) {
1939
+ const optionGroups = {};
1940
+
1941
+ // Process each variation
1942
+ variants.forEach(variation => {
1943
+ const options = variation.options || [];
1944
+
1945
+
1946
+ options.forEach(option => {
1947
+ const optionName = (option.optionName || 'Option').toLowerCase();
1948
+ const cleanName = optionName.replace(/[^a-z]/g, ''); // Remove non-alphabetic chars
1949
+
1950
+ // Map common option names
1951
+ let mappedName = cleanName;
1952
+ if (cleanName.includes('color') || cleanName.includes('colour')) {
1953
+ mappedName = 'color';
1954
+ } else if (cleanName.includes('size')) {
1955
+ mappedName = 'size';
1956
+ }
1957
+
1958
+ if (!optionGroups[mappedName]) {
1959
+ optionGroups[mappedName] = {
1960
+ name: mappedName === 'color' ? 'Color' : (mappedName === 'size' ? 'Size' : option.optionName || 'Option'),
1961
+ type: option.displayType || (mappedName === 'color' ? 'color' : 'text'),
1962
+ values: new Map()
1963
+ };
1964
+ }
1965
+
1966
+ const value = option.value || '';
1967
+ if (!optionGroups[mappedName].values.has(value)) {
1968
+ optionGroups[mappedName].values.set(value, {
1969
+ value: value,
1970
+ available: variation.inStock !== false && variation.available !== false,
1971
+ images: variation.images || [],
1972
+ combinationId: combinations.length > 0 ? combinations[0].productId : variation.productId
1973
+ });
1974
+ }
1975
+ });
1976
+ });
1977
+
1978
+ return optionGroups;
1979
+ }
1980
+ }
1981
+
1982
+ // Render option groups
1983
+ function renderOptionGroups() {
1984
+ const optionGroups = buildOptionGroups();
1985
+ if (!optionGroups || Object.keys(optionGroups).length === 0) return;
1986
+
1987
+ optionsContainer.innerHTML = '';
1988
+ const variants = productData.variations || productData.variants || [];
1989
+
1990
+ // Initialize option selections from a real variant so multi-option products
1991
+ // (e.g. Color + Size) start with a valid combination.
1992
+ const initialVariant = variants.find(v => v && v.inStock !== false && v.available !== false) || variants[0];
1993
+ if (initialVariant && Array.isArray(initialVariant.options)) {
1994
+ initialVariant.options.forEach(opt => {
1995
+ const optName = (opt.optionName || 'Option').toLowerCase().replace(/[^a-z]/g, '');
1996
+ let mappedName = optName;
1997
+ if (optName.includes('color') || optName.includes('colour')) {
1998
+ mappedName = 'color';
1999
+ } else if (optName.includes('size')) {
2000
+ mappedName = 'size';
2001
+ }
2002
+ if (!selectedOptions[mappedName]) {
2003
+ selectedOptions[mappedName] = opt.value || '';
2004
+ }
2005
+ });
2006
+ }
2007
+
2008
+ // Sort options: color first, then size, then others
2009
+ const sortedKeys = Object.keys(optionGroups).sort((a, b) => {
2010
+ const order = { color: 1, size: 2 };
2011
+ return (order[a] || 99) - (order[b] || 99);
2012
+ });
2013
+
2014
+ sortedKeys.forEach((key, groupIndex) => {
2015
+ const group = optionGroups[key];
2016
+ const optionDiv = document.createElement('div');
2017
+ optionDiv.className = 'product-option';
2018
+
2019
+ const label = document.createElement('label');
2020
+ label.className = 'option-label';
2021
+ label.textContent = group.name;
2022
+ optionDiv.appendChild(label);
2023
+
2024
+ const valuesDiv = document.createElement('div');
2025
+ valuesDiv.className = 'option-values';
2026
+
2027
+ // Convert Map values to an array
2028
+ const valuesArray = Array.from(group.values.values());
2029
+ valuesArray.forEach((valueObj, index) => {
2030
+ if (!selectedOptions[key] && valueObj.available) {
2031
+ selectedOptions[key] = valueObj.value;
2032
+ } else if (!selectedOptions[key] && index === 0) {
2033
+ selectedOptions[key] = valueObj.value;
2034
+ }
2035
+
2036
+ const button = document.createElement('button');
2037
+ button.type = 'button';
2038
+ // Add .product-option-btn class so global click handler can detect these buttons
2039
+ button.className = `option-value option-value-${group.type} product-option-btn ${(selectedOptions[key] === valueObj.value) ? 'selected' : ''} ${!valueObj.available ? 'disabled' : ''}`;
2040
+ button.dataset.optionKey = key;
2041
+ button.dataset.optionValue = valueObj.value;
2042
+ button.dataset.combinationId = valueObj.combinationId;
2043
+ button.dataset.available = valueObj.available;
2044
+
2045
+ if (group.type === 'color') {
2046
+ // Color swatch
2047
+ const colorValue = valueObj.value.toLowerCase().trim();
2048
+ const colorMap = {
2049
+ 'red': '#ef4444',
2050
+ 'blue': '#3b82f6',
2051
+ 'green': '#10b981',
2052
+ 'yellow': '#fbbf24',
2053
+ 'black': '#000000',
2054
+ 'white': '#ffffff',
2055
+ 'gray': '#6b7280',
2056
+ 'grey': '#6b7280',
2057
+ 'pink': '#ec4899',
2058
+ 'purple': '#a855f7',
2059
+ 'orange': '#f97316',
2060
+ 'brown': '#92400e',
2061
+ 'navy': '#1e3a8a',
2062
+ 'tan': '#d4a574',
2063
+ 'beige': '#f5f5dc',
2064
+ 'cream': '#fffdd0',
2065
+ 'pumice': '#c8c5b9'
2066
+ };
2067
+
2068
+ const color = colorMap[colorValue] || colorValue;
2069
+ button.style.backgroundColor = color;
2070
+ button.style.borderColor = (color === '#ffffff' || color === '#fffdd0' || color === '#f5f5dc') ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.1)';
2071
+
2072
+ // Add screen reader text
2073
+ const srText = document.createElement('span');
2074
+ srText.className = 'sr-only';
2075
+ srText.textContent = valueObj.value;
2076
+ button.appendChild(srText);
2077
+ } else {
2078
+ // Text/Size button
2079
+ button.textContent = valueObj.value;
2080
+ }
2081
+
2082
+ if (!valueObj.available) {
2083
+ button.disabled = true;
2084
+ }
2085
+
2086
+ valuesDiv.appendChild(button);
2087
+ });
2088
+
2089
+ optionDiv.appendChild(valuesDiv);
2090
+ optionsContainer.appendChild(optionDiv);
2091
+ });
2092
+
2093
+ // Find initial variant
2094
+ findMatchingVariant();
2095
+ }
2096
+
2097
+
2098
+ function renderCombinationUI() {
2099
+ const combos = productData.combinations || [];
2100
+ if (combos.length === 0) return;
2101
+
2102
+ const grouped = {};
2103
+ combos.forEach(c => {
2104
+ if (!grouped[c.groupName]) grouped[c.groupName] = [];
2105
+ grouped[c.groupName].push(c);
2106
+ });
2107
+
2108
+ const container = document.getElementById("comboContainer");
2109
+ container.innerHTML = "";
2110
+
2111
+ Object.keys(grouped).forEach(groupName => {
2112
+ const items = grouped[groupName];
2113
+
2114
+ const header = document.createElement("div");
2115
+ header.className = "combo-group-header";
2116
+ header.innerHTML = `<strong>${groupName}</strong> (Select ${items[0].minimumSelectable}-${items[0].maximumSelectable})`;
2117
+ container.appendChild(header);
2118
+
2119
+ const groupDiv = document.createElement("div");
2120
+ groupDiv.className = "combo-group";
2121
+
2122
+ items.forEach(item => {
2123
+ const card = document.createElement("div");
2124
+ card.className = "combo-card";
2125
+ card.dataset.group = groupName;
2126
+ card.dataset.productId = item.productId;
2127
+ card.dataset.price = item.prices.price;
2128
+
2129
+ card.innerHTML = `
2130
+ <div class="combo-image">
2131
+ <img src="${item.thumbnailImage1.url}" alt="${item.name}" />
2132
+ <div class="combo-checkbox">
2133
+ <label>
2134
+ <input type="checkbox" />
2135
+ <span class="checkbox-custom"></span>
2136
+ </label>
2137
+ </div>
2138
+ </div>
2139
+ <div class="combo-info">
2140
+ <span class="combo-name">${item.name} × 1</span>
2141
+ <span class="combo-price">${formatMoney(item.prices.price)}</span>
2142
+ </div>`;
2143
+
2144
+ card.addEventListener("click", () => toggleComboSelection(card));
2145
+ groupDiv.appendChild(card);
2146
+ });
2147
+
2148
+ container.appendChild(groupDiv);
2149
+ });
2150
+
2151
+ // ⬇️ AUTO SELECT MINIMUM REQUIRED CARDS IN EACH GROUP
2152
+ Object.keys(grouped).forEach(groupName => {
2153
+ const items = grouped[groupName];
2154
+ const minSelectable = items[0]?.minimumSelectable || 1;
2155
+
2156
+ // Get all cards for this group
2157
+ const groupCards = Array.from(container.querySelectorAll(`.combo-card[data-group="${groupName}"]`));
2158
+
2159
+ // Select minimum required items
2160
+ let selected = 0;
2161
+ for (let i = 0; i < groupCards.length && selected < minSelectable; i++) {
2162
+ const card = groupCards[i];
2163
+ if (card && !card.classList.contains("selected")) {
2164
+ card.classList.add("selected");
2165
+ const checkbox = card.querySelector('input[type="checkbox"]');
2166
+ if (checkbox) {
2167
+ checkbox.checked = true;
2168
+ }
2169
+
2170
+ if (!bundleSelections[groupName]) bundleSelections[groupName] = [];
2171
+ bundleSelections[groupName].push(card.dataset.productId);
2172
+ selected++;
2173
+ }
2174
+ }
2175
+ });
2176
+
2177
+ updateBundlePriceUI();
2178
+ }
2179
+
2180
+
2181
+ // Find matching variant based on selected options
2182
+ function findMatchingVariant() {
2183
+ const variants = productData.variations || productData.variants || [];
2184
+
2185
+ // If no variants, use base product
2186
+ if (variants.length === 0) {
2187
+ currentVariant = {
2188
+ productId: productData.productId,
2189
+ price: productData.price,
2190
+ mrp: productData.mrp,
2191
+ showCallForPricing: productData.showCallForPricing,
2192
+ inStock: productData.inStock,
2193
+ available: productData.available
2194
+ };
2195
+ updateVariantUI();
2196
+ return;
2197
+ }
2198
+
2199
+ // Find matching variation
2200
+ for (const variation of variants) {
2201
+ const options = variation.options || [];
2202
+ let matches = true;
2203
+
2204
+ for (const [key, value] of Object.entries(selectedOptions)) {
2205
+ const hasMatchingOption = options.some(opt => {
2206
+ const optName = (opt.optionName || 'Option').toLowerCase().replace(/[^a-z]/g, '');
2207
+ let mappedName = optName;
2208
+ if (optName.includes('color') || optName.includes('colour')) {
2209
+ mappedName = 'color';
2210
+ } else if (optName.includes('size')) {
2211
+ mappedName = 'size';
2212
+ }
2213
+ const optValue = opt.value || '';
2214
+ return mappedName === key && optValue === value;
2215
+ });
2216
+
2217
+ if (!hasMatchingOption) {
2218
+ matches = false;
2219
+ break;
2220
+ }
2221
+ }
2222
+
2223
+ if (matches) {
2224
+ currentVariant = {
2225
+ productId: variation.productId,
2226
+ price: (variation.prices && variation.prices.price) || productData.price,
2227
+ mrp: (variation.prices && variation.prices.mrp) || productData.mrp,
2228
+ showCallForPricing: variation.showCallForPricing,
2229
+ inStock: variation.inStock !== false,
2230
+ available: variation.available !== false,
2231
+ images: variation.images || [],
2232
+ stockQuantity: variation.stockQuantity || 0
2233
+ };
2234
+ updateVariantUI();
2235
+ return;
2236
+ }
2237
+ }
2238
+
2239
+ // If no match found, use first variation
2240
+ if (variants.length > 0) {
2241
+ const firstVar = variants[0];
2242
+ currentVariant = {
2243
+ productId: firstVar.productId,
2244
+ price: (firstVar.prices && firstVar.prices.price) || productData.price,
2245
+ mrp: (firstVar.prices && firstVar.prices.mrp) || productData.mrp,
2246
+ showCallForPricing: firstVar.showCallForPricing,
2247
+ inStock: firstVar.inStock !== false,
2248
+ available: firstVar.available !== false,
2249
+ images: firstVar.images || [],
2250
+ stockQuantity: firstVar.stockQuantity || 0
2251
+ };
2252
+ } else {
2253
+ currentVariant = {
2254
+ productId: productData.productId,
2255
+ price: productData.price,
2256
+ mrp: productData.mrp,
2257
+ showCallForPricing: productData.showCallForPricing,
2258
+ inStock: productData.inStock,
2259
+ available: productData.available
2260
+ };
2261
+ }
2262
+
2263
+ updateVariantUI();
2264
+ }
2265
+
2266
+ // Update UI based on selected variant
2267
+ // Update product attributes based on current variant
2268
+ function updateAttributesUI() {
2269
+ if (!currentVariant) return;
2270
+
2271
+ try {
2272
+ const variants = productData.variations || productData.variants || [];
2273
+ const matchingVariant = variants.find(v => v.productId === currentVariant.productId);
2274
+
2275
+ // Select all attribute cards
2276
+ const attributeCards = document.querySelectorAll('.attributes-card[data-attribute-name]');
2277
+ if (!attributeCards || attributeCards.length === 0) return;
2278
+
2279
+ // Build map of possible attribute values from the variant
2280
+ const attributeValueMap = {};
2281
+
2282
+ if (matchingVariant) {
2283
+ // variantAttributes
2284
+ if (matchingVariant.variantAttributes && Array.isArray(matchingVariant.variantAttributes)) {
2285
+ matchingVariant.variantAttributes.forEach(a => {
2286
+ const name = a.name || a.attributeName;
2287
+ if (name) attributeValueMap[name] = a.value;
2288
+ });
2289
+ }
2290
+
2291
+ // options
2292
+ if (matchingVariant.options && Array.isArray(matchingVariant.options)) {
2293
+ matchingVariant.options.forEach(o => {
2294
+ const name = o.optionName || o.name;
2295
+ if (name) attributeValueMap[name] = o.value;
2296
+ });
2297
+ }
2298
+
2299
+ // attributes root
2300
+ if (matchingVariant.attributes && Array.isArray(matchingVariant.attributes)) {
2301
+ matchingVariant.attributes.forEach(a => {
2302
+ const name = a.name || a.attributeName;
2303
+ if (name) attributeValueMap[name] = a.value;
2304
+ });
2305
+ }
2306
+
2307
+ // additionalData
2308
+ if (matchingVariant.additionalData) {
2309
+ let add = matchingVariant.additionalData;
2310
+ if (typeof add === 'string') {
2311
+ try { add = JSON.parse(add); } catch(e) { add = null; }
2312
+ }
2313
+ if (add && add.attributes && Array.isArray(add.attributes)) {
2314
+ add.attributes.forEach(a => {
2315
+ const name = a.name || a.attributeName;
2316
+ if (name) attributeValueMap[name] = a.value;
2317
+ });
2318
+ }
2319
+ }
2320
+ }
2321
+
2322
+ // Update DOM cards
2323
+ attributeCards.forEach(card => {
2324
+ const name = card.getAttribute('data-attribute-name');
2325
+ const base = card.getAttribute('data-base-value');
2326
+ const valueEl = card.querySelector('.attribute-value-text');
2327
+ if (!valueEl) return;
2328
+
2329
+ // exact or case-insensitive match
2330
+ let newVal = null;
2331
+ if (attributeValueMap[name]) newVal = attributeValueMap[name];
2332
+ else {
2333
+ for (const k in attributeValueMap) {
2334
+ if (k && k.toLowerCase() === (name || '').toLowerCase()) { newVal = attributeValueMap[k]; break; }
2335
+ }
2336
+ }
2337
+
2338
+ if (newVal !== null && newVal !== undefined) {
2339
+ valueEl.textContent = newVal;
2340
+ valueEl.setAttribute('data-current-value', newVal);
2341
+ } else if (base !== null && base !== undefined) {
2342
+ valueEl.textContent = base;
2343
+ valueEl.setAttribute('data-current-value', base);
2344
+ }
2345
+ });
2346
+ } catch (err) {
2347
+ console.error('Error updating attributes UI', err);
2348
+ }
2349
+ }
2350
+
2351
+ function updateVariantUI() {
2352
+ if (!currentVariant) return;
2353
+
2354
+ // Update price
2355
+ if (priceElement) {
2356
+ const showCallForPricing = isCallForPricingEnabled(currentVariant) || isCallForPricingEnabled(productData);
2357
+ if (showCallForPricing) {
2358
+ priceElement.textContent = 'Call for pricing';
2359
+ } else {
2360
+ const currentPrice = Number(currentVariant.price || 0);
2361
+ priceElement.textContent = formatMoney(currentPrice);
2362
+ }
2363
+ }
2364
+
2365
+ if (priceCompareElement) {
2366
+ const showCallForPricing = isCallForPricingEnabled(currentVariant) || isCallForPricingEnabled(productData);
2367
+ if (showCallForPricing) {
2368
+ priceCompareElement.style.display = 'none';
2369
+ } else {
2370
+ const currentPrice = Number(currentVariant.price || 0);
2371
+ const currentMrp = Number(currentVariant.mrp || 0);
2372
+ if (currentMrp > currentPrice) {
2373
+ priceCompareElement.textContent = formatMoney(currentMrp);
2374
+ priceCompareElement.style.display = '';
2375
+ } else {
2376
+ priceCompareElement.style.display = 'none';
2377
+ }
2378
+ }
2379
+ }
2380
+
2381
+ // Update availability
2382
+ if (addToCartBtn) {
2383
+ addToCartBtn.disabled = !currentVariant.available;
2384
+ const btnText = addToCartBtn.querySelector('.btn-text');
2385
+ if (btnText) {
2386
+ if(productData.productType == 90){
2387
+ btnText.textContent = 'Subscribe';
2388
+ }else{
2389
+ btnText.textContent = currentVariant.available ? 'Add to Cart' : 'Out of Stock';
2390
+ }
2391
+ }
2392
+ }
2393
+
2394
+ if (currentVariant.images && currentVariant.images.length > 0) {
2395
+ applyVariantGalleryImages(currentVariant.images);
2396
+ } else {
2397
+ restoreBaseGalleryImages();
2398
+ }
2399
+
2400
+ // Update quantity max
2401
+ if (quantityInput && currentVariant.stockQuantity) {
2402
+ quantityInput.max = currentVariant.stockQuantity;
2403
+ }
2404
+
2405
+ // Update product attributes based on variant
2406
+ updateAttributesUI();
2407
+ }
2408
+
2409
+ // Switch to image by index
2410
+ function switchToImage(index) {
2411
+ if (!mainImages || mainImages.length === 0) return;
2412
+
2413
+ mainImages.forEach((img, i) => {
2414
+ if (i === index) {
2415
+ img.classList.add('active');
2416
+ currentImageIndex = index;
2417
+ } else {
2418
+ img.classList.remove('active');
2419
+ }
2420
+ });
2421
+
2422
+ updateThumbnails();
2423
+ }
2424
+
2425
+ // Update thumbnails active state
2426
+ function updateThumbnails() {
2427
+ if (!thumbnails || thumbnails.length === 0) return;
2428
+ thumbnails.forEach((thumb, index) => {
2429
+ if (parseInt(thumb.dataset.index) === currentImageIndex) {
2430
+ thumb.classList.add('active');
2431
+ } else {
2432
+ thumb.classList.remove('active');
2433
+ }
2434
+ });
2435
+ }
2436
+
2437
+ document.addEventListener("click", (e) => {
2438
+ const btn = e.target.closest(".product-option-btn");
2439
+ if (!btn) return;
2440
+
2441
+ const key = btn.dataset.optionKey;
2442
+ const value = btn.dataset.optionValue;
2443
+
2444
+ // Update selected option state
2445
+ if (key && value !== undefined) {
2446
+ selectedOptions[key] = value;
2447
+ }
2448
+
2449
+ // Toggle selected styling within this option group
2450
+ document.querySelectorAll(`.product-option-btn[data-option-key="${key}"]`)
2451
+ .forEach(b => b.classList.remove("selected"));
2452
+
2453
+ btn.classList.add("selected");
2454
+
2455
+ // Recalculate matching variant and refresh UI (price, stock, images, attributes, etc.)
2456
+ findMatchingVariant();
2457
+ });
2458
+
2459
+ // Initialize on DOM ready
2460
+ document.addEventListener('DOMContentLoaded', () => {
2461
+ // Initialize DOM elements
2462
+ refreshGalleryElements();
2463
+ baseMainImageUrls = Array.from(mainImages || []).map(img => img?.getAttribute('src') || img?.src || '').filter(Boolean);
2464
+ baseThumbnailImageUrls = Array.from(thumbnails || []).map(thumb => {
2465
+ const thumbImg = thumb.querySelector('img');
2466
+ return thumb?.dataset?.image || thumbImg?.getAttribute('src') || thumbImg?.src || '';
2467
+ }).filter(Boolean);
2468
+ productForm = document.getElementById('productForm');
2469
+ addToCartBtn = document.getElementById('addToCartBtn');
2470
+ quantityInput = document.getElementById('quantity');
2471
+ priceElement = document.getElementById('productPrice');
2472
+ priceCompareElement = document.querySelector('.price-compare');
2473
+ optionsContainer = document.getElementById('productOptionsContainer');
2474
+ galleryModal = document.getElementById('galleryModal');
2475
+ galleryModalImage = document.getElementById('galleryModalImage');
2476
+ galleryModalClose = document.getElementById('galleryModalClose');
2477
+ galleryModalPrev = document.getElementById('galleryModalPrev');
2478
+ galleryModalNext = document.getElementById('galleryModalNext');
2479
+ galleryModalCounter = document.getElementById('galleryModalCounter');
2480
+ galleryZoomBtn = document.getElementById('galleryZoomBtn');
2481
+ cartMessage = document.getElementById('cartMessage');
2482
+
2483
+ // Render option groups
2484
+ renderOptionGroups();
2485
+
2486
+ // Initialize shipping methods
2487
+ initializeShippingMethods();
2488
+
2489
+ // Initialize Product Attributes Tabs
2490
+ const attributeTabLinks = document.querySelectorAll('.attributes-tab-link');
2491
+ attributeTabLinks.forEach(link => {
2492
+ link.addEventListener('click', (e) => {
2493
+ e.preventDefault();
2494
+ const tabId = e.currentTarget.getAttribute('data-tab');
2495
+
2496
+ // Remove active class from all links and panes
2497
+ attributeTabLinks.forEach(l => l.classList.remove('active'));
2498
+ document.querySelectorAll('.attributes-tab-pane').forEach(pane => pane.classList.remove('active'));
2499
+
2500
+ // Add active class to clicked link and corresponding pane
2501
+ e.currentTarget.classList.add('active');
2502
+ e.currentTarget.setAttribute('aria-selected', 'true');
2503
+
2504
+ const activePane = document.getElementById(tabId);
2505
+ if (activePane) {
2506
+ activePane.classList.add('active');
2507
+ }
2508
+ });
2509
+ });
2510
+
2511
+ // Image Gallery Thumbnails (delegated, supports dynamic thumbnail rebuild)
2512
+ document.addEventListener('click', (e) => {
2513
+ const thumbnail = e.target.closest('.gallery-thumbnail');
2514
+ if (!thumbnail) return;
2515
+ const imageIndex = parseInt(thumbnail.dataset.index, 10);
2516
+ if (Number.isFinite(imageIndex)) {
2517
+ switchToImage(imageIndex);
2518
+ }
2519
+ });
2520
+
2521
+ // Option selection
2522
+ document.addEventListener('click', (e) => {
2523
+ const optionBtn = e.target.closest('.option-value');
2524
+ if (!optionBtn || optionBtn.disabled) return;
2525
+
2526
+ const optionKey = optionBtn.dataset.optionKey;
2527
+ const optionValue = optionBtn.dataset.optionValue;
2528
+
2529
+ if (!optionKey || !optionValue) return;
2530
+
2531
+ // Deselect other options in same group
2532
+ const optionGroup = optionBtn.closest('.product-option');
2533
+ optionGroup.querySelectorAll('.option-value').forEach(btn => {
2534
+ btn.classList.remove('selected');
2535
+ });
2536
+
2537
+ // Select clicked option
2538
+ optionBtn.classList.add('selected');
2539
+
2540
+ // Update selected options
2541
+ selectedOptions[optionKey] = optionValue;
2542
+
2543
+ // Find matching variant
2544
+ findMatchingVariant();
2545
+
2546
+ // Explicitly update attributes immediately
2547
+ setTimeout(() => {
2548
+ updateAttributesUI();
2549
+ }, 50);
2550
+ });
2551
+
2552
+ // Quantity Controls
2553
+ const decreaseBtn = document.querySelector('.quantity-decrease');
2554
+ const increaseBtn = document.querySelector('.quantity-increase');
2555
+
2556
+ if (decreaseBtn && quantityInput) {
2557
+ decreaseBtn.addEventListener('click', () => {
2558
+ const val = parseInt(quantityInput.value) || 1;
2559
+ if (val > 1) {
2560
+ quantityInput.value = val - 1;
2561
+ }
2562
+ });
2563
+ }
2564
+
2565
+ if (increaseBtn && quantityInput) {
2566
+ increaseBtn.addEventListener('click', () => {
2567
+ const val = parseInt(quantityInput.value) || 1;
2568
+ const max = parseInt(quantityInput.max) || 99;
2569
+ if (val < max) {
2570
+ quantityInput.value = val + 1;
2571
+ }
2572
+ });
2573
+ }
2574
+
2575
+ // Add to Cart
2576
+ if (productForm && addToCartBtn) {
2577
+ productForm.addEventListener('submit', async (e) => {
2578
+ e.preventDefault();
2579
+
2580
+ if (addToCartBtn.disabled || addToCartBtn.classList.contains('loading')) return;
2581
+
2582
+ const productId = currentVariant ? currentVariant.productId : productData.productId;
2583
+ const quantity = quantityInput != null && quantityInput.value != null && quantityInput.value != ""? parseInt(quantityInput.value) : 1;
2584
+
2585
+ addToCartBtn.classList.add('loading');
2586
+ const btnText = addToCartBtn.querySelector('.btn-text');
2587
+ if (btnText) {
2588
+ btnText.textContent = 'Adding...';
2589
+ }
2590
+
2591
+ try {
2592
+ // Validate subscription before submission
2593
+ if (productData.productType == 90) {
2594
+ const validation = validateSubscription();
2595
+ if (!validation.valid) {
2596
+ showSubscriptionError(validation.message);
2597
+ addToCartBtn.classList.remove('loading');
2598
+ if (btnText) {
2599
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
2600
+ }
2601
+ return;
2602
+ }
2603
+ clearSubscriptionError();
2604
+ }
2605
+
2606
+ // Validate combination products before submission
2607
+ const hasCombinations = productData.combinations && productData.combinations.length > 0;
2608
+ if (hasCombinations) {
2609
+ const bundleValidation = validateBundle();
2610
+ if (!bundleValidation) {
2611
+ addToCartBtn.classList.remove('loading');
2612
+ if (btnText) {
2613
+ btnText.textContent = 'Add to Cart';
2614
+ }
2615
+ return;
2616
+ }
2617
+ }
2618
+
2619
+ // Store variation image in localStorage before adding to cart
2620
+ if (currentVariant && currentVariant.images && currentVariant.images.length > 0) {
2621
+ const firstVariantImage = currentVariant.images[0];
2622
+ const variantImageUrl = typeof firstVariantImage === 'string'
2623
+ ? firstVariantImage
2624
+ : (firstVariantImage.url || firstVariantImage.Url || firstVariantImage);
2625
+
2626
+ if (variantImageUrl) {
2627
+ // Store variation image for this productId
2628
+ const imageKey = `variantImage_${productId}`;
2629
+ try {
2630
+ localStorage.setItem(imageKey, variantImageUrl);
2631
+ } catch (e) {
2632
+ console.warn('Failed to store variant image in localStorage:', e);
2633
+ }
2634
+ }
2635
+ }
2636
+
2637
+ let bodyData = {
2638
+ productId: parseInt(productId),
2639
+ quantity: quantity
2640
+ };
2641
+
2642
+ // If product type is subscription → attach Additional Settings
2643
+ if (productData.productType == 90) {
2644
+ try {
2645
+ bodyData.additionalSettings = typeof(productData.additionalData) == "string"
2646
+ ? productData.additionalData
2647
+ : JSON.stringify(productData.additionalData);
2648
+ } catch(e) {
2649
+ console.warn("Invalid additionalData JSON");
2650
+ showSubscriptionError("Error preparing subscription data. Please try again.");
2651
+ addToCartBtn.classList.remove('loading');
2652
+ if (btnText) {
2653
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
2654
+ }
2655
+ return;
2656
+ }
2657
+ }
2658
+ // If product has combinations (and is NOT a subscription) → attach Additional Settings with Items array
2659
+ else {
2660
+ const hasCombinations = productData.combinations && productData.combinations.length > 0;
2661
+ if (hasCombinations) {
2662
+ try {
2663
+ // Ensure bundle selections are up to date
2664
+ updateBundlePriceUI();
2665
+
2666
+ // Get the Items array from additionalData
2667
+ const combinationData = productData.additionalData || {};
2668
+ const items = combinationData.Items || [];
2669
+
2670
+ // Only include additionalSettings if there are selected items
2671
+ if (items.length > 0) {
2672
+ bodyData.additionalSettings = JSON.stringify({
2673
+ Items: items
2674
+ });
2675
+ }
2676
+ } catch(e) {
2677
+ console.warn("Error preparing combination data:", e);
2678
+ addToCartBtn.classList.remove('loading');
2679
+ if (btnText) {
2680
+ btnText.textContent = 'Add to Cart';
2681
+ }
2682
+ return;
2683
+ }
2684
+ }
2685
+ }
2686
+
2687
+ const response = await fetch('/webstoreapi/carts/add', {
2688
+ method: 'POST',
2689
+ headers: {
2690
+ 'Content-Type': 'application/json',
2691
+ 'X-Requested-With': 'XMLHttpRequest'
2692
+ },
2693
+ body: JSON.stringify(bodyData)
2694
+ });
2695
+
2696
+ const contentType = response.headers.get('content-type') || '';
2697
+ const isJson = contentType.includes('application/json');
2698
+ let data = {};
2699
+
2700
+ if (isJson) {
2701
+ data = await response.json();
2702
+ } else if (!response.ok) {
2703
+ // Non-JSON error response (often auth/session middleware HTML response)
2704
+ if (response.status === 401 || response.status === 403 || response.status === 404) {
2705
+ openLoginModal();
2706
+ if (btnText) {
2707
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
2708
+ }
2709
+ addToCartBtn.classList.remove('loading');
2710
+ return;
2711
+ }
2712
+ throw new Error('Failed to add to cart');
2713
+ }
2714
+
2715
+ const responseMessage = String(data.error || data.message || '').toLowerCase();
2716
+ const looksAuthError =
2717
+ data.requiresAuth ||
2718
+ response.status === 401 ||
2719
+ response.status === 403 ||
2720
+ response.status === 404 ||
2721
+ responseMessage.includes('auth') ||
2722
+ responseMessage.includes('login') ||
2723
+ responseMessage.includes('sign in') ||
2724
+ responseMessage.includes('unauthorized') ||
2725
+ responseMessage.includes('forbidden') ||
2726
+ responseMessage.includes('session');
2727
+
2728
+ if (looksAuthError) {
2729
+ openLoginModal();
2730
+ if (btnText) {
2731
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
2732
+ }
2733
+ addToCartBtn.classList.remove('loading');
2734
+ return;
2735
+ }
2736
+
2737
+ if (data.success) {
2738
+ // Fetch cart count after successful add to ensure instant update (like other pages)
2739
+ // Use CartManager to get cart count, which uses /carts/quantity API
2740
+ let cartCount = 0;
2741
+ try {
2742
+ if (window.CartManager && typeof window.CartManager.getCartCount === 'function') {
2743
+ // Force refresh to get the latest count after adding item
2744
+ cartCount = await window.CartManager.getCartCount(true);
2745
+ // Update cart data with the fetched count
2746
+ data.data = data.data || {};
2747
+ data.data.itemCount = cartCount;
2748
+ // Dispatch cart updated event to update all badges instantly
2749
+ if (window.CartManager && typeof window.CartManager.dispatchCartUpdated === 'function') {
2750
+ window.CartManager.dispatchCartUpdated({ itemCount: cartCount, cart: data.data });
2751
+ }
2752
+ } else {
2753
+ // Fallback to direct fetch if CartManager not available
2754
+ const countResponse = await fetch('/webstoreapi/carts/quantity', {
2755
+ method: 'GET',
2756
+ credentials: 'same-origin',
2757
+ headers: { 'Accept': 'application/json' }
2758
+ });
2759
+ if (countResponse.ok) {
2760
+ const countData = await countResponse.json();
2761
+ if (countData.success && countData.data) {
2762
+ cartCount = countData.data.cartQuantity || 0;
2763
+ data.data = data.data || {};
2764
+ data.data.itemCount = cartCount;
2765
+ // Fallback: update badges manually if CartManager not available
2766
+ const countElements = document.querySelectorAll('[data-cart-count]');
2767
+ countElements.forEach(element => {
2768
+ element.textContent = cartCount;
2769
+ element.setAttribute('data-cart-count', cartCount.toString());
2770
+ if (cartCount > 0) {
2771
+ element.removeAttribute('style');
2772
+ } else {
2773
+ const isDrawerTitle = element.closest('.cart-drawer-title');
2774
+ if (!isDrawerTitle) {
2775
+ element.style.display = 'none';
2776
+ }
2777
+ }
2778
+ });
2779
+ }
2780
+ }
2781
+ }
2782
+ } catch (e) {
2783
+ console.warn('Failed to fetch cart count after add:', e);
2784
+ // If we have itemCount in the response, use it as fallback
2785
+ if (data.data && (data.data.itemCount !== undefined || data.data.items)) {
2786
+ cartCount = data.data.itemCount || (data.data.items ? data.data.items.length : 0);
2787
+ }
2788
+ }
2789
+
2790
+ // Show success message - match widget format
2791
+ const successMessage = productData.productType == 90
2792
+ ? 'Subscription added to cart successfully!'
2793
+ : 'Product added to cart!';
2794
+ showUserMessage(successMessage, 'success');
2795
+
2796
+ if (btnText) {
2797
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
2798
+ }
2799
+ addToCartBtn.classList.remove('loading');
2800
+
2801
+ // Clear any validation errors
2802
+ clearSubscriptionError();
2803
+ showCombinationError("");
2804
+
2805
+ // Update cart UI with the latest data (includes total and count)
2806
+ if (window.Theme && typeof window.Theme.updateCartUI === 'function') {
2807
+ window.Theme.updateCartUI(data.data);
2808
+ } else if (window.theme && typeof window.theme.updateCartUI === 'function') {
2809
+ window.theme.updateCartUI(data.data);
2810
+ }
2811
+ } else {
2812
+ throw new Error(data.error || 'Failed to add to cart');
2813
+ }
2814
+ } catch (error) {
2815
+ console.error('Error adding to cart:', error);
2816
+
2817
+ const errorMessage = String(error.message || '').toLowerCase();
2818
+ const isAuthCookiePresent =
2819
+ document.cookie.includes('O2VENDIsUserLoggedin=true') ||
2820
+ document.cookie.includes('O2VENDIsUserLoggedin=1') ||
2821
+ document.cookie.includes('O2VENDUserToken=');
2822
+ const looksAuthError =
2823
+ errorMessage.includes('auth') ||
2824
+ errorMessage.includes('login') ||
2825
+ errorMessage.includes('sign in') ||
2826
+ errorMessage.includes('unauthorized') ||
2827
+ errorMessage.includes('forbidden') ||
2828
+ errorMessage.includes('session');
2829
+
2830
+ if (looksAuthError || !isAuthCookiePresent) {
2831
+ openLoginModal();
2832
+ } else {
2833
+ // Show user-friendly error message for non-auth failures
2834
+ showUserMessage(error.message || 'Failed to add item to cart. Please try again.', 'error');
2835
+ }
2836
+
2837
+ if (btnText) {
2838
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
2839
+ }
2840
+ addToCartBtn.classList.remove('loading');
2841
+ }
2842
+ });
2843
+ }
2844
+
2845
+ // Full Screen Gallery
2846
+ if (galleryZoomBtn && galleryModal) {
2847
+ const getCurrentGalleryImageUrls = () => {
2848
+ if (thumbnails && thumbnails.length > 0) {
2849
+ const thumbUrls = Array.from(thumbnails)
2850
+ .filter(thumb => thumb.style.display !== 'none')
2851
+ .map(thumb => thumb.dataset.image || thumb.querySelector('img')?.src || '')
2852
+ .filter(Boolean);
2853
+ if (thumbUrls.length > 0) return thumbUrls;
2854
+ }
2855
+ if (mainImages && mainImages.length > 0) {
2856
+ return Array.from(mainImages)
2857
+ .filter(img => img.style.display !== 'none')
2858
+ .map(img => img.getAttribute('src') || img.src || '')
2859
+ .filter(Boolean);
2860
+ }
2861
+ const images = productData.images || [];
2862
+ return images.map(img => typeof img === 'string' ? img : (img.url || img.Url || '')).filter(Boolean);
2863
+ };
2864
+
2865
+ if (getCurrentGalleryImageUrls().length > 0) {
2866
+ galleryZoomBtn.style.display = 'flex';
2867
+
2868
+ galleryZoomBtn.addEventListener('click', () => {
2869
+ const imageUrls = getCurrentGalleryImageUrls();
2870
+ if (imageUrls.length === 0) return;
2871
+ currentImageIndex = 0;
2872
+ galleryModalImage.src = imageUrls[currentImageIndex];
2873
+ updateGalleryModal(imageUrls);
2874
+ galleryModal.classList.add('active');
2875
+ document.body.style.overflow = 'hidden';
2876
+ });
2877
+
2878
+ galleryModalPrev.addEventListener('click', () => {
2879
+ const imageUrls = getCurrentGalleryImageUrls();
2880
+ if (imageUrls.length === 0) return;
2881
+ if (currentImageIndex > 0) {
2882
+ currentImageIndex--;
2883
+ } else {
2884
+ currentImageIndex = imageUrls.length - 1;
2885
+ }
2886
+ galleryModalImage.src = imageUrls[currentImageIndex];
2887
+ updateGalleryModal(imageUrls);
2888
+ });
2889
+
2890
+ galleryModalNext.addEventListener('click', () => {
2891
+ const imageUrls = getCurrentGalleryImageUrls();
2892
+ if (imageUrls.length === 0) return;
2893
+ if (currentImageIndex < imageUrls.length - 1) {
2894
+ currentImageIndex++;
2895
+ } else {
2896
+ currentImageIndex = 0;
2897
+ }
2898
+ galleryModalImage.src = imageUrls[currentImageIndex];
2899
+ updateGalleryModal(imageUrls);
2900
+ });
2901
+
2902
+ function updateGalleryModal(imageUrls) {
2903
+ if (!imageUrls || imageUrls.length === 0) {
2904
+ galleryModalCounter.textContent = '0 / 0';
2905
+ return;
2906
+ }
2907
+ galleryModalCounter.textContent = `${currentImageIndex + 1} / ${imageUrls.length}`;
2908
+ }
2909
+ } else {
2910
+ if (galleryZoomBtn) galleryZoomBtn.style.display = 'none';
2911
+ }
2912
+
2913
+ galleryModalClose.addEventListener('click', () => {
2914
+ galleryModal.classList.remove('active');
2915
+ document.body.style.overflow = '';
2916
+ });
2917
+
2918
+ galleryModal.addEventListener('click', (e) => {
2919
+ if (e.target === galleryModal || e.target.classList.contains('gallery-modal-overlay')) {
2920
+ galleryModal.classList.remove('active');
2921
+ document.body.style.overflow = '';
2922
+ }
2923
+ });
2924
+
2925
+ // Keyboard navigation
2926
+ document.addEventListener('keydown', (e) => {
2927
+ if (!galleryModal.classList.contains('active')) return;
2928
+
2929
+ if (e.key === 'Escape') {
2930
+ galleryModal.classList.remove('active');
2931
+ document.body.style.overflow = '';
2932
+ } else if (e.key === 'ArrowLeft' && galleryModalPrev) {
2933
+ galleryModalPrev.click();
2934
+ } else if (e.key === 'ArrowRight' && galleryModalNext) {
2935
+ galleryModalNext.click();
2936
+ }
2937
+ });
2938
+ }
2939
+ });
2940
+ })();