1/** 2 * Stock Validation - Vanilla JS Version 3 * 4 * This script provides client-side stock validation without jQuery dependency 5 * - Works with native JavaScript and fetch API 6 * - Graceful fallback for older browsers 7 * - Same functionality as jQuery version 8 */ 9 10window.StockValidator = (function() { 11 'use strict'; 12 13 const config = { 14 apiEndpoint: '/api/stock/check.php', 15 modalSelector: '#stock-modal-overlay', // We create our own modal 16 loadingClass: 'loading-stock', 17 disabledClass: 'stock-disabled', 18 debug: true // Enable debug logging 19 }; 20 21 /** 22 * Cross-browser event listener helper 23 */ 24 function addEventListener(element, event, handler) { 25 if (element.addEventListener) { 26 element.addEventListener(event, handler); 27 } else if (element.attachEvent) { 28 element.attachEvent('on' + event, handler); 29 } 30 } 31 32 /** 33 * Get stock information from embedded variant data (instant, no AJAX needed) 34 */ 35 function getStockFromEmbeddedData(variantId) { 36 // Check if embedded variant data exists 37 if (!window.__PD_VARIANTS_WITH_BRANCH__ || !Array.isArray(window.__PD_VARIANTS_WITH_BRANCH__)) { 38 return null; 39 } 40 41 const variants = window.__PD_VARIANTS_WITH_BRANCH__; 42 43 // SPECIAL CASE: Simple product (no variants) - empty array means simple product 44 if (variants.length === 0) { 45 // For simple products, we cannot determine stock from embedded data 46 // Return null to force API validation 47 return null; 48 } 49 50 // If no variant specified, check base product stock (all variants) 51 if (!variantId || variantId <= 0) { 52 // For base product, check if ANY variant has stock 53 const isPreorder = window.isPreorderProduct || false; 54 let hasAnyStock = false; 55 let totalStock = 0; 56 57 if (isPreorder) { 58 // For pre-order, check preorder_remaining 59 hasAnyStock = variants.some(v => { 60 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 61 return v.preorder_remaining > 0; 62 } 63 return v.stock > 0; 64 }); 65 totalStock = variants.reduce((sum, v) => { 66 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 67 return sum + (v.preorder_remaining || 0); 68 } 69 return sum + (v.stock || 0); 70 }, 0); 71 } else { 72 hasAnyStock = variants.some(v => v.stock > 0); 73 totalStock = variants.reduce((sum, v) => sum + (v.stock || 0), 0); 74 } 75 76 return { 77 hasStock: hasAnyStock, 78 stock: totalStock, 79 sku: 'Base Product', 80 variant: null 81 }; 82 } 83 84 // Find specific variant 85 const variant = variants.find(v => v.id === variantId); 86 87 if (!variant) { 88 return { 89 hasStock: false, 90 stock: 0, 91 sku: 'Unknown Variant', 92 variant: null 93 }; 94 } 95 96 // For pre-order products, check preorder_remaining instead of stock 97 const isPreorder = window.isPreorderProduct || false; 98 let hasStock = false; 99 let availableStock = 0; 100 101 if (isPreorder && 'preorder_remaining' in variant && variant.preorder_remaining !== null) { 102 availableStock = parseInt(variant.preorder_remaining, 10); 103 if (isNaN(availableStock)) availableStock = 0; 104 hasStock = availableStock > 0; 105 } else { 106 availableStock = variant.stock || 0; 107 hasStock = availableStock > 0; 108 } 109 110 return { 111 hasStock: hasStock, 112 stock: availableStock, 113 sku: variant.sku, 114 variant: variant 115 }; 116 } 117 118 /** 119 * Cross-browser AJAX helper 120 */
121 function makeRequest(url, options) { 122 return new Promise((resolve, reject) => { 123 // Try fetch first (modern browsers) 124 if (typeof fetch !== 'undefined') { 125 const fetchOptions = { 126 method: options.method || 'POST', 127 headers: { 128 'Content-Type': 'application/x-www-form-urlencoded', 129 }, 130 body: options.data ? new URLSearchParams(options.data).toString() : null 131 }; 132 133 fetch(url, fetchOptions) 134 .then(response => { 135 if (!response.ok) { 136 throw new Error(`HTTP ${response.status}`); 137 } 138 return response.json(); 139 }) 140 .then(resolve) 141 .catch(reject); 142 } else { 143 // Fallback to XMLHttpRequest 144 const xhr = new XMLHttpRequest(); 145 xhr.open(options.method || 'POST', url); 146 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 147 148 xhr.onload = function() { 149 if (xhr.status >= 200 && xhr.status < 300) { 150 try { 151 resolve(JSON.parse(xhr.responseText)); 152 } catch (e) { 153 reject(new Error('Invalid JSON response')); 154 } 155 } else { 156 reject(new Error(`HTTP ${xhr.status}`)); 157 } 158 }; 159 160 xhr.onerror = function() { 161 reject(new Error('Network error')); 162 }; 163 164 const data = options.data ? new URLSearchParams(options.data).toString() : null; 165 xhr.send(data); 166 } 167 }); 168 } 169 170 /** 171 * Check stock via AJAX 172 */ 173 function checkStock(productId, variantId, qty) { 174 return makeRequest(config.apiEndpoint, { 175 method: 'POST', 176 data: { 177 product_id: productId, 178 variant_id: variantId || 0, 179 qty: qty || 1 180 } 181 }); 182 } 183 184 /** 185 * Create and show stock alert modal using modal.css structure 186 */ 187 function showStockModal(stockInfo, action = 'add to cart') { 188 let title, message, type, iconClass; 189 190 // Handle both string and object formats 191 if (typeof stockInfo === 'string') { 192 title = 'Stok Tidak Tersedia'; 193 message = stockInfo; 194 type = 'warning'; 195 iconClass = 'warning'; 196 } else if (stockInfo && typeof stockInfo === 'object') { 197 if (!stockInfo.is_active) { 198 title = 'Produk Tidak Tersedia'; 199 message = 'Maaf, produk ini sudah tidak aktif atau telah dihapus dari katalog.'; 200 type = 'danger'; 201 iconClass = 'danger'; 202 } else if (!stockInfo.is_in_stock) { 203 title = 'Stok Habis'; 204 message = `<strong>${stockInfo.product_name || 'Produk ini'}</strong> sedang habis stok. Silakan pilih varian lain atau tunggu restock.`; 205 if (stockInfo.sku) { 206 message += `<br><small style="color: #64748b;">SKU: ${stockInfo.sku}</small>`; 207 } 208 type = 'warning'; 209 iconClass = 'warning'; 210 } else if (!stockInfo.can_add_to_cart) { 211 title = 'Stok Tidak Mencukupi'; 212 message = `Stok <strong>${stockInfo.product_name || 'produk'}</strong> tidak mencukupi.<br>`; 213 message += `Tersedia: <strong>${stockInfo.available_stock}</strong>, diminta: <strong>${stockInfo.requested_qty}</strong><br>`; 214 message += `Silakan kurangi jumlah atau pilih varian lain.`; 215 type = 'warning'; 216 iconClass = 'warning'; 217 } else { 218 title = 'Stok Tidak Tersedia'; 219 message = stockInfo.message || 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 220 type = 'warning'; 221 iconClass = 'warning'; 222 } 223 } else { 224 title = 'Stok Tidak Tersedia'; 225 message = 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 226 type = 'warning'; 227 iconClass = 'warning'; 228 } 229 230 // Create modal HTML using modal.css structure 231 const modalHTML = ` 232 <div class="modal-overlay" id="stock-modal-overlay"> 233 <div class="modal-dialog"> 234 <div class="modal-header"> 235 <div class="modal-icon ${iconClass}"> 236 ${getIconSVG(iconClass)} 237 </div> 238 <h3 class="modal-title">${title}</h3> 239 </div> 240 <div class="modal-body"> 241 <p class="modal-message">${message}</p> 242 <div class="modal-actions"> 243 <button type="button" class="modal-btn modal-btn-secondary" data-action="close"> 244 Tutup 245 </button> 246 <button type="button" class="modal-btn modal-btn-primary" data-action="reload"> 247 Muat Ulang 248 </button> 249 </div> 250 </div> 251 </div> 252 </div> 253 `; 254 255 // Remove existing modal if any 256 const existingModal = document.getElementById('stock-modal-overlay'); 257 if (existingModal) { 258 existingModal.remove(); 259 } 260 261 // Add modal to body 262 document.body.insertAdjacentHTML('beforeend', modalHTML); 263 264 // Show modal with animation 265 const modal = document.getElementById('stock-modal-overlay'); 266 setTimeout(() => { 267 modal.classList.add('show'); 268 }, 10); 269 270 // Add event listeners to modal buttons 271 const closeBtn = modal.querySelector('[data-action="close"]'); 272 const reloadBtn = modal.querySelector('[data-action="reload"]'); 273 274 if (closeBtn) { 275 addEventListener(closeBtn, 'click', closeStockModal); 276 } 277 278 if (reloadBtn) { 279 addEventListener(reloadBtn, 'click', function() { 280 closeStockModal(); 281 window.location.reload(); 282 }); 283 } 284 285 // Close modal when clicking overlay 286 addEventListener(modal, 'click', function(event) { 287 if (event.target === modal) { 288 closeStockModal(); 289 } 290 }); 291 292 // Auto-close after 8 seconds 293 setTimeout(() => { 294 closeStockModal(); 295 }, 8000); 296 } 297 298 /** 299 * Get icon SVG based on type 300 */ 301 function getIconSVG(type) { 302 const icons = { 303 danger: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>', 304 warning: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
304<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>', 305 success: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>', 306 info: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>' 307 }; 308 return icons[type] || icons.info; 309 } 310 311 /** 312 * Close stock modal 313 */ 314 function closeStockModal() { 315 const modal = document.getElementById('stock-modal-overlay'); 316 if (modal) { 317 modal.classList.remove('show'); 318 setTimeout(() => { 319 modal.remove(); 320 }, 200); 321 } 322 } 323 324 // Make closeStockModal globally available 325 if (typeof window !== 'undefined') { 326 window.closeStockModal = closeStockModal; 327 } 328 329 /** 330 * Get element data attribute with fallback 331 */ 332 function getDataAttribute(element, attr) { 333 if (element.dataset) { 334 return element.dataset[attr]; 335 } else { 336 return element.getAttribute('data-' + attr); 337 } 338 } 339 340 /** 341 * Validate stock before cart action 342 */ 343 async function validateBeforeCart(productId, variantId, qty, action = 'add') { 344 try { 345 // SMART VALIDATION: For simple products, check if we already have cached stock info 346 // If buttons are enabled and cached info shows stock available, skip API validation 347 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart) { 348 349 return true; 350 } 351 352 // For variant products or when cached info is not available, do API validation 353 const stockInfo = await checkStock(productId, variantId, qty); 354 355 if (!stockInfo.ok) { 356 showStockModal({ 357 message: stockInfo.error || 'Gagal mengecek stok', 358 is_active: false 359 }, action); 360 return false; 361 } 362 363 if (!stockInfo.can_add_to_cart) { 364 showStockModal(stockInfo, action); 365 return false; 366 } 367 368 return true; // Stock available, proceed 369 370 } catch (error) { 371 console.error('Stock validation error:', error); 372 showStockModal({ 373 message: 'Terjadi kesalahan saat mengecek stok. Silakan coba lagi.', 374 is_active: false 375 }, action); 376 return false; 377 } 378 } 379 380 /** 381 * Handle button click with stock validation 382 */ 383 function handleButtonClick(event, action) { 384 const button = event.target; 385 386 387 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 388 if (button.hasAttribute('data-stock-blocked')) { 389 event.preventDefault(); 390 event.stopImmediatePropagation(); 391 392 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 393 showStockModal(reason); 394 395 // Show security warning 396 if (window.showToast) { 397 window.showToast({ 398 type: 'warning', 399 title: 'Aksi Tidak Diizinkan', 400 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 401 duration: 3000 402 }); 403 } 404 return false; 405 } 406 407 // SECOND CHECK: Real-time stock validation against cached stock info 408 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 409 event.preventDefault(); 410 event.stopImmediatePropagation(); 411 412 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 413 414 // Re-enforce button disabling 415 button.disabled = true; 416 button.style.opacity = '0.5'; 417 button.style.cursor = 'not-allowed'; 418 button.setAttribute('data-stock-blocked', 'true'); 419 420 return false; 421 } 422 423 // Skip if already processing 424 if (button.classList.contains(config.loadingClass)) { 425 event.preventDefault(); 426 return false; 427 } 428 429 // Get product data from button attributes or form 430 let productId = parseInt(getDataAttribute(button, 'productId') || getDataAttribute(button, 'product-id') || 0); 431 let variantId = parseInt(getDataAttribute(button, 'variantId') || getDataAttribute(button, 'variant-id') || 0); 432 let qty = parseInt(getDataAttribute(button, 'qty') || 1); 433 434 // Try to get from global variables (set by product detail page) 435 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 436 productId = parseInt(window.PD_ID || 0); 437 } 438 439 // Try to get from parent form if not in button 440 if (productId <= 0) { 441 const form = button.closest ? button.closest('form') : null; 442 if (form) { 443 const pidAttr = form.getAttribute('data-pid'); 444 const pidInput = form.querySelector('[name="product_id"]'); 445 const vidInput = form.querySelector('[name="variant_id"]');
446 const qtyInput = form.querySelector('[name="qty"]'); 447 448 if (pidAttr) productId = parseInt(pidAttr); 449 if (pidInput) productId = parseInt(pidInput.value || 0); 450 if (vidInput) variantId = parseInt(vidInput.value || 0); 451 if (qtyInput) qty = parseInt(qtyInput.value || 1); 452 } 453 } 454 455 // CRITICAL: Always get current variant from selector (overrides button attributes) 456 // This ensures we validate against the currently selected variant, not cached button data 457 const variantSelector = document.getElementById('pd-variant-id'); 458 if (variantSelector) { 459 const currentVariantId = parseInt(variantSelector.value || 0); 460 if (currentVariantId > 0) { 461 variantId = currentVariantId; 462 } 463 } 464 465 466 if (productId <= 0) { 467 return true; // Let default handler deal with it 468 } 469 470 // Prevent default and validate stock 471 event.preventDefault(); 472 event.stopPropagation(); 473 474 // Show loading state 475 button.classList.add(config.loadingClass); 476 const originalDisabled = button.disabled; 477 button.disabled = true; 478 479 480 validateBeforeCart(productId, variantId, qty, action) 481 .then(isValid => { 482 if (isValid) { 483 // Stock OK, proceed with original action 484 if (action === 'cart' && typeof window.addToCart === 'function') { 485 window.addToCart(productId, qty, variantId); 486 } else if (action === 'buy' && typeof window.buyNow === 'function') { 487 // Call buyNow from pd-buynow.js without parameters (it gets data internally) 488 window.buyNow(); 489 } else { 490 // Add fallback cart/buy logic here if needed 491 } 492 } 493 }) 494 .catch(error => { 495 }) 496 .finally(() => { 497 // Remove loading state 498 button.classList.remove(config.loadingClass); 499 button.disabled = originalDisabled; 500 }); 501 502 return false; 503 } 504 505 /** 506 * Handle mobile button click with stock validation 507 */ 508 function handleMobileButtonClick(event, action) { 509 const button = event.target; 510 511 512 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 513 if (button.hasAttribute('data-stock-blocked')) { 514 event.preventDefault(); 515 event.stopImmediatePropagation(); 516 517 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 518 showStockModal(reason); 519 520 // Show security warning 521 if (window.showToast) { 522 window.showToast({ 523 type: 'warning', 524 title: 'Aksi Tidak Diizinkan', 525 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 526 duration: 3000 527 }); 528 } 529 return false; 530 } 531 532 // SECOND CHECK: Real-time stock validation against cached stock info 533 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 534 event.preventDefault(); 535 event.stopImmediatePropagation(); 536 537 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 538 539 // Re-enforce button disabling 540 button.disabled = true; 541 button.style.opacity = '0.5'; 542 button.style.cursor = 'not-allowed'; 543 button.setAttribute('data-stock-blocked', 'true'); 544 545 return false; 546 } 547 548 // Skip if already processing 549 if (button.classList.contains(config.loadingClass)) { 550 event.preventDefault(); 551 return false; 552 } 553 554 // Get product data from mobile FAB container 555 let productId = 0; 556 let variantId = 0; 557 let qty = 1; 558
559 // Try to get from mobile FAB data-pid 560 const mobileFab = document.getElementById('pd-mobile-fab'); 561 if (mobileFab) { 562 const pidAttr = getDataAttribute(mobileFab, 'pid'); 563 if (pidAttr) productId = parseInt(pidAttr); 564 } 565 566 // Try to get from global variables (set by product detail page) 567 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 568 productId = parseInt(window.PD_ID || 0); 569 } 570 571 // CRITICAL: Always get current variant from selector (real-time) 572 // This ensures we validate against the currently selected variant 573 const variantSelector = document.getElementById('pd-variant-id'); 574 if (variantSelector) { 575 variantId = parseInt(variantSelector.value || 0); 576 } 577 578 // Try to get quantity from quantity input 579 const qtySelector = document.querySelector('[name="qty"]'); 580 if (qtySelector) { 581 qty = parseInt(qtySelector.value || 1); 582 } 583 584 // Ensure minimum quantity is 1 585 qty = Math.max(1, qty); 586 587 588 if (productId <= 0) { 589 return true; // Let default handler deal with it 590 } 591 592 // Event already prevented at interceptor level, just proceed with validation 593 594 // Show loading state 595 button.classList.add(config.loadingClass); 596 const originalDisabled = button.disabled; 597 const originalText = button.textContent; 598 button.disabled = true; 599 button.textContent = action === 'cart' ? 'Validasi...' : 'Validasi...'; 600 601 602 // PREEMPTIVE BLOCKING: Set blocking flags BEFORE validation to prevent race conditions 603 if (window.addToCart && typeof window.addToCart === 'function') { 604 window.addToCart._stockValidatorBlocked = true; 605 } 606 607 // Also preemptively block other cart functions 608 const originalAddToCartSubmit = window.addToCartSubmit; 609 const originalPdAddToCartSubmit = window._pdAddToCartSubmit; 610 611 // Temporarily block ALL cart functions during validation 612 window.addToCartSubmit = (...args) => { 613 return Promise.resolve(false); 614 }; 615 616 if (originalPdAddToCartSubmit) { 617 window._pdAddToCartSubmit = (...args) => { 618 return Promise.resolve(false); 619 }; 620 } 621 622 validateBeforeCart(productId, variantId, qty, action) 623 .then(isValid => { 624 if (isValid) { 625 // Stock OK, proceed with original action 626 627 if (action === 'cart') { 628 // For mobile cart, we need to recreate the original behavior 629 // Strategy: Try multiple methods to ensure cart add + toast success 630 631 632 let cartAddSuccess = false; 633 let errorMessage = ''; 634 635 // Method 1: Try original addToCart function 636 const originalAddToCart = window.addToCart && window.addToCart._original; 637 if (originalAddToCart && !cartAddSuccess) { 638 try { 639 const result = originalAddToCart(productId, qty, variantId); 640 641 // Handle both sync and async returns 642 if (result && typeof result.then === 'function') { 643 result.then(() => { 644 cartAddSuccess = true; 645 }).catch(err => { 646 errorMessage = err.message || 'addToCart failed'; 647 }); 648 } else { 649 cartAddSuccess = true; 650 } 651 } catch (error) { 652 errorMessage = error.message || 'addToCart exception'; 653 } 654 } 655 656 // Method 2: Try pd-addtocart.js function (backup method) 657 if (typeof window._pdAddToCartSubmit === 'function' && !cartAddSuccess) { 658 try { 659 660 // Create fake form for pd-addtocart.js 661 const fakeForm = document.createElement('form');
662 const qtyInput = document.createElement('input'); 663 qtyInput.name = 'qty'; 664 qtyInput.value = qty; 665 fakeForm.appendChild(qtyInput); 666 667 // Set variant selector value for pd-addtocart to read 668 const variantSelector = document.getElementById('pd-variant-id'); 669 if (variantSelector) { 670 variantSelector.value = variantId; 671 } 672 673 const fakeEvent = { 674 preventDefault: () => {}, 675 target: fakeForm 676 }; 677 678 // Call pd-addtocart.js function (this should handle toast internally) 679 const result = window._pdAddToCartSubmit(fakeEvent, productId); 680 if (result && typeof result.then === 'function') { 681 result.then(() => { 682 cartAddSuccess = true; 683 }); 684 } else { 685 cartAddSuccess = true; 686 } 687 } catch (error) { 688 } 689 } 690 691 // Method 3: Direct addToCart call (fallback) 692 if (typeof window.addToCart === 'function' && !cartAddSuccess) { 693 try { 694 const result = window.addToCart(productId, qty, variantId); 695 if (result && typeof result.then === 'function') { 696 result.then(() => { 697 cartAddSuccess = true; 698 }); 699 } else { 700 cartAddSuccess = true; 701 } 702 } catch (error) { 703 } 704 } 705 706 // Always show success toast for mobile (regardless of which method worked) 707 // This ensures user gets feedback for successful cart addition 708 setTimeout(() => { 709 if (window.showToast) { 710 711 // Get product details for toast 712 const getVariantSummaryText = () => { 713 const vs = document.getElementById('pd-variant-summary'); 714 if (!vs) return ''; 715 const items = Array.from(vs.querySelectorAll('.vs-item strong')) 716 .map(el => el.textContent.trim()) 717 .filter(Boolean); 718 return items.length ? items.join(' / ') : ''; 719 }; 720 721 const escapeHtml = (s) => String(s||'').replace(/[&<>"']/g, m=>({ 722 '&':'&','<':'<','>':'>','"':'"',"'":''' 723 }[m])); 724 725 const variantText = getVariantSummaryText(); 726 const skuText = (function(){ 727 const el = document.getElementById('pd-sku'); 728 return el ? el.textContent.trim() : (window.PD_SKU || 'N/A'); 729 })(); 730 const productName = window.PD_NAME || 'Product'; 731 732 window.showToast({ 733 type: 'success', 734 title: 'Berhasil ditambahkan ke Keranjang', 735 message: `<strong>${escapeHtml(productName)}</strong>${variantText ? ` · <em>${escapeHtml(variantText)}</em>` : ''}<br>SKU: ${escapeHtml(skuText)}
735 · Qty: ${qty}`, 736 duration: 5000, 737 actions: [ 738 { 739 label: 'Lihat Keranjang', 740 onClick: () => { window.location.href = '/keranjang'; } 741 }, 742 { 743 label: 'Lanjut Belanja', 744 variant: 'alt', 745 onClick: () => {} 746 } 747 ] 748 }); 749 } else { 750 } 751 }, 100); // Small delay to ensure cart operation completes first 752 753 } else if (action === 'buy') { 754 // For mobile buy, call buyNow 755 if (typeof window.buyNow === 'function') { 756 window.buyNow(); 757 } 758 } 759 } else { 760 // Stock validation FAILED - absolutely NO cart function calls allowed 761 762 // DO NOT CALL ANY CART FUNCTIONS - only show modal (already handled by validateBeforeCart) 763 } 764 765 // Always restore functions after validation (success or failure) 766 setTimeout(() => { 767 // Remove blocking flag 768 if (window.addToCart && typeof window.addToCart === 'function') { 769 window.addToCart._stockValidatorBlocked = false; 770 } 771 772 // Restore other functions 773 if (originalAddToCartSubmit && typeof originalAddToCartSubmit === 'function') { 774 window.addToCartSubmit = originalAddToCartSubmit; 775 } 776 if (originalPdAddToCartSubmit && typeof originalPdAddToCartSubmit === 'function') { 777 window._pdAddToCartSubmit = originalPdAddToCartSubmit; 778 } 779 780 }, 1000); // Shorter timeout since we want to restore quickly after success 781 }) 782 .catch(error => { 783 // On error, also block the action 784 }) 785 .finally(() => { 786 // Remove loading state 787 button.classList.remove(config.loadingClass); 788 button.disabled = originalDisabled; 789 button.textContent = originalText; 790 }); 791 792 return false; 793 } 794 795 /** 796 * Intercept cart buttons 797 */ 798 function interceptCartButtons() { 799 800 801 // CRITICAL: Remove existing mobile cart handlers from product-detail.php 802 setTimeout(() => { 803 const mobileCartBtn = document.getElementById('mf-cart'); 804 if (mobileCartBtn) { 805 // Clone the button to remove ALL existing event listeners 806 const newBtn = mobileCartBtn.cloneNode(true); 807 mobileCartBtn.parentNode.replaceChild(newBtn, mobileCartBtn); 808 809 810 // Add our own handler to the new button 811 newBtn.addEventListener('click', function(event) { 812 event.preventDefault(); 813 event.stopPropagation(); 814 event.stopImmediatePropagation(); 815 handleMobileButtonClick(event, 'cart'); 816 return false; 817 }, true); 818 } 819 }, 50); 820 821 // Note: Add to cart buttons are now handled via form submission override (addToCartSubmit) 822 // This provides better integration with existing pd-addtocart.js toast system 823 824 // Buy now buttons - updated selectors (desktop + mobile) 825 addEventListener(document, 'click', function(event) { 826 const target = event.target; 827 828 // Desktop buy buttons 829 if (target.classList.contains('btn-buy') || // Product detail buy button 830 target.classList.contains('btn-buy-now') || 831 target.classList.contains('buy-now-btn') || 832 target.id === 'btn-buy' || // Specific ID match 833 getDataAttribute(target, 'action') === 'buy-now' || 834 getDataAttribute(target, 'action') === 'buy') { 835 836 handleButtonClick(event, 'buy'); 837 } 838 839 // Mobile buy buttons 840 else if (target.classList.contains('mf-buy') || // Mobile fab buy button 841 target.id === 'mf-buy') { // Mobile buy button ID 842 843 handleMobileButtonClick(event, 'buy'); 844 } 845 846 // Mobile cart buttons - ENHANCED BLOCKING 847 else if (target.classList.contains('mf-cart') || // Mobile fab cart button 848 target.id === 'mf-cart') { // Mobile cart button ID 849 850 851 // Immediately prevent all event propagation 852 event.preventDefault(); 853 event.stopPropagation(); 854 event.stopImmediatePropagation(); 855 856 handleMobileButtonClick(event, 'cart'); 857 858 // Return false to ensure no other handlers run 859 return false; 860 } 861 }, true);
861 // Use capturing phase to ensure we intercept before other handlers 862 863 // Additional protection: Override addToCart function to add mobile button state checking 864 const originalAddToCart = typeof window.addToCart === 'function' ? window.addToCart : null; 865 if (originalAddToCart) { 866 window.addToCart = async function(...args) { 867 // Check if this call is coming from a mobile button that's currently being validated 868 const mobileCartBtn = document.getElementById('mf-cart'); 869 if (mobileCartBtn && mobileCartBtn.classList.contains('loading-stock')) { 870 return Promise.reject(new Error('Mobile validation in progress - cart action blocked')); 871 } 872 873 // Check if functions are temporarily blocked (during stock failure) 874 if (window.addToCart._stockValidatorBlocked) { 875 return Promise.reject(new Error('Stock validation failed - cart action blocked')); 876 } 877 878 // Allow normal calls 879 return originalAddToCart.apply(this, args); 880 }; 881 882 // Store reference to original for restoration 883 window.addToCart._original = originalAddToCart; 884 } 885 } 886 887 /** 888 * Setup global click interceptor for security 889 * This catches ALL clicks on cart/buy buttons and validates stock BEFORE any other handler 890 */ 891 function setupGlobalClickInterceptor() { 892 // Add capture-phase listener to catch clicks before other handlers 893 document.addEventListener('click', function(event) { 894 const target = event.target; 895 896 // Check if this is a cart/buy button 897 const isCartButton = target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') || 898 target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart'); 899 const isBuyButton = target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') || 900 target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy'); 901 902 if (!isCartButton && !isBuyButton) { 903 return; // Not our target, allow normal processing 904 } 905 906 const button = isCartButton ? 907 (target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') ? target : target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart')) : 908 (target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') ? target : target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy')); 909 910 // IMMEDIATE BLOCK: Prevent any processing first 911 event.preventDefault(); 912 event.stopImmediatePropagation(); 913 914 // SECURITY CHECK 1: Is button marked as stock-blocked? 915 if (button && button.hasAttribute('data-stock-blocked')) { 916 917 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 918 showStockModal(reason); 919 920 // Show security alert 921 if (window.showToast) { 922 window.showToast({ 923 type: 'error', 924 title: 'Aksi Diblokir', 925 message: 'Produk ini sedang habis stok dan tidak dapat ditambahkan ke keranjang.', 926 duration: 4000 927 }); 928 } 929 930 return false; 931 } 932 933 // SECURITY CHECK 2: Real-time validation against cached stock info 934 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 935 936 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 937 938 // Re-enforce button disabling 939 if (button) { 940 button.disabled = true; 941 button.style.opacity = '0.5'; 942 button.style.cursor = 'not-allowed'; 943 button.setAttribute('data-stock-blocked', 'true'); 944 button.setAttribute('data-stock-reason', window._currentStockInfo.message || 'Stok tidak tersedia'); 945 } 946 947 return false; 948 } 949 950 // SECURITY CHECK 3: Smart client-side validation 951 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 952 const variantSelector = document.getElementById('pd-variant-id'); 953 const variantId = variantSelector ? parseInt(variantSelector.value || 0
953) : 0; 954 955 if (productId > 0) { 956 // SMART VALIDATION: If we already have cached stock info showing stock is available, 957 // and buttons are enabled, proceed immediately without re-validation 958 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart && 959 button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 960 961 // Allow the action to proceed immediately with toast 962 if (isCartButton) { 963 // Trigger cart action with proper toast handling 964 if (typeof window._pdAddToCartSubmit === 'function') { 965 // Use pd-addtocart.js function for proper toast handling 966 const form = document.getElementById('pd-buy-form') || button.closest('form'); 967 if (form) { 968 const fakeEvent = { 969 target: form, 970 preventDefault: () => {} 971 }; 972 window._pdAddToCartSubmit(fakeEvent, productId); 973 } 974 } else if (typeof window.addToCartSubmit === 'function') { 975 // Fallback to addToCartSubmit 976 const form = document.getElementById('pd-buy-form') || button.closest('form'); 977 if (form) { 978 const fakeEvent = { 979 target: form, 980 preventDefault: () => {} 981 }; 982 window.addToCartSubmit(fakeEvent, productId); 983 } 984 } else if (typeof window.addToCart === 'function') { 985 // Direct addToCart call 986 window.addToCart(productId, 1, variantId); 987 } 988 } else if (isBuyButton) { 989 // Trigger buy action 990 if (typeof window.buyNow === 'function') { 991 window.buyNow(); 992 } 993 } 994 995 return false; // Action completed 996 } 997 998 // Get stock info from embedded variant data (for variant products) 999 const stockInfo = getStockFromEmbeddedData(variantId); 1000 1001 if (stockInfo && stockInfo.hasStock) { 1002 1003 // Update global stock cache 1004 window._currentStockInfo = { 1005 can_add_to_cart: true, 1006 is_in_stock: true, 1007 available_stock: stockInfo.stock, 1008 message: 'Stok tersedia' 1009 }; 1010 1011 // IMPORTANT: Check if button was explicitly blocked by variant logic (e.g., pre-order exhausted) 1012 // If data-stock-blocked is set, respect it and don't enable the button 1013 const isExplicitlyBlocked = button.getAttribute('data-stock-blocked') === 'true'; 1014 1015 if (!isExplicitlyBlocked) { 1016 // Remove stock blocking if it was set and enable button 1017 button.removeAttribute('data-stock-blocked'); 1018 button.removeAttribute('data-stock-reason'); 1019 button.disabled = false; 1020 button.style.opacity = ''; 1021 button.style.cursor = ''; 1022 button.classList.remove(config.disabledClass); 1023 } else { 1024 // Button is blocked by variant logic, show reason 1025 const reason = button.getAttribute('data-stock-reason') || 'Tidak tersedia'; 1026 console.log('Button blocked by variant logic:', reason); 1027 e.preventDefault(); 1028 e.stopPropagation(); 1029 return false; 1030 } 1031 1032 // Allow the action to proceed immediately with toast 1033 if (isCartButton) { 1034 // Trigger cart action with proper toast handling 1035 if (typeof window._pdAddToCartSubmit === 'function') { 1036 // Use pd-addtocart.js function for proper toast handling 1037 const form = document.getElementById('pd-buy-form') || button.closest('form'); 1038 if (form) { 1039 const fakeEvent = { 1040 target: form, 1041 preventDefault: () => {} 1042 }; 1043 window._pdAddToCartSubmit(fakeEvent, productId); 1044 } 1045 } else if (typeof window.addToCartSubmit === 'function') { 1046 // Fallback to addToCartSubmit 1047 const form = document.getElementById('pd-buy-form') || button.closest('form'); 1048 if (form) { 1049 const fakeEvent = { 1050 target: form, 1051 preventDefault: () => {} 1052 }; 1053 window.addToCartSubmit(fakeEvent, productId); 1054 } 1055 } else if (typeof window.addToCart === 'function') { 1056 // Direct addToCart call 1057 window.addToCart(productId, 1, variantId); 1058 } 1059 } else if (isBuyButton) { 1060 // Trigger buy action 1061 if (typeof window.buyNow === 'function') { 1062 window.buyNow(); 1063 } 1064 } 1065 } else if (stockInfo && !stockInfo.hasStock) { 1066 1067 // Update global stock cache 1068 window._currentStockInfo = { 1069 can_add_to_cart: false, 1070 is_in_stock: false, 1071 available_stock: stockInfo.stock, 1072 message: `Stok tidak tersedia${stockInfo.sku ? ` untuk ${stockInfo.sku}` : ''}` 1073 }; 1074 1075 // Force block button 1076 button.disabled = true; 1077 button.style.opacity = '0.5'; 1078 button.style.cursor = 'not-allowed'; 1079 button.setAttribute('data-stock-blocked', 'true'); 1080 button.setAttribute('data-stock-reason', window._currentStockInfo.message); 1081 1082 // Show modal with variant info 1083 const modalMessage = stockInfo ? 1084 `Varian yang dipilih (${stockInfo.sku}) sedang habis stok. Silakan pilih varian lain.` : 1085 'Produk ini sedang habis stok. Silakan pilih varian lain.'; 1086 1087 showStockModal(modalMessage); 1088 1089 // Show security warning 1090 if (window.showToast) { 1091 window.showToast({ 1092 type: 'warning',
1093 title: 'Stok Tidak Tersedia', 1094 message: 'Varian yang dipilih sedang habis stok. Silakan pilih varian lain.', 1095 duration: 3000 1096 }); 1097 } 1098 } else { 1099 // stockInfo is null (simple product) - this means embedded data is not available 1100 // For simple products, if buttons are enabled, we should proceed (trust the API response from updateStockDisplay) 1101 if (button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 1102 1103 // Allow the action to proceed immediately with toast 1104 if (isCartButton) { 1105 // Trigger cart action with proper toast handling 1106 if (typeof window._pdAddToCartSubmit === 'function') { 1107 // Use pd-addtocart.js function for proper toast handling 1108 const form = document.getElementById('pd-buy-form') || button.closest('form'); 1109 if (form) { 1110 const fakeEvent = { 1111 target: form, 1112 preventDefault: () => {} 1113 }; 1114 window._pdAddToCartSubmit(fakeEvent, productId); 1115 } 1116 } else if (typeof window.addToCartSubmit === 'function') { 1117 // Fallback to addToCartSubmit 1118 const form = document.getElementById('pd-buy-form') || button.closest('form'); 1119 if (form) { 1120 const fakeEvent = { 1121 target: form, 1122 preventDefault: () => {} 1123 }; 1124 window.addToCartSubmit(fakeEvent, productId); 1125 } 1126 } else if (typeof window.addToCart === 'function') { 1127 // Direct addToCart call 1128 window.addToCart(productId, 1, variantId); 1129 } 1130 } else if (isBuyButton) { 1131 // Trigger buy action 1132 if (typeof window.buyNow === 'function') { 1133 window.buyNow(); 1134 } 1135 } 1136 } else { 1137 // Button is disabled, don't allow action 1138 showStockModal('Produk ini sedang tidak tersedia.'); 1139 } 1140 } 1141 } else { 1142 // No product ID, block action 1143 showStockModal('Data produk tidak valid.'); 1144 } 1145 1146 return false; // Always block at capture phase 1147 1148 }, true); // Use capture phase to run BEFORE other listeners 1149 1150 1151 } 1152 1153 /** 1154 * Initialize button data attributes from current page 1155 */ 1156 function initializeButtonAttributes() { 1157 // Get product data from page globals or form 1158 let productId = 0, variantId = 0; 1159 1160 // Try to get from global variables (set by PHP) 1161 if (window.PD_ID) productId = parseInt(window.PD_ID); 1162 1163 // Try to get from form 1164 const form = document.getElementById('pd-buy-form'); 1165 if (form) { 1166 const pidAttr = form.getAttribute('data-pid'); 1167 if (pidAttr) productId = parseInt(pidAttr); 1168 1169 const varInput = document.getElementById('pd-variant-id'); 1170 if (varInput) variantId = parseInt(varInput.value || 0); 1171 } 1172 1173 // Try to get from mobile FAB 1174 const mobileFab = document.getElementById('pd-mobile-fab'); 1175 if (mobileFab && productId <= 0) { 1176 const pidAttr = getDataAttribute(mobileFab, 'pid'); 1177 if (pidAttr) productId = parseInt(pidAttr); 1178 } 1179 1180 // Set attributes on desktop buttons 1181 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 1182 desktopButtons.forEach(button => { 1183 if (productId > 0) { 1184 button.setAttribute('data-product-id', productId); 1185 if (variantId > 0) { 1186 button.setAttribute('data-variant-id', variantId); 1187 } 1188 } 1189 }); 1190 1191 // Set attributes on mobile buttons 1192 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 1193 mobileButtons.forEach(button => { 1194 if (productId > 0) { 1195 button.setAttribute('data-product-id', productId); 1196 if (variantId > 0) { 1197 button.setAttribute('data-variant-id', variantId); 1198 } 1199 } 1200 }); 1201 1202 1203 1204 // Initial stock display update on page load 1205 if (productId > 0) { 1206 setTimeout(() => { 1207 updateStockDisplay(productId, variantId); 1208 1209 }, 100); 1210 } 1211 } 1212 1213 /** 1214 * Update button attributes when variant changes 1215 */ 1216 function updateButtonAttributesForVariant(variantId) { 1217 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 1218 1219 if (productId <= 0) return; 1220 1221 // Update desktop buttons 1222 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 1223 desktopButtons.forEach(button => { 1224 button.setAttribute('data-product-id', productId); 1225 if (variantId > 0) { 1226 button.setAttribute('data-variant-id', variantId); 1227 } else { 1228 button.removeAttribute('data-variant-id'); 1229 } 1230 }); 1231 1232 // Update mobile buttons 1233 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 1234 mobileButtons.forEach(button => { 1235 button.setAttribute('data-product-id', productId); 1236 if (variantId > 0) { 1237 button.setAttribute('data-variant-id', variantId); 1238 } else { 1239 button.removeAttribute('data-variant-id'); 1240 } 1241 }); 1242 1243 1244 } 1245 1246 /** 1247 * Setup variant change integration with existing product-detail.php handlers 1248 */ 1249 function setupVariantChangeIntegration() { 1250 // Strategy: Hook into existing applyVariant function instead of adding new listener 1251 // This prevents conflicts with existing variant change functionality (gallery, pricing, etc.) 1252 1253 // Method 1: Hook into existing applyVariant function 1254 setTimeout(() => { 1255 if (typeof window.applyVariant === 'function') { 1256 const originalApplyVariant = window.applyVariant; 1257 window.applyVariant = function(v, focusImage) { 1258 // Call original function first 1259 const result = originalApplyVariant.call(this, v, focusImage); 1260 1261 // Then add our stock validator logic 1262 const variantId = v ? (v.id || v.ID || 0) : 0; 1263 1264 // Update button attributes for stock validator 1265 updateButtonAttributesForVariant(variantId); 1266 1267 // Update stock display using embedded data (instant) 1268 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 1269 if (productId > 0) { 1270 updateStockDisplay(productId, variantId); 1271 } return result; 1272 }; 1273 1274 1275 } else { 1276 1277 setupVariantObserver(); 1278 } 1279 }, 500); 1280 1281 // Method 2: Direct observation of hidden input value changes (fallback) 1282 setTimeout(() => { 1283 setupVariantObserver(); 1284 }, 1000); 1285 } 1286 1287 /** 1288 * Setup variant observer as fallback when applyVariant hook fail
1288s 1289 */ 1290 function setupVariantObserver() { 1291 const variantInput = document.getElementById('pd-variant-id'); 1292 if (variantInput) { 1293 let lastVariantId = parseInt(variantInput.value || '0'); 1294 1295 // Use MutationObserver to watch for value changes 1296 if (window.MutationObserver) { 1297 const observer = new MutationObserver((mutations) => { 1298 mutations.forEach((mutation) => { 1299 if (mutation.type === 'attributes' && mutation.attributeName === 'value') { 1300 const newVariantId = parseInt(variantInput.value || '0'); 1301 if (newVariantId !== lastVariantId) { 1302 1303 updateButtonAttributesForVariant(newVariantId); 1304 1305 // Update stock display with embedded data 1306 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 1307 if (productId > 0) { 1308 updateStockDisplay(productId, newVariantId); 1309 } 1310 1311 lastVariantId = newVariantId; 1312 } 1313 } 1314 }); 1315 }); 1316 1317 observer.observe(variantInput, { 1318 attributes: true, 1319 attributeFilter: ['value'] 1320 }); 1321 1322 1323 } 1324 1325 // Additional polling as ultimate fallback 1326 setInterval(() => { 1327 const currentVariantId = parseInt(variantInput.value || '0'); 1328 if (currentVariantId !== lastVariantId) { 1329 1330 updateButtonAttributesForVariant(currentVariantId); 1331 1332 // Update stock display with embedded data 1333 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 1334 if (productId > 0) { 1335 updateStockDisplay(productId, currentVariantId); 1336 } 1337 1338 lastVariantId = currentVariantId; 1339 } 1340 }, 1000); 1341 1342 1343 } 1344 } 1345 1346 /** 1347 * Update stock display and enforce stock-based button blocking using embedded data 1348 */ 1349 function updateStockDisplay(productId, variantId = 0) { 1350 1351 1352 // Get stock info from embedded data (instant) 1353 const embeddedStockInfo = getStockFromEmbeddedData(variantId); 1354 1355 let stockInfo; 1356 if (embeddedStockInfo) { 1357 // Convert embedded data to standard format 1358 stockInfo = { 1359 can_add_to_cart: embeddedStockInfo.hasStock, 1360 is_in_stock: embeddedStockInfo.hasStock, 1361 available_stock: embeddedStockInfo.stock, 1362 message: embeddedStockInfo.hasStock ? 'Stok tersedia' : `Stok habis${embeddedStockInfo.sku ? ` untuk ${embeddedStockInfo.sku}` : ''}`, 1363 product_name: window.PD_NAME || 'Produk', 1364 sku: embeddedStockInfo.sku 1365 }; 1366 1367 1368 } else { 1369 // Fallback to API call if embedded data not available 1370 1371 checkStock(productId, variantId, 1) 1372 .then(apiStockInfo => { 1373 updateStockDisplayWithInfo(apiStockInfo, productId, variantId); 1374 }) 1375 .catch(error => { 1376 console.error('Failed to update stock display via API:', error); 1377 }); 1378 return; 1379 } 1380 1381 updateStockDisplayWithInfo(stockInfo, productId, variantId); 1382 } 1383 1384 /** 1385 * Update stock display with provided stock info 1386 */ 1387 function updateStockDisplayWithInfo(stockInfo, productId, variantId) { 1388 // Store current stock status globally for blocking checks 1389 window._currentStockInfo = stockInfo; 1390 1391 // Update stock indicators 1392 const stockIndicators = document.querySelectorAll('.stock-indicator');
1393 stockIndicators.forEach(indicator => { 1394 if (stockInfo.is_in_stock) { 1395 indicator.classList.add('in-stock'); 1396 indicator.classList.remove('out-of-stock'); 1397 } else { 1398 indicator.classList.add('out-of-stock'); 1399 indicator.classList.remove('in-stock'); 1400 } 1401 }); 1402 1403 // Update stock count 1404 const stockCounts = document.querySelectorAll('.stock-count'); 1405 stockCounts.forEach(count => { 1406 count.textContent = stockInfo.available_stock || 0; 1407 }); 1408 1409 // CRITICAL: For digital products, DO NOT modify button states here 1410 // PHP already handles button disable/enable based on LICENSE_POOL count 1411 // Only physical products need JavaScript stock validation 1412 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 1413 1414 if (isDigitalProduct) { 1415 // For digital products: ONLY update global stock info, don't touch buttons 1416 // Let PHP-rendered disabled state remain authoritative 1417 console.log('[Stock Validator] Digital product detected - skipping button state modification'); 1418 return; // Exit early, preserve PHP-set button states 1419 } 1420 1421 // ENHANCED: Force disable buttons for out-of-stock with anti-tampering (PHYSICAL PRODUCTS ONLY) 1422 const allButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy'); 1423 allButtons.forEach(button => { 1424 if (stockInfo.can_add_to_cart) { 1425 // STOCK AVAILABLE - ENABLE BUTTONS 1426 button.classList.remove(config.disabledClass); 1427 button.disabled = false; 1428 button.style.opacity = ''; 1429 button.style.cursor = ''; 1430 1431 // CRITICAL: Remove stock blocking attributes when stock becomes available 1432 button.removeAttribute('data-stock-blocked'); 1433 button.removeAttribute('data-stock-reason'); 1434 1435 1436 } else { 1437 // OUT OF STOCK - FORCE DISABLE WITH ANTI-TAMPERING 1438 button.classList.add(config.disabledClass); 1439 button.disabled = true; 1440 button.style.opacity = '0.5'; 1441 button.style.cursor = 'not-allowed'; 1442 1443 // Mark as stock-blocked to prevent re-enabling 1444 button.setAttribute('data-stock-blocked', 'true'); 1445 button.setAttribute('data-stock-reason', stockInfo.message || 'Stok tidak tersedia'); 1446 1447 1448 } 1449 }); 1450 1451 // Start aggressive button monitoring for out-of-stock variants 1452 if (!stockInfo.can_add_to_cart) { 1453 enforceStockDisabling(productId, variantId); 1454 } 1455 1456 1457 } 1458 1459 /** 1460 * Aggressively enforce button disabling for out-of-stock items 1461 * CRITICAL: For digital products, DO NOT modify button states (PHP is authoritative) 1462 */ 1463 function enforceStockDisabling(productId, variantId) { 1464 // CRITICAL: Skip enforcement for digital products - PHP handles button states 1465 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 1466 if (isDigitalProduct) { 1467 console.log('[Stock Validator] Digital product - skipping button enforcement'); 1468 return; // Exit early, let PHP-rendered disabled state remain 1469 } 1470 1471 const monitorInterval = setInterval(() => { 1472 // Check if variant changed 1473 const currentVariantSelector = document.getElementById('pd-variant-id'); 1474 const currentVariantId = currentVariantSelector ? parseInt(currentVariantSelector.value || 0) : 0; 1475 1476 if (currentVariantId !== variantId) { 1477 // Variant changed, stop monitoring this variant 1478 clearInterval(monitorInterval); 1479 1480 return; 1481 } 1482 1483 // Check current stock status using embedded data (real-time check) 1484 const currentStockInfo = getStockFromEmbeddedData(currentVariantId); 1485 1486 if (currentStockInfo && !currentStockInfo.hasStock) { 1487 // Still out of stock - continue monitoring for tampering 1488 const buttons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy'); 1489 buttons.forEach(button => { 1490 // Check if button was manually enabled (inspect attack) 1491 if (!button.disabled || button.style.opacity !== '0.5') { 1492 1493 // Force re-disable 1494 button.disabled = true; 1495 button.style.opacity = '0.5'; 1496 button.style.cursor = 'not-allowed'; 1497 button.setAttribute('data-stock-blocked', 'true'); 1498 button.setAttribute('data-stock-reason', `Stok tidak tersedia${currentStockInfo.sku ? ` untuk ${currentStockInfo.sku}` : ''}`); 1499 1500 // Show warning toast 1501 if (window.showToast) { 1502 window.showToast({ 1503 type: 'warning',
1504 title: 'Aksi Tidak Diizinkan', 1505 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 1506 duration: 3000 1507 }); 1508 } 1509 } 1510 }); 1511 } else { 1512 // Stock available OR embedded data shows stock - stop monitoring 1513 clearInterval(monitorInterval); 1514 1515 // For PHYSICAL products only, enable buttons 1516 // For DIGITAL products, skip (PHP is authoritative) 1517 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 1518 1519 if (!isDigitalProduct && currentStockInfo && currentStockInfo.hasStock) { 1520 const buttons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy'); 1521 buttons.forEach(button => { 1522 button.disabled = false; 1523 button.style.opacity = ''; 1524 button.style.cursor = ''; 1525 button.removeAttribute('data-stock-blocked'); 1526 button.removeAttribute('data-stock-reason'); 1527 button.classList.remove(config.disabledClass); 1528 }); 1529 1530 } 1531 } 1532 }, 500); // Check every 500ms for tampering attempts 1533 1534 // Auto-stop monitoring after 30 seconds to prevent memory leaks 1535 setTimeout(() => { 1536 clearInterval(monitorInterval); 1537 1538 }, 30000); 1539 } 1540 1541 /** 1542 * Override addToCartSubmit function to add stock validation 1543 * This function now works with pd-addtocart.js integration 1544 */ 1545 function overrideAddToCartSubmitFunction() { 1546 // Check if pd-addtocart.js is already loaded 1547 const pdAddToCartExists = typeof window.addToCartSubmit === 'function' && 1548 window.addToCartSubmit.toString().includes('showToast'); 1549 1550 if (pdAddToCartExists) { 1551 1552 1553 // Store the pd-addtocart.js function 1554 const pdAddToCartSubmit = window.addToCartSubmit; 1555 window._pdAddToCartSubmit = pdAddToCartSubmit; 1556 1557 // Override with stock validation that calls pd-addtocart.js 1558 window.addToCartSubmit = async function(e, productId) { 1559 1560 // Don't prevent default here - let pd-addtocart handle it if stock is valid 1561 1562 try { 1563 // Extract product data from form (same way as pd-addtocart.js) 1564 const form = e.target; 1565 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 1566 1567 // CRITICAL: Always get current variant from selector (real-time) 1568 let variantId = 0; 1569 const variantSelector = document.getElementById('pd-variant-id'); 1570 if (variantSelector) { 1571 variantId = parseInt(variantSelector.value || '0', 10) || 0; 1572 } 1573 1574 if (!productId || productId <= 0) { 1575 return pdAddToCartSubmit(e, productId); 1576 } 1577 1578 1579 // Validate stock before proceeding 1580 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 1581 1582 if (isValid) { 1583 // Stock OK, call pd-addtocart.js function (it will handle toast) 1584 return pdAddToCartSubmit(e, productId); 1585 } else { 1586 // Prevent the form submission 1587 e.preventDefault(); 1588 // Stock validation failed, modal already shown by validateBeforeCart 1589 return false; 1590 } 1591 1592 } catch (error) { 1593 e.preventDefault(); 1594 // On error, show modal and don't proceed 1595 showStockModal({
1596 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 1597 is_active: false 1598 }, 'cart'); 1599 return false; 1600 } 1601 }; 1602 } else { 1603 // Original logic for when pd-addtocart.js is not present 1604 const originalAddToCartSubmit = typeof window.addToCartSubmit === 'function' ? window.addToCartSubmit : null; 1605 1606 if (originalAddToCartSubmit) { 1607 window._originalAddToCartSubmit = originalAddToCartSubmit; 1608 } 1609 1610 // Override with stock validation 1611 window.addToCartSubmit = async function(e, productId) { 1612 1613 e.preventDefault(); 1614 1615 try { 1616 const form = e.target; 1617 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 1618 1619 // CRITICAL: Always get current variant from selector (real-time) 1620 let variantId = 0; 1621 const variantSelector = document.getElementById('pd-variant-id'); 1622 if (variantSelector) { 1623 variantId = parseInt(variantSelector.value || '0', 10) || 0; 1624 } 1625 1626 if (!productId || productId <= 0) { 1627 if (originalAddToCartSubmit) return originalAddToCartSubmit(e, productId); 1628 return false; 1629 } 1630 1631 1632 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 1633 1634 if (isValid) { 1635 if (originalAddToCartSubmit) { 1636 return originalAddToCartSubmit(e, productId); 1637 } else { 1638 if (typeof window.addToCart === 'function') { 1639 await window.addToCart(productId, qty, variantId); 1640 } 1641 } 1642 } else { 1643 } 1644 1645 } catch (error) { 1646 showStockModal({ 1647 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 1648 is_active: false 1649 }, 'cart'); 1650 } 1651 1652 return false; 1653 }; 1654 } 1655 1656 1657 } 1658 1659 /** 1660 * Override buyNow function to add stock validation 1661 */ 1662 function overrideBuyNowFunction() { 1663 // Store original buyNow function if it exists 1664 const originalBuyNow = typeof window.buyNow === 'function' ? window.buyNow : null; 1665 1666 // Override with stock validation 1667 window.buyNow = async function(e) { 1668 1669 try { 1670 // Get product data (same way as pd-buynow.js) 1671 const form = document.getElementById('pd-buy-form'); 1672 if (!form) { 1673 if (originalBuyNow) return originalBuyNow(e); 1674 return; 1675 } 1676 1677 const productId = parseInt(form.dataset.pid || '0', 10); 1678 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 1679 1680 // CRITICAL: Always get current variant from selector (real-time) 1681 let variantId = 0; 1682 const variantSelector = document.getElementById('pd-variant-id'); 1683 if (variantSelector) { 1684 variantId = parseInt(variantSelector.value || '0', 10) || 0; 1685 } 1686 1687 if (!productId || productId <= 0) { 1688 if (originalBuyNow) return originalBuyNow(e); 1689 return; 1690 } 1691 1692 1693 // Validate stock before proceeding 1694 const isValid = await validateBeforeCart(productId, variantId, qty, 'buy'); 1695 1696 if (isValid) { 1697 // Stock OK, call original buyNow function 1698 if (originalBuyNow) { 1699 return originalBuyNow(e); 1700 } else { 1701 } 1702 } else { 1703 // Stock validation failed, modal already shown by validateBeforeCart 1704 } 1705 1706 } catch (error) { 1707 // On error, show modal and don't proceed 1708 showStockModal({
1709 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 1710 is_active: false 1711 }, 'buy'); 1712 } 1713 }; 1714 1715 1716 } 1717 1718 /** 1719 * Initialize stock validation 1720 */ 1721 function init() { 1722 // Wait for DOM ready 1723 if (document.readyState === 'loading') { 1724 addEventListener(document, 'DOMContentLoaded', function() { 1725 initializeButtonAttributes(); 1726 setupVariantChangeIntegration(); // Setup variant change integration 1727 interceptCartButtons(); 1728 setupGlobalClickInterceptor(); // Setup global click security interceptor // Override functions after other scripts load 1729 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 1730 setTimeout(() => { 1731 overrideAddToCartSubmitFunction(); 1732 overrideBuyNowFunction(); 1733 }, 100); 1734 1735 setTimeout(() => { 1736 // Re-check and re-override if pd-addtocart.js loaded later 1737 overrideAddToCartSubmitFunction(); 1738 }, 500); 1739 1740 setTimeout(() => { 1741 // Final check for very late loading scripts 1742 overrideAddToCartSubmitFunction(); 1743 }, 1000); 1744 1745 1746 }); 1747 } else { 1748 initializeButtonAttributes(); 1749 setupVariantChangeIntegration(); // Setup variant change integration 1750 interceptCartButtons(); 1751 setupGlobalClickInterceptor(); // Setup global click security interceptor 1752 1753 // Override functions after other scripts load 1754 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 1755 setTimeout(() => { 1756 overrideAddToCartSubmitFunction(); 1757 overrideBuyNowFunction(); 1758 }, 100); 1759 1760 setTimeout(() => { 1761 // Re-check and re-override if pd-addtocart.js loaded later 1762 overrideAddToCartSubmitFunction(); 1763 }, 500); 1764 1765 setTimeout(() => { 1766 // Final check for very late loading scripts 1767 overrideAddToCartSubmitFunction(); 1768 }, 1000); 1769 1770 1771 } 1772 } 1773 1774 // Public API 1775 return { 1776 init: init, 1777 checkStock: checkStock, 1778 validateBeforeCart: validateBeforeCart, 1779 updateStockDisplay: updateStockDisplay, 1780 showStockModal: showStockModal, 1781 config: config 1782 }; 1783})(); 1784 1785// Auto-initialize 1786if (typeof window !== 'undefined') { 1787 StockValidator.init(); 1788}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.