/** * Stock Validation - Vanilla JS Version * * This script provides client-side stock validation without jQuery dependency * - Works with native JavaScript and fetch API * - Graceful fallback for older browsers * - Same functionality as jQuery version */ window.StockValidator = (function() { 'use strict'; const config = { apiEndpoint: '/api/stock/check.php', modalSelector: '#stock-modal-overlay', // We create our own modal loadingClass: 'loading-stock', disabledClass: 'stock-disabled', debug: true // Enable debug logging }; /** * Cross-browser event listener helper */ function addEventListener(element, event, handler) { if (element.addEventListener) { element.addEventListener(event, handler); } else if (element.attachEvent) { element.attachEvent('on' + event, handler); } } /** * Get stock information from embedded variant data (instant, no AJAX needed) */ function getStockFromEmbeddedData(variantId) { // Check if embedded variant data exists if (!window.__PD_VARIANTS_WITH_BRANCH__ || !Array.isArray(window.__PD_VARIANTS_WITH_BRANCH__)) { return null; } const variants = window.__PD_VARIANTS_WITH_BRANCH__; // SPECIAL CASE: Simple product (no variants) - empty array means simple product if (variants.length === 0) { // For simple products, we cannot determine stock from embedded data // Return null to force API validation return null; } // If no variant specified, check base product stock (all variants) if (!variantId || variantId <= 0) { // For base product, check if ANY variant has stock const isPreorder = window.isPreorderProduct || false; let hasAnyStock = false; let totalStock = 0; if (isPreorder) { // For pre-order, check preorder_remaining hasAnyStock = variants.some(v => { if ('preorder_remaining' in v && v.preorder_remaining !== null) { return v.preorder_remaining > 0; } return v.stock > 0; }); totalStock = variants.reduce((sum, v) => { if ('preorder_remaining' in v && v.preorder_remaining !== null) { return sum + (v.preorder_remaining || 0); } return sum + (v.stock || 0); }, 0); } else { hasAnyStock = variants.some(v => v.stock > 0); totalStock = variants.reduce((sum, v) => sum + (v.stock || 0), 0); } return { hasStock: hasAnyStock, stock: totalStock, sku: 'Base Product', variant: null }; } // Find specific variant const variant = variants.find(v => v.id === variantId); if (!variant) { return { hasStock: false, stock: 0, sku: 'Unknown Variant', variant: null }; } // For pre-order products, check preorder_remaining instead of stock const isPreorder = window.isPreorderProduct || false; let hasStock = false; let availableStock = 0; if (isPreorder && 'preorder_remaining' in variant && variant.preorder_remaining !== null) { availableStock = parseInt(variant.preorder_remaining, 10); if (isNaN(availableStock)) availableStock = 0; hasStock = availableStock > 0; } else { availableStock = variant.stock || 0; hasStock = availableStock > 0; } return { hasStock: hasStock, stock: availableStock, sku: variant.sku, variant: variant }; } /** * Cross-browser AJAX helper */ function makeRequest(url, options) { return new Promise((resolve, reject) => { // Try fetch first (modern browsers) if (typeof fetch !== 'undefined') { const fetchOptions = { method: options.method || 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: options.data ? new URLSearchParams(options.data).toString() : null }; fetch(url, fetchOptions) .then(response => { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); }) .then(resolve) .catch(reject); } else { // Fallback to XMLHttpRequest const xhr = new XMLHttpRequest(); xhr.open(options.method || 'POST', url); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onload = function() { if (xhr.status >= 200 && xhr.status < 300) { try { resolve(JSON.parse(xhr.responseText)); } catch (e) { reject(new Error('Invalid JSON response')); } } else { reject(new Error(`HTTP ${xhr.status}`)); } }; xhr.onerror = function() { reject(new Error('Network error')); }; const data = options.data ? new URLSearchParams(options.data).toString() : null; xhr.send(data); } }); } /** * Check stock via AJAX */ function checkStock(productId, variantId, qty) { return makeRequest(config.apiEndpoint, { method: 'POST', data: { product_id: productId, variant_id: variantId || 0, qty: qty || 1 } }); } /** * Create and show stock alert modal using modal.css structure */ function showStockModal(stockInfo, action = 'add to cart') { let title, message, type, iconClass; // Handle both string and object formats if (typeof stockInfo === 'string') { title = 'Stok Tidak Tersedia'; message = stockInfo; type = 'warning'; iconClass = 'warning'; } else if (stockInfo && typeof stockInfo === 'object') { if (!stockInfo.is_active) { title = 'Produk Tidak Tersedia'; message = 'Maaf, produk ini sudah tidak aktif atau telah dihapus dari katalog.'; type = 'danger'; iconClass = 'danger'; } else if (!stockInfo.is_in_stock) { title = 'Stok Habis'; message = `${stockInfo.product_name || 'Produk ini'} sedang habis stok. Silakan pilih varian lain atau tunggu restock.`; if (stockInfo.sku) { message += `
SKU: ${stockInfo.sku}`; } type = 'warning'; iconClass = 'warning'; } else if (!stockInfo.can_add_to_cart) { title = 'Stok Tidak Mencukupi'; message = `Stok ${stockInfo.product_name || 'produk'} tidak mencukupi.
`; message += `Tersedia: ${stockInfo.available_stock}, diminta: ${stockInfo.requested_qty}
`; message += `Silakan kurangi jumlah atau pilih varian lain.`; type = 'warning'; iconClass = 'warning'; } else { title = 'Stok Tidak Tersedia'; message = stockInfo.message || 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; type = 'warning'; iconClass = 'warning'; } } else { title = 'Stok Tidak Tersedia'; message = 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; type = 'warning'; iconClass = 'warning'; } // Create modal HTML using modal.css structure const modalHTML = ` `; // Remove existing modal if any const existingModal = document.getElementById('stock-modal-overlay'); if (existingModal) { existingModal.remove(); } // Add modal to body document.body.insertAdjacentHTML('beforeend', modalHTML); // Show modal with animation const modal = document.getElementById('stock-modal-overlay'); setTimeout(() => { modal.classList.add('show'); }, 10); // Add event listeners to modal buttons const closeBtn = modal.querySelector('[data-action="close"]'); const reloadBtn = modal.querySelector('[data-action="reload"]'); if (closeBtn) { addEventListener(closeBtn, 'click', closeStockModal); } if (reloadBtn) { addEventListener(reloadBtn, 'click', function() { closeStockModal(); window.location.reload(); }); } // Close modal when clicking overlay addEventListener(modal, 'click', function(event) { if (event.target === modal) { closeStockModal(); } }); // Auto-close after 8 seconds setTimeout(() => { closeStockModal(); }, 8000); } /** * Get icon SVG based on type */ function getIconSVG(type) { const icons = { danger: '', warning: '', success: '', info: '' }; return icons[type] || icons.info; } /** * Close stock modal */ function closeStockModal() { const modal = document.getElementById('stock-modal-overlay'); if (modal) { modal.classList.remove('show'); setTimeout(() => { modal.remove(); }, 200); } } // Make closeStockModal globally available if (typeof window !== 'undefined') { window.closeStockModal = closeStockModal; } /** * Get element data attribute with fallback */ function getDataAttribute(element, attr) { if (element.dataset) { return element.dataset[attr]; } else { return element.getAttribute('data-' + attr); } } /** * Validate stock before cart action */ async function validateBeforeCart(productId, variantId, qty, action = 'add') { try { // SMART VALIDATION: For simple products, check if we already have cached stock info // If buttons are enabled and cached info shows stock available, skip API validation if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart) { return true; } // For variant products or when cached info is not available, do API validation const stockInfo = await checkStock(productId, variantId, qty); if (!stockInfo.ok) { showStockModal({ message: stockInfo.error || 'Gagal mengecek stok', is_active: false }, action); return false; } if (!stockInfo.can_add_to_cart) { showStockModal(stockInfo, action); return false; } return true; // Stock available, proceed } catch (error) { console.error('Stock validation error:', error); showStockModal({ message: 'Terjadi kesalahan saat mengecek stok. Silakan coba lagi.', is_active: false }, action); return false; } } /** * Handle button click with stock validation */ function handleButtonClick(event, action) { const button = event.target; // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) if (button.hasAttribute('data-stock-blocked')) { event.preventDefault(); event.stopImmediatePropagation(); const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; showStockModal(reason); // Show security warning if (window.showToast) { window.showToast({ type: 'warning', title: 'Aksi Tidak Diizinkan', message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', duration: 3000 }); } return false; } // SECOND CHECK: Real-time stock validation against cached stock info if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { event.preventDefault(); event.stopImmediatePropagation(); showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); // Re-enforce button disabling button.disabled = true; button.style.opacity = '0.5'; button.style.cursor = 'not-allowed'; button.setAttribute('data-stock-blocked', 'true'); return false; } // Skip if already processing if (button.classList.contains(config.loadingClass)) { event.preventDefault(); return false; } // Get product data from button attributes or form let productId = parseInt(getDataAttribute(button, 'productId') || getDataAttribute(button, 'product-id') || 0); let variantId = parseInt(getDataAttribute(button, 'variantId') || getDataAttribute(button, 'variant-id') || 0); let qty = parseInt(getDataAttribute(button, 'qty') || 1); // Try to get from global variables (set by product detail page) if (productId <= 0 && typeof window.PD_ID !== 'undefined') { productId = parseInt(window.PD_ID || 0); } // Try to get from parent form if not in button if (productId <= 0) { const form = button.closest ? button.closest('form') : null; if (form) { const pidAttr = form.getAttribute('data-pid'); const pidInput = form.querySelector('[name="product_id"]'); const vidInput = form.querySelector('[name="variant_id"]'); const qtyInput = form.querySelector('[name="qty"]'); if (pidAttr) productId = parseInt(pidAttr); if (pidInput) productId = parseInt(pidInput.value || 0); if (vidInput) variantId = parseInt(vidInput.value || 0); if (qtyInput) qty = parseInt(qtyInput.value || 1); } } // CRITICAL: Always get current variant from selector (overrides button attributes) // This ensures we validate against the currently selected variant, not cached button data const variantSelector = document.getElementById('pd-variant-id'); if (variantSelector) { const currentVariantId = parseInt(variantSelector.value || 0); if (currentVariantId > 0) { variantId = currentVariantId; } } if (productId <= 0) { return true; // Let default handler deal with it } // Prevent default and validate stock event.preventDefault(); event.stopPropagation(); // Show loading state button.classList.add(config.loadingClass); const originalDisabled = button.disabled; button.disabled = true; validateBeforeCart(productId, variantId, qty, action) .then(isValid => { if (isValid) { // Stock OK, proceed with original action if (action === 'cart' && typeof window.addToCart === 'function') { window.addToCart(productId, qty, variantId); } else if (action === 'buy' && typeof window.buyNow === 'function') { // Call buyNow from pd-buynow.js without parameters (it gets data internally) window.buyNow(); } else { // Add fallback cart/buy logic here if needed } } }) .catch(error => { }) .finally(() => { // Remove loading state button.classList.remove(config.loadingClass); button.disabled = originalDisabled; }); return false; } /** * Handle mobile button click with stock validation */ function handleMobileButtonClick(event, action) { const button = event.target; // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) if (button.hasAttribute('data-stock-blocked')) { event.preventDefault(); event.stopImmediatePropagation(); const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; showStockModal(reason); // Show security warning if (window.showToast) { window.showToast({ type: 'warning', title: 'Aksi Tidak Diizinkan', message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', duration: 3000 }); } return false; } // SECOND CHECK: Real-time stock validation against cached stock info if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { event.preventDefault(); event.stopImmediatePropagation(); showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); // Re-enforce button disabling button.disabled = true; button.style.opacity = '0.5'; button.style.cursor = 'not-allowed'; button.setAttribute('data-stock-blocked', 'true'); return false; } // Skip if already processing if (button.classList.contains(config.loadingClass)) { event.preventDefault(); return false; } // Get product data from mobile FAB container let productId = 0; let variantId = 0; let qty = 1; // Try to get from mobile FAB data-pid const mobileFab = document.getElementById('pd-mobile-fab'); if (mobileFab) { const pidAttr = getDataAttribute(mobileFab, 'pid'); if (pidAttr) productId = parseInt(pidAttr); } // Try to get from global variables (set by product detail page) if (productId <= 0 && typeof window.PD_ID !== 'undefined') { productId = parseInt(window.PD_ID || 0); } // CRITICAL: Always get current variant from selector (real-time) // This ensures we validate against the currently selected variant const variantSelector = document.getElementById('pd-variant-id'); if (variantSelector) { variantId = parseInt(variantSelector.value || 0); } // Try to get quantity from quantity input const qtySelector = document.querySelector('[name="qty"]'); if (qtySelector) { qty = parseInt(qtySelector.value || 1); } // Ensure minimum quantity is 1 qty = Math.max(1, qty); if (productId <= 0) { return true; // Let default handler deal with it } // Event already prevented at interceptor level, just proceed with validation // Show loading state button.classList.add(config.loadingClass); const originalDisabled = button.disabled; const originalText = button.textContent; button.disabled = true; button.textContent = action === 'cart' ? 'Validasi...' : 'Validasi...'; // PREEMPTIVE BLOCKING: Set blocking flags BEFORE validation to prevent race conditions if (window.addToCart && typeof window.addToCart === 'function') { window.addToCart._stockValidatorBlocked = true; } // Also preemptively block other cart functions const originalAddToCartSubmit = window.addToCartSubmit; const originalPdAddToCartSubmit = window._pdAddToCartSubmit; // Temporarily block ALL cart functions during validation window.addToCartSubmit = (...args) => { return Promise.resolve(false); }; if (originalPdAddToCartSubmit) { window._pdAddToCartSubmit = (...args) => { return Promise.resolve(false); }; } validateBeforeCart(productId, variantId, qty, action) .then(isValid => { if (isValid) { // Stock OK, proceed with original action if (action === 'cart') { // For mobile cart, we need to recreate the original behavior // Strategy: Try multiple methods to ensure cart add + toast success let cartAddSuccess = false; let errorMessage = ''; // Method 1: Try original addToCart function const originalAddToCart = window.addToCart && window.addToCart._original; if (originalAddToCart && !cartAddSuccess) { try { const result = originalAddToCart(productId, qty, variantId); // Handle both sync and async returns if (result && typeof result.then === 'function') { result.then(() => { cartAddSuccess = true; }).catch(err => { errorMessage = err.message || 'addToCart failed'; }); } else { cartAddSuccess = true; } } catch (error) { errorMessage = error.message || 'addToCart exception'; } } // Method 2: Try pd-addtocart.js function (backup method) if (typeof window._pdAddToCartSubmit === 'function' && !cartAddSuccess) { try { // Create fake form for pd-addtocart.js const fakeForm = document.createElement('form'); const qtyInput = document.createElement('input'); qtyInput.name = 'qty'; qtyInput.value = qty; fakeForm.appendChild(qtyInput); // Set variant selector value for pd-addtocart to read const variantSelector = document.getElementById('pd-variant-id'); if (variantSelector) { variantSelector.value = variantId; } const fakeEvent = { preventDefault: () => {}, target: fakeForm }; // Call pd-addtocart.js function (this should handle toast internally) const result = window._pdAddToCartSubmit(fakeEvent, productId); if (result && typeof result.then === 'function') { result.then(() => { cartAddSuccess = true; }); } else { cartAddSuccess = true; } } catch (error) { } } // Method 3: Direct addToCart call (fallback) if (typeof window.addToCart === 'function' && !cartAddSuccess) { try { const result = window.addToCart(productId, qty, variantId); if (result && typeof result.then === 'function') { result.then(() => { cartAddSuccess = true; }); } else { cartAddSuccess = true; } } catch (error) { } } // Always show success toast for mobile (regardless of which method worked) // This ensures user gets feedback for successful cart addition setTimeout(() => { if (window.showToast) { // Get product details for toast const getVariantSummaryText = () => { const vs = document.getElementById('pd-variant-summary'); if (!vs) return ''; const items = Array.from(vs.querySelectorAll('.vs-item strong')) .map(el => el.textContent.trim()) .filter(Boolean); return items.length ? items.join(' / ') : ''; }; const escapeHtml = (s) => String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); const variantText = getVariantSummaryText(); const skuText = (function(){ const el = document.getElementById('pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU || 'N/A'); })(); const productName = window.PD_NAME || 'Product'; window.showToast({ type: 'success', title: 'Berhasil ditambahkan ke Keranjang', message: `${escapeHtml(productName)}${variantText ? ` · ${escapeHtml(variantText)}` : ''}
SKU: ${escapeHtml(skuText)} · Qty: ${qty}`, duration: 5000, actions: [ { label: 'Lihat Keranjang', onClick: () => { window.location.href = '/keranjang'; } }, { label: 'Lanjut Belanja', variant: 'alt', onClick: () => {} } ] }); } else { } }, 100); // Small delay to ensure cart operation completes first } else if (action === 'buy') { // For mobile buy, call buyNow if (typeof window.buyNow === 'function') { window.buyNow(); } } } else { // Stock validation FAILED - absolutely NO cart function calls allowed // DO NOT CALL ANY CART FUNCTIONS - only show modal (already handled by validateBeforeCart) } // Always restore functions after validation (success or failure) setTimeout(() => { // Remove blocking flag if (window.addToCart && typeof window.addToCart === 'function') { window.addToCart._stockValidatorBlocked = false; } // Restore other functions if (originalAddToCartSubmit && typeof originalAddToCartSubmit === 'function') { window.addToCartSubmit = originalAddToCartSubmit; } if (originalPdAddToCartSubmit && typeof originalPdAddToCartSubmit === 'function') { window._pdAddToCartSubmit = originalPdAddToCartSubmit; } }, 1000); // Shorter timeout since we want to restore quickly after success }) .catch(error => { // On error, also block the action }) .finally(() => { // Remove loading state button.classList.remove(config.loadingClass); button.disabled = originalDisabled; button.textContent = originalText; }); return false; } /** * Intercept cart buttons */ function interceptCartButtons() { // CRITICAL: Remove existing mobile cart handlers from product-detail.php setTimeout(() => { const mobileCartBtn = document.getElementById('mf-cart'); if (mobileCartBtn) { // Clone the button to remove ALL existing event listeners const newBtn = mobileCartBtn.cloneNode(true); mobileCartBtn.parentNode.replaceChild(newBtn, mobileCartBtn); // Add our own handler to the new button newBtn.addEventListener('click', function(event) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); handleMobileButtonClick(event, 'cart'); return false; }, true); } }, 50); // Note: Add to cart buttons are now handled via form submission override (addToCartSubmit) // This provides better integration with existing pd-addtocart.js toast system // Buy now buttons - updated selectors (desktop + mobile) addEventListener(document, 'click', function(event) { const target = event.target; // Desktop buy buttons if (target.classList.contains('btn-buy') || // Product detail buy button target.classList.contains('btn-buy-now') || target.classList.contains('buy-now-btn') || target.id === 'btn-buy' || // Specific ID match getDataAttribute(target, 'action') === 'buy-now' || getDataAttribute(target, 'action') === 'buy') { handleButtonClick(event, 'buy'); } // Mobile buy buttons else if (target.classList.contains('mf-buy') || // Mobile fab buy button target.id === 'mf-buy') { // Mobile buy button ID handleMobileButtonClick(event, 'buy'); } // Mobile cart buttons - ENHANCED BLOCKING else if (target.classList.contains('mf-cart') || // Mobile fab cart button target.id === 'mf-cart') { // Mobile cart button ID // Immediately prevent all event propagation event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); handleMobileButtonClick(event, 'cart'); // Return false to ensure no other handlers run return false; } }, true); // Use capturing phase to ensure we intercept before other handlers // Additional protection: Override addToCart function to add mobile button state checking const originalAddToCart = typeof window.addToCart === 'function' ? window.addToCart : null; if (originalAddToCart) { window.addToCart = async function(...args) { // Check if this call is coming from a mobile button that's currently being validated const mobileCartBtn = document.getElementById('mf-cart'); if (mobileCartBtn && mobileCartBtn.classList.contains('loading-stock')) { return Promise.reject(new Error('Mobile validation in progress - cart action blocked')); } // Check if functions are temporarily blocked (during stock failure) if (window.addToCart._stockValidatorBlocked) { return Promise.reject(new Error('Stock validation failed - cart action blocked')); } // Allow normal calls return originalAddToCart.apply(this, args); }; // Store reference to original for restoration window.addToCart._original = originalAddToCart; } } /** * Setup global click interceptor for security * This catches ALL clicks on cart/buy buttons and validates stock BEFORE any other handler */ function setupGlobalClickInterceptor() { // Add capture-phase listener to catch clicks before other handlers document.addEventListener('click', function(event) { const target = event.target; // Check if this is a cart/buy button const isCartButton = target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') || target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart'); const isBuyButton = target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') || target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy'); if (!isCartButton && !isBuyButton) { return; // Not our target, allow normal processing } const button = isCartButton ? (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')) : (target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') ? target : target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy')); // IMMEDIATE BLOCK: Prevent any processing first event.preventDefault(); event.stopImmediatePropagation(); // SECURITY CHECK 1: Is button marked as stock-blocked? if (button && button.hasAttribute('data-stock-blocked')) { const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; showStockModal(reason); // Show security alert if (window.showToast) { window.showToast({ type: 'error', title: 'Aksi Diblokir', message: 'Produk ini sedang habis stok dan tidak dapat ditambahkan ke keranjang.', duration: 4000 }); } return false; } // SECURITY CHECK 2: Real-time validation against cached stock info if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); // Re-enforce button disabling if (button) { button.disabled = true; button.style.opacity = '0.5'; button.style.cursor = 'not-allowed'; button.setAttribute('data-stock-blocked', 'true'); button.setAttribute('data-stock-reason', window._currentStockInfo.message || 'Stok tidak tersedia'); } return false; } // SECURITY CHECK 3: Smart client-side validation const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; const variantSelector = document.getElementById('pd-variant-id'); const variantId = variantSelector ? parseInt(variantSelector.value || 0) : 0; if (productId > 0) { // SMART VALIDATION: If we already have cached stock info showing stock is available, // and buttons are enabled, proceed immediately without re-validation if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart && button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { // Allow the action to proceed immediately with toast if (isCartButton) { // Trigger cart action with proper toast handling if (typeof window._pdAddToCartSubmit === 'function') { // Use pd-addtocart.js function for proper toast handling const form = document.getElementById('pd-buy-form') || button.closest('form'); if (form) { const fakeEvent = { target: form, preventDefault: () => {} }; window._pdAddToCartSubmit(fakeEvent, productId); } } else if (typeof window.addToCartSubmit === 'function') { // Fallback to addToCartSubmit const form = document.getElementById('pd-buy-form') || button.closest('form'); if (form) { const fakeEvent = { target: form, preventDefault: () => {} }; window.addToCartSubmit(fakeEvent, productId); } } else if (typeof window.addToCart === 'function') { // Direct addToCart call window.addToCart(productId, 1, variantId); } } else if (isBuyButton) { // Trigger buy action if (typeof window.buyNow === 'function') { window.buyNow(); } } return false; // Action completed } // Get stock info from embedded variant data (for variant products) const stockInfo = getStockFromEmbeddedData(variantId); if (stockInfo && stockInfo.hasStock) { // Update global stock cache window._currentStockInfo = { can_add_to_cart: true, is_in_stock: true, available_stock: stockInfo.stock, message: 'Stok tersedia' }; // IMPORTANT: Check if button was explicitly blocked by variant logic (e.g., pre-order exhausted) // If data-stock-blocked is set, respect it and don't enable the button const isExplicitlyBlocked = button.getAttribute('data-stock-blocked') === 'true'; if (!isExplicitlyBlocked) { // Remove stock blocking if it was set and enable button button.removeAttribute('data-stock-blocked'); button.removeAttribute('data-stock-reason'); button.disabled = false; button.style.opacity = ''; button.style.cursor = ''; button.classList.remove(config.disabledClass); } else { // Button is blocked by variant logic, show reason const reason = button.getAttribute('data-stock-reason') || 'Tidak tersedia'; console.log('Button blocked by variant logic:', reason); e.preventDefault(); e.stopPropagation(); return false; } // Allow the action to proceed immediately with toast if (isCartButton) { // Trigger cart action with proper toast handling if (typeof window._pdAddToCartSubmit === 'function') { // Use pd-addtocart.js function for proper toast handling const form = document.getElementById('pd-buy-form') || button.closest('form'); if (form) { const fakeEvent = { target: form, preventDefault: () => {} }; window._pdAddToCartSubmit(fakeEvent, productId); } } else if (typeof window.addToCartSubmit === 'function') { // Fallback to addToCartSubmit const form = document.getElementById('pd-buy-form') || button.closest('form'); if (form) { const fakeEvent = { target: form, preventDefault: () => {} }; window.addToCartSubmit(fakeEvent, productId); } } else if (typeof window.addToCart === 'function') { // Direct addToCart call window.addToCart(productId, 1, variantId); } } else if (isBuyButton) { // Trigger buy action if (typeof window.buyNow === 'function') { window.buyNow(); } } } else if (stockInfo && !stockInfo.hasStock) { // Update global stock cache window._currentStockInfo = { can_add_to_cart: false, is_in_stock: false, available_stock: stockInfo.stock, message: `Stok tidak tersedia${stockInfo.sku ? ` untuk ${stockInfo.sku}` : ''}` }; // Force block button button.disabled = true; button.style.opacity = '0.5'; button.style.cursor = 'not-allowed'; button.setAttribute('data-stock-blocked', 'true'); button.setAttribute('data-stock-reason', window._currentStockInfo.message); // Show modal with variant info const modalMessage = stockInfo ? `Varian yang dipilih (${stockInfo.sku}) sedang habis stok. Silakan pilih varian lain.` : 'Produk ini sedang habis stok. Silakan pilih varian lain.'; showStockModal(modalMessage); // Show security warning if (window.showToast) { window.showToast({ type: 'warning', title: 'Stok Tidak Tersedia', message: 'Varian yang dipilih sedang habis stok. Silakan pilih varian lain.', duration: 3000 }); } } else { // stockInfo is null (simple product) - this means embedded data is not available // For simple products, if buttons are enabled, we should proceed (trust the API response from updateStockDisplay) if (button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { // Allow the action to proceed immediately with toast if (isCartButton) { // Trigger cart action with proper toast handling if (typeof window._pdAddToCartSubmit === 'function') { // Use pd-addtocart.js function for proper toast handling const form = document.getElementById('pd-buy-form') || button.closest('form'); if (form) { const fakeEvent = { target: form, preventDefault: () => {} }; window._pdAddToCartSubmit(fakeEvent, productId); } } else if (typeof window.addToCartSubmit === 'function') { // Fallback to addToCartSubmit const form = document.getElementById('pd-buy-form') || button.closest('form'); if (form) { const fakeEvent = { target: form, preventDefault: () => {} }; window.addToCartSubmit(fakeEvent, productId); } } else if (typeof window.addToCart === 'function') { // Direct addToCart call window.addToCart(productId, 1, variantId); } } else if (isBuyButton) { // Trigger buy action if (typeof window.buyNow === 'function') { window.buyNow(); } } } else { // Button is disabled, don't allow action showStockModal('Produk ini sedang tidak tersedia.'); } } } else { // No product ID, block action showStockModal('Data produk tidak valid.'); } return false; // Always block at capture phase }, true); // Use capture phase to run BEFORE other listeners } /** * Initialize button data attributes from current page */ function initializeButtonAttributes() { // Get product data from page globals or form let productId = 0, variantId = 0; // Try to get from global variables (set by PHP) if (window.PD_ID) productId = parseInt(window.PD_ID); // Try to get from form const form = document.getElementById('pd-buy-form'); if (form) { const pidAttr = form.getAttribute('data-pid'); if (pidAttr) productId = parseInt(pidAttr); const varInput = document.getElementById('pd-variant-id'); if (varInput) variantId = parseInt(varInput.value || 0); } // Try to get from mobile FAB const mobileFab = document.getElementById('pd-mobile-fab'); if (mobileFab && productId <= 0) { const pidAttr = getDataAttribute(mobileFab, 'pid'); if (pidAttr) productId = parseInt(pidAttr); } // Set attributes on desktop buttons const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); desktopButtons.forEach(button => { if (productId > 0) { button.setAttribute('data-product-id', productId); if (variantId > 0) { button.setAttribute('data-variant-id', variantId); } } }); // Set attributes on mobile buttons const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); mobileButtons.forEach(button => { if (productId > 0) { button.setAttribute('data-product-id', productId); if (variantId > 0) { button.setAttribute('data-variant-id', variantId); } } }); // Initial stock display update on page load if (productId > 0) { setTimeout(() => { updateStockDisplay(productId, variantId); }, 100); } } /** * Update button attributes when variant changes */ function updateButtonAttributesForVariant(variantId) { const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; if (productId <= 0) return; // Update desktop buttons const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); desktopButtons.forEach(button => { button.setAttribute('data-product-id', productId); if (variantId > 0) { button.setAttribute('data-variant-id', variantId); } else { button.removeAttribute('data-variant-id'); } }); // Update mobile buttons const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); mobileButtons.forEach(button => { button.setAttribute('data-product-id', productId); if (variantId > 0) { button.setAttribute('data-variant-id', variantId); } else { button.removeAttribute('data-variant-id'); } }); } /** * Setup variant change integration with existing product-detail.php handlers */ function setupVariantChangeIntegration() { // Strategy: Hook into existing applyVariant function instead of adding new listener // This prevents conflicts with existing variant change functionality (gallery, pricing, etc.) // Method 1: Hook into existing applyVariant function setTimeout(() => { if (typeof window.applyVariant === 'function') { const originalApplyVariant = window.applyVariant; window.applyVariant = function(v, focusImage) { // Call original function first const result = originalApplyVariant.call(this, v, focusImage); // Then add our stock validator logic const variantId = v ? (v.id || v.ID || 0) : 0; // Update button attributes for stock validator updateButtonAttributesForVariant(variantId); // Update stock display using embedded data (instant) const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; if (productId > 0) { updateStockDisplay(productId, variantId); } return result; }; } else { setupVariantObserver(); } }, 500); // Method 2: Direct observation of hidden input value changes (fallback) setTimeout(() => { setupVariantObserver(); }, 1000); } /** * Setup variant observer as fallback when applyVariant hook fails */ function setupVariantObserver() { const variantInput = document.getElementById('pd-variant-id'); if (variantInput) { let lastVariantId = parseInt(variantInput.value || '0'); // Use MutationObserver to watch for value changes if (window.MutationObserver) { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'value') { const newVariantId = parseInt(variantInput.value || '0'); if (newVariantId !== lastVariantId) { updateButtonAttributesForVariant(newVariantId); // Update stock display with embedded data const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; if (productId > 0) { updateStockDisplay(productId, newVariantId); } lastVariantId = newVariantId; } } }); }); observer.observe(variantInput, { attributes: true, attributeFilter: ['value'] }); } // Additional polling as ultimate fallback setInterval(() => { const currentVariantId = parseInt(variantInput.value || '0'); if (currentVariantId !== lastVariantId) { updateButtonAttributesForVariant(currentVariantId); // Update stock display with embedded data const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; if (productId > 0) { updateStockDisplay(productId, currentVariantId); } lastVariantId = currentVariantId; } }, 1000); } } /** * Update stock display and enforce stock-based button blocking using embedded data */ function updateStockDisplay(productId, variantId = 0) { // Get stock info from embedded data (instant) const embeddedStockInfo = getStockFromEmbeddedData(variantId); let stockInfo; if (embeddedStockInfo) { // Convert embedded data to standard format stockInfo = { can_add_to_cart: embeddedStockInfo.hasStock, is_in_stock: embeddedStockInfo.hasStock, available_stock: embeddedStockInfo.stock, message: embeddedStockInfo.hasStock ? 'Stok tersedia' : `Stok habis${embeddedStockInfo.sku ? ` untuk ${embeddedStockInfo.sku}` : ''}`, product_name: window.PD_NAME || 'Produk', sku: embeddedStockInfo.sku }; } else { // Fallback to API call if embedded data not available checkStock(productId, variantId, 1) .then(apiStockInfo => { updateStockDisplayWithInfo(apiStockInfo, productId, variantId); }) .catch(error => { console.error('Failed to update stock display via API:', error); }); return; } updateStockDisplayWithInfo(stockInfo, productId, variantId); } /** * Update stock display with provided stock info */ function updateStockDisplayWithInfo(stockInfo, productId, variantId) { // Store current stock status globally for blocking checks window._currentStockInfo = stockInfo; // Update stock indicators const stockIndicators = document.querySelectorAll('.stock-indicator'); stockIndicators.forEach(indicator => { if (stockInfo.is_in_stock) { indicator.classList.add('in-stock'); indicator.classList.remove('out-of-stock'); } else { indicator.classList.add('out-of-stock'); indicator.classList.remove('in-stock'); } }); // Update stock count const stockCounts = document.querySelectorAll('.stock-count'); stockCounts.forEach(count => { count.textContent = stockInfo.available_stock || 0; }); // CRITICAL: For digital products, DO NOT modify button states here // PHP already handles button disable/enable based on LICENSE_POOL count // Only physical products need JavaScript stock validation const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; if (isDigitalProduct) { // For digital products: ONLY update global stock info, don't touch buttons // Let PHP-rendered disabled state remain authoritative console.log('[Stock Validator] Digital product detected - skipping button state modification'); return; // Exit early, preserve PHP-set button states } // ENHANCED: Force disable buttons for out-of-stock with anti-tampering (PHYSICAL PRODUCTS ONLY) 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'); allButtons.forEach(button => { if (stockInfo.can_add_to_cart) { // STOCK AVAILABLE - ENABLE BUTTONS button.classList.remove(config.disabledClass); button.disabled = false; button.style.opacity = ''; button.style.cursor = ''; // CRITICAL: Remove stock blocking attributes when stock becomes available button.removeAttribute('data-stock-blocked'); button.removeAttribute('data-stock-reason'); } else { // OUT OF STOCK - FORCE DISABLE WITH ANTI-TAMPERING button.classList.add(config.disabledClass); button.disabled = true; button.style.opacity = '0.5'; button.style.cursor = 'not-allowed'; // Mark as stock-blocked to prevent re-enabling button.setAttribute('data-stock-blocked', 'true'); button.setAttribute('data-stock-reason', stockInfo.message || 'Stok tidak tersedia'); } }); // Start aggressive button monitoring for out-of-stock variants if (!stockInfo.can_add_to_cart) { enforceStockDisabling(productId, variantId); } } /** * Aggressively enforce button disabling for out-of-stock items * CRITICAL: For digital products, DO NOT modify button states (PHP is authoritative) */ function enforceStockDisabling(productId, variantId) { // CRITICAL: Skip enforcement for digital products - PHP handles button states const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; if (isDigitalProduct) { console.log('[Stock Validator] Digital product - skipping button enforcement'); return; // Exit early, let PHP-rendered disabled state remain } const monitorInterval = setInterval(() => { // Check if variant changed const currentVariantSelector = document.getElementById('pd-variant-id'); const currentVariantId = currentVariantSelector ? parseInt(currentVariantSelector.value || 0) : 0; if (currentVariantId !== variantId) { // Variant changed, stop monitoring this variant clearInterval(monitorInterval); return; } // Check current stock status using embedded data (real-time check) const currentStockInfo = getStockFromEmbeddedData(currentVariantId); if (currentStockInfo && !currentStockInfo.hasStock) { // Still out of stock - continue monitoring for tampering 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'); buttons.forEach(button => { // Check if button was manually enabled (inspect attack) if (!button.disabled || button.style.opacity !== '0.5') { // Force re-disable button.disabled = true; button.style.opacity = '0.5'; button.style.cursor = 'not-allowed'; button.setAttribute('data-stock-blocked', 'true'); button.setAttribute('data-stock-reason', `Stok tidak tersedia${currentStockInfo.sku ? ` untuk ${currentStockInfo.sku}` : ''}`); // Show warning toast if (window.showToast) { window.showToast({ type: 'warning', title: 'Aksi Tidak Diizinkan', message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', duration: 3000 }); } } }); } else { // Stock available OR embedded data shows stock - stop monitoring clearInterval(monitorInterval); // For PHYSICAL products only, enable buttons // For DIGITAL products, skip (PHP is authoritative) const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; if (!isDigitalProduct && currentStockInfo && currentStockInfo.hasStock) { 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'); buttons.forEach(button => { button.disabled = false; button.style.opacity = ''; button.style.cursor = ''; button.removeAttribute('data-stock-blocked'); button.removeAttribute('data-stock-reason'); button.classList.remove(config.disabledClass); }); } } }, 500); // Check every 500ms for tampering attempts // Auto-stop monitoring after 30 seconds to prevent memory leaks setTimeout(() => { clearInterval(monitorInterval); }, 30000); } /** * Override addToCartSubmit function to add stock validation * This function now works with pd-addtocart.js integration */ function overrideAddToCartSubmitFunction() { // Check if pd-addtocart.js is already loaded const pdAddToCartExists = typeof window.addToCartSubmit === 'function' && window.addToCartSubmit.toString().includes('showToast'); if (pdAddToCartExists) { // Store the pd-addtocart.js function const pdAddToCartSubmit = window.addToCartSubmit; window._pdAddToCartSubmit = pdAddToCartSubmit; // Override with stock validation that calls pd-addtocart.js window.addToCartSubmit = async function(e, productId) { // Don't prevent default here - let pd-addtocart handle it if stock is valid try { // Extract product data from form (same way as pd-addtocart.js) const form = e.target; const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; // CRITICAL: Always get current variant from selector (real-time) let variantId = 0; const variantSelector = document.getElementById('pd-variant-id'); if (variantSelector) { variantId = parseInt(variantSelector.value || '0', 10) || 0; } if (!productId || productId <= 0) { return pdAddToCartSubmit(e, productId); } // Validate stock before proceeding const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); if (isValid) { // Stock OK, call pd-addtocart.js function (it will handle toast) return pdAddToCartSubmit(e, productId); } else { // Prevent the form submission e.preventDefault(); // Stock validation failed, modal already shown by validateBeforeCart return false; } } catch (error) { e.preventDefault(); // On error, show modal and don't proceed showStockModal({ message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', is_active: false }, 'cart'); return false; } }; } else { // Original logic for when pd-addtocart.js is not present const originalAddToCartSubmit = typeof window.addToCartSubmit === 'function' ? window.addToCartSubmit : null; if (originalAddToCartSubmit) { window._originalAddToCartSubmit = originalAddToCartSubmit; } // Override with stock validation window.addToCartSubmit = async function(e, productId) { e.preventDefault(); try { const form = e.target; const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; // CRITICAL: Always get current variant from selector (real-time) let variantId = 0; const variantSelector = document.getElementById('pd-variant-id'); if (variantSelector) { variantId = parseInt(variantSelector.value || '0', 10) || 0; } if (!productId || productId <= 0) { if (originalAddToCartSubmit) return originalAddToCartSubmit(e, productId); return false; } const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); if (isValid) { if (originalAddToCartSubmit) { return originalAddToCartSubmit(e, productId); } else { if (typeof window.addToCart === 'function') { await window.addToCart(productId, qty, variantId); } } } else { } } catch (error) { showStockModal({ message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', is_active: false }, 'cart'); } return false; }; } } /** * Override buyNow function to add stock validation */ function overrideBuyNowFunction() { // Store original buyNow function if it exists const originalBuyNow = typeof window.buyNow === 'function' ? window.buyNow : null; // Override with stock validation window.buyNow = async function(e) { try { // Get product data (same way as pd-buynow.js) const form = document.getElementById('pd-buy-form'); if (!form) { if (originalBuyNow) return originalBuyNow(e); return; } const productId = parseInt(form.dataset.pid || '0', 10); const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; // CRITICAL: Always get current variant from selector (real-time) let variantId = 0; const variantSelector = document.getElementById('pd-variant-id'); if (variantSelector) { variantId = parseInt(variantSelector.value || '0', 10) || 0; } if (!productId || productId <= 0) { if (originalBuyNow) return originalBuyNow(e); return; } // Validate stock before proceeding const isValid = await validateBeforeCart(productId, variantId, qty, 'buy'); if (isValid) { // Stock OK, call original buyNow function if (originalBuyNow) { return originalBuyNow(e); } else { } } else { // Stock validation failed, modal already shown by validateBeforeCart } } catch (error) { // On error, show modal and don't proceed showStockModal({ message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', is_active: false }, 'buy'); } }; } /** * Initialize stock validation */ function init() { // Wait for DOM ready if (document.readyState === 'loading') { addEventListener(document, 'DOMContentLoaded', function() { initializeButtonAttributes(); setupVariantChangeIntegration(); // Setup variant change integration interceptCartButtons(); setupGlobalClickInterceptor(); // Setup global click security interceptor // Override functions after other scripts load // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js setTimeout(() => { overrideAddToCartSubmitFunction(); overrideBuyNowFunction(); }, 100); setTimeout(() => { // Re-check and re-override if pd-addtocart.js loaded later overrideAddToCartSubmitFunction(); }, 500); setTimeout(() => { // Final check for very late loading scripts overrideAddToCartSubmitFunction(); }, 1000); }); } else { initializeButtonAttributes(); setupVariantChangeIntegration(); // Setup variant change integration interceptCartButtons(); setupGlobalClickInterceptor(); // Setup global click security interceptor // Override functions after other scripts load // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js setTimeout(() => { overrideAddToCartSubmitFunction(); overrideBuyNowFunction(); }, 100); setTimeout(() => { // Re-check and re-override if pd-addtocart.js loaded later overrideAddToCartSubmitFunction(); }, 500); setTimeout(() => { // Final check for very late loading scripts overrideAddToCartSubmitFunction(); }, 1000); } } // Public API return { init: init, checkStock: checkStock, validateBeforeCart: validateBeforeCart, updateStockDisplay: updateStockDisplay, showStockModal: showStockModal, config: config }; })(); // Auto-initialize if (typeof window !== 'undefined') { StockValidator.init(); }