@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.
- package/README.md +4 -0
- package/lib/lib/dev-server.js +344 -48
- package/lib/lib/liquid-engine.js +3 -1
- package/lib/lib/mock-data.js +473 -119
- package/lib/lib/widget-service.js +12 -4
- package/package.json +2 -2
- package/test-theme/assets/async-sections.js +32 -24
- package/test-theme/assets/cart-drawer.js +20 -22
- package/test-theme/assets/cart-manager.js +1 -15
- package/test-theme/assets/checkout-price-handler.js +12 -11
- package/test-theme/assets/checkout.css +1415 -0
- package/test-theme/assets/checkout.js +3174 -0
- package/test-theme/assets/components.css +178 -29
- package/test-theme/assets/delivery-zone.js +1 -1
- package/test-theme/assets/product-detail.css +1050 -0
- package/test-theme/assets/product-detail.js +2940 -0
- package/test-theme/assets/theme.css +95 -120
- package/test-theme/assets/theme.js +781 -186
- package/test-theme/layout/theme.liquid +91 -17
- package/test-theme/sections/content.liquid +64 -57
- package/test-theme/sections/footer-fallback.liquid +57 -7
- package/test-theme/sections/footer.liquid +63 -12
- package/test-theme/sections/header-fallback.liquid +41 -41
- package/test-theme/sections/header.liquid +41 -51
- package/test-theme/sections/hero-fallback.liquid +1 -1
- package/test-theme/sections/hero.liquid +159 -136
- package/test-theme/snippets/account-sidebar.liquid +121 -29
- package/test-theme/snippets/add-to-cart-modal.liquid +258 -206
- package/test-theme/snippets/breadcrumbs.liquid +98 -11
- package/test-theme/snippets/cart-drawer.liquid +93 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +101 -15
- package/test-theme/snippets/delivery-zone-modal.liquid +529 -84
- package/test-theme/snippets/delivery-zone-search.liquid +104 -18
- package/test-theme/snippets/login-modal.liquid +269 -82
- package/test-theme/snippets/mega-menu.liquid +130 -43
- package/test-theme/snippets/news-thumbnail.liquid +120 -28
- package/test-theme/snippets/pagination.liquid +1 -1
- package/test-theme/snippets/price.liquid +100 -9
- package/test-theme/snippets/product-card-related.liquid +22 -4
- package/test-theme/snippets/product-card-simple.liquid +521 -25
- package/test-theme/snippets/product-card.liquid +145 -232
- package/test-theme/snippets/rating.liquid +100 -9
- package/test-theme/snippets/skeleton-collection-grid.liquid +94 -8
- package/test-theme/snippets/skeleton-product-card.liquid +102 -16
- package/test-theme/snippets/skeleton-product-grid.liquid +87 -1
- package/test-theme/snippets/social-sharing.liquid +133 -32
- package/test-theme/templates/account/dashboard.liquid +30 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +29 -28
- package/test-theme/templates/account/loyalty.liquid +45 -43
- package/test-theme/templates/account/order-detail.liquid +15 -8
- package/test-theme/templates/account/orders.liquid +189 -35
- package/test-theme/templates/account/profile.liquid +509 -114
- package/test-theme/templates/account/register.liquid +18 -8
- package/test-theme/templates/account/return-orders.liquid +31 -30
- package/test-theme/templates/account/store-credit.liquid +27 -26
- package/test-theme/templates/account/subscriptions.liquid +22 -5
- package/test-theme/templates/account/wishlist.liquid +88 -19
- package/test-theme/templates/address-book.liquid +166 -69
- package/test-theme/templates/categories.liquid +90 -30
- package/test-theme/templates/checkout.liquid +137 -3834
- package/test-theme/templates/error.liquid +23 -21
- package/test-theme/templates/index.liquid +29 -0
- package/test-theme/templates/login.liquid +33 -6
- package/test-theme/templates/order-confirmation.liquid +67 -9
- package/test-theme/templates/page.liquid +418 -206
- package/test-theme/templates/product-detail.liquid +124 -3878
- package/test-theme/templates/products.liquid +155 -30
- package/test-theme/templates/search.liquid +739 -225
- package/test-theme/widgets/brand-carousel.liquid +102 -82
- package/test-theme/widgets/brand.liquid +78 -50
- package/test-theme/widgets/carousel.liquid +253 -121
- package/test-theme/widgets/category-list-carousel.liquid +32 -8
- package/test-theme/widgets/category-list.liquid +21 -6
- package/test-theme/widgets/category.liquid +104 -37
- package/test-theme/widgets/discount-time.liquid +326 -119
- package/test-theme/widgets/footer-menu.liquid +115 -23
- package/test-theme/widgets/footer.liquid +118 -5
- package/test-theme/widgets/gallery.liquid +29 -5
- package/test-theme/widgets/header-menu.liquid +25 -13
- package/test-theme/widgets/header.liquid +64 -26
- package/test-theme/widgets/html.liquid +29 -6
- package/test-theme/widgets/news.liquid +6 -0
- package/test-theme/widgets/product-canvas.liquid +20 -12
- package/test-theme/widgets/product-carousel.liquid +118 -56
- package/test-theme/widgets/shared/product-grid.liquid +12 -0
- package/test-theme/widgets/single-product.liquid +688 -250
- package/test-theme/widgets/spacebar-carousel.liquid +39 -10
- package/test-theme/widgets/spacebar.liquid +77 -6
- package/test-theme/widgets/splash.liquid +40 -30
- 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
|
+
>−</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
|
+
})();
|