document.addEventListener('DOMContentLoaded', () => { highlightCurrentBottomNav(); initOffersLink(); initCarousel(); initProductAddModal(); initWholesaleModal(); initProspectRegisterForm(); initCheckoutForm(); if (window.location.hash === '#ofertas') { setTimeout(() => scrollToOffers(), 300); } }); let ofertas = []; let currentIndex = 0; let modalIndex = 0; let carouselInterval = null; let activeProductPrice = 0; let activeProductMeta = { id: '', name: '', sku: '', unit: '', price: 0, equivalence: 0, offered: false, taxTypeId: 0, productionComments: '', productionCommentsAvailable: false, meals: '' }; function normalizeUnitText(unit) { return String(unit || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .trim() .toLowerCase(); } function unitAllowsFractional(unit) { const normalized = normalizeUnitText(unit); const integerOnlyUnits = ['pza', 'pz', 'pieza', 'piezas', 'paquete', 'paquetes', 'paq', 'h87']; return !integerOnlyUnits.some(item => normalized === item || normalized.includes(item)); } function normalizeIntegerQuantity(value) { const number = Number(value || 1); return Math.max(1, Math.floor(number)); } function highlightCurrentBottomNav() { const currentPath = window.location.pathname; document.querySelectorAll('.bottom-nav a').forEach(link => { const href = link.getAttribute('href') || ''; const cleanHref = href.split('#')[0].split('?')[0]; if (cleanHref === currentPath) { link.style.fontWeight = '700'; } }); } function initOffersLink() { const ofertasLinks = document.querySelectorAll('a[href*="#ofertas"]'); ofertasLinks.forEach(link => { link.addEventListener('click', event => { event.preventDefault(); const isHome = window.location.pathname === '/' || window.location.pathname === ''; const ofertasSection = document.getElementById('ofertas'); if (!isHome) { window.location.href = '/#ofertas'; return; } if (ofertasSection) { scrollToOffers(); setTimeout(() => { if (Array.isArray(ofertas) && ofertas.length > 0) { openOfferModal(currentIndex || 0); } }, 350); } }); }); } function scrollToOffers() { const ofertasSection = document.getElementById('ofertas'); if (!ofertasSection) return; ofertasSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); ofertasSection.classList.add('offers-focus'); setTimeout(() => { ofertasSection.classList.remove('offers-focus'); }, 1200); } async function initCarousel() { const track = document.getElementById('carouselTrack'); const dotsContainer = document.getElementById('carouselDots'); if (!track) return; const modalClose = document.getElementById('offerModalClose'); const modalBackdrop = document.getElementById('offerModalBackdrop'); const modalPrev = document.getElementById('offerModalPrev'); const modalNext = document.getElementById('offerModalNext'); const prevBtn = document.getElementById('prevSlide'); const nextBtn = document.getElementById('nextSlide'); const offerAddToCart = document.getElementById('offerAddToCart'); ofertas = await loadOffers(); track.innerHTML = ''; if (dotsContainer) dotsContainer.innerHTML = ''; if (!ofertas.length) { track.innerHTML = ''; return; } ofertas.forEach((offer, index) => { const slide = document.createElement('div'); slide.className = 'carousel-slide'; slide.setAttribute('role', 'button'); slide.setAttribute('tabindex', '0'); slide.setAttribute('aria-label', 'Ver oferta ampliada'); const image = document.createElement('img'); image.alt = offer.productName || 'Oferta vigente'; setOfferImage(image, offer); slide.appendChild(image); slide.addEventListener('click', () => openOfferModal(index)); slide.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openOfferModal(index); } }); track.appendChild(slide); if (dotsContainer) { const dot = document.createElement('button'); dot.className = 'carousel-dot'; if (index === 0) dot.classList.add('is-active'); dot.type = 'button'; dot.setAttribute('aria-label', `Ir a oferta ${index + 1}`); dot.addEventListener('click', () => goToSlide(index)); dotsContainer.appendChild(dot); } }); if (nextBtn) nextBtn.onclick = () => moveSlide(1); if (prevBtn) prevBtn.onclick = () => moveSlide(-1); if (modalClose) modalClose.onclick = closeOfferModal; if (modalBackdrop) modalBackdrop.onclick = closeOfferModal; if (modalPrev) modalPrev.onclick = () => navigateModal(-1); if (modalNext) modalNext.onclick = () => navigateModal(1); if (offerAddToCart) { offerAddToCart.onclick = () => { const offer = ofertas[modalIndex]; if (!offer) return; closeOfferModal(); setTimeout(() => { openProductAddModalFromData({ productId: offer.productId, productName: offer.productName, productSku: offer.productSku, productUnit: offer.productUnit, productPrice: offer.productPrice, productEquivalence: offer.productEquivalence, productOffered: (offer.productOffered ?? offer.productOfertado ?? offer.ofertado ?? true), productTaxTypeId: offer.productTaxTypeId || offer.taxTypeId || 0, productProductionComments: offer.productProductionComments || '', productProductionCommentsAvailable: Boolean(offer.productProductionCommentsAvailable || offer.productionCommentsAvailable), productMeals: offer.productMeals || offer.meals || '' }); }, 120); }; } document.addEventListener('keydown', event => { const modal = document.getElementById('offerModal'); const isOpen = modal && modal.classList.contains('is-open'); if (event.key === 'Escape') { closeOfferModal(); closeProductAddModal(); } if (isOpen && event.key === 'ArrowLeft') navigateModal(-1); if (isOpen && event.key === 'ArrowRight') navigateModal(1); }); updateCarousel(); startCarouselAutoplay(); } async function loadOffers() { try { const response = await fetch('/catalog/offers.json', { headers: { 'Accept': 'application/json' }, cache: 'no-store' }); if (!response.ok) return []; const data = await response.json(); return Array.isArray(data.offers) ? data.offers : []; } catch (error) { console.warn('No se pudieron cargar las ofertas.', error); return []; } } function setOfferImage(imageElement, offer) { imageElement.onerror = () => { imageElement.onerror = null; imageElement.src = '/static/img/product-placeholder.svg'; }; imageElement.src = offer.image; } function getSlidesPerView() { if (window.innerWidth <= 520) return 1; if (window.innerWidth <= 900) return 2; return 3; } function updateCarousel() { const track = document.getElementById('carouselTrack'); if (!track) return; const slides = track.querySelectorAll('.carousel-slide'); const dots = document.querySelectorAll('.carousel-dot'); if (!slides.length) return; const gap = parseFloat(window.getComputedStyle(track).gap || '0') || 0; const slideWidth = slides[0].getBoundingClientRect().width + gap; track.style.transform = `translateX(-${currentIndex * slideWidth}px)`; dots.forEach((dot, index) => { dot.classList.toggle('is-active', index === currentIndex); }); } function moveSlide(direction) { const slidesPerView = getSlidesPerView(); const maxIndex = Math.max(ofertas.length - slidesPerView, 0); currentIndex += direction; if (currentIndex < 0) currentIndex = maxIndex; if (currentIndex > maxIndex) currentIndex = 0; updateCarousel(); } function goToSlide(index) { const slidesPerView = getSlidesPerView(); const maxIndex = Math.max(ofertas.length - slidesPerView, 0); currentIndex = Math.min(index, maxIndex); updateCarousel(); } function startCarouselAutoplay() { if (carouselInterval) clearInterval(carouselInterval); carouselInterval = setInterval(() => { moveSlide(1); }, 4000); } function openOfferModal(index) { const modal = document.getElementById('offerModal'); const modalImage = document.getElementById('offerModalImage'); const addButton = document.getElementById('offerAddToCart'); if (!modal || !modalImage) return; modalIndex = index; const offer = ofertas[modalIndex]; if (!offer) return; setOfferImage(modalImage, offer); if (addButton) { addButton.dataset.productId = offer.productId; } modal.classList.add('is-open'); modal.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; } function navigateModal(direction) { const modalImage = document.getElementById('offerModalImage'); const addButton = document.getElementById('offerAddToCart'); if (!modalImage || !ofertas.length) return; modalIndex = (modalIndex + direction + ofertas.length) % ofertas.length; const offer = ofertas[modalIndex]; setOfferImage(modalImage, offer); if (addButton) { addButton.dataset.productId = offer.productId; } } function closeOfferModal() { const modal = document.getElementById('offerModal'); const modalImage = document.getElementById('offerModalImage'); if (!modal || !modalImage) return; modal.classList.remove('is-open'); modal.setAttribute('aria-hidden', 'true'); modalImage.src = ''; document.body.style.overflow = ''; } function initProductAddModal() { const modal = document.getElementById('productAddModal'); if (!modal) return; document.querySelectorAll('.js-open-product-modal').forEach(button => { button.addEventListener('click', event => { event.preventDefault(); openProductAddModal(button); }); }); const closeBtn = document.getElementById('productAddClose'); const cancelBtn = document.getElementById('productAddCancel'); const backdrop = document.getElementById('productAddBackdrop'); const quantityInput = document.getElementById('productAddQuantity'); if (closeBtn) closeBtn.onclick = closeProductAddModal; if (cancelBtn) cancelBtn.onclick = closeProductAddModal; if (backdrop) backdrop.onclick = closeProductAddModal; if (quantityInput) { quantityInput.addEventListener('input', updateProductAddTotal); quantityInput.addEventListener('blur', updateProductAddModeState); } const modeSelect = document.getElementById('productAddMode'); if (modeSelect) modeSelect.addEventListener('change', updateProductAddModeState); const form = document.getElementById('productAddForm'); if (form) { form.addEventListener('submit', () => { syncSelectedProductComments(); syncProductAddHiddenFields(); }); } } function openProductAddModal(button) { if (!button) return; openProductAddModalFromData({ productId: button.dataset.productId || '', productName: button.dataset.productName || 'Producto', productSku: button.dataset.productSku || 'N/D', productUnit: button.dataset.productUnit || 'Unidad', productPrice: Number(button.dataset.productPrice || 0), productEquivalence: button.dataset.productEquivalence || '', productOffered: button.dataset.productOfertado === '1' || button.dataset.productOffered === '1', productTaxTypeId: Number(button.dataset.productTaxTypeId || 0), productProductionComments: button.dataset.productProductionComments || '', productProductionCommentsAvailable: button.dataset.productProductionCommentsAvailable === '1', productMeals: button.dataset.productMeals || '' }); } function openProductAddModalFromData(product) { const modal = document.getElementById('productAddModal'); if (!modal || !product) return; const productId = product.productId || ''; const productName = product.productName || 'Producto'; const productSku = product.productSku || 'N/D'; const productUnit = product.productUnit || 'Unidad'; const productPrice = Number(product.productPrice || 0); const productEquivalence = Number(product.productEquivalence || 0); const productOffered = Boolean(product.productOffered || product.productOfertado || product.offered || product.ofertado); const productTaxTypeId = Number(product.productTaxTypeId || product.taxTypeId || 0); const productProductionComments = String(product.productProductionComments || product.productionComments || ''); const productProductionCommentsAvailable = Boolean(product.productProductionCommentsAvailable || product.productionCommentsAvailable || productProductionComments.trim()); const productMeals = String(product.productMeals || product.meals || ''); activeProductPrice = productPrice; activeProductMeta = { id: productId, name: productName, sku: productSku, unit: productUnit, price: productPrice, equivalence: productEquivalence, offered: productOffered, taxTypeId: productTaxTypeId, productionComments: productProductionComments, productionCommentsAvailable: productProductionCommentsAvailable, meals: productMeals }; setText('productAddTitle', productName); setText('productAddName', productName); setText('productAddSku', productSku); setText('productAddUnit', productUnit); setText('productAddPrice', formatCurrency(productPrice)); const equivalenceRow = document.getElementById('productAddEquivalenceRow'); if (productEquivalence > 0) { setText('productAddEquivalence', productEquivalence); if (equivalenceRow) equivalenceRow.style.display = ''; } else if (equivalenceRow) { equivalenceRow.style.display = 'none'; } const idInput = document.getElementById('productAddId'); const quantityInput = document.getElementById('productAddQuantity'); const commentsInput = document.getElementById('productAddComments'); const returnToInput = document.getElementById('productAddReturnTo'); const modeSelect = document.getElementById('productAddMode'); const modeHelp = document.getElementById('productAddModeHelp'); const quantityHelp = document.getElementById('productAddQuantityHelp'); if (idInput) idInput.value = productId; if (quantityInput) { const allowsFractional = unitAllowsFractional(productUnit); quantityInput.value = 1; quantityInput.step = allowsFractional ? '0.01' : '1'; quantityInput.min = allowsFractional ? '0.01' : '1'; quantityInput.inputMode = allowsFractional ? 'decimal' : 'numeric'; } if (commentsInput) commentsInput.value = ''; renderProductPreparationOptions(); if (returnToInput) returnToInput.value = window.location.pathname + window.location.search + window.location.hash; configureProductAddModeOptions(productUnit, productEquivalence); if (modeHelp) { modeHelp.textContent = productEquivalence > 0 ? `Puedes capturar por unidad de venta o por pieza. 1 pieza equivale aprox. a ${productEquivalence} ${productUnit}.` : (unitAllowsFractional(productUnit) ? 'Puedes capturar cantidades con decimales.' : 'Captura piezas, paquetes o unidades completas.'); } if (quantityHelp) { quantityHelp.textContent = unitAllowsFractional(productUnit) ? `Unidad = ${productUnit}. Ejemplo: 0.25, 0.50, 1.` : 'Cantidad en unidades completas.'; } syncProductAddHiddenFields(); updateProductAddModeState(); updateProductAddTotal(); modal.classList.add('is-open'); modal.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; } function splitProductOptionText(value) { return String(value || '') .split(',') .map(item => item.trim()) .filter(Boolean); } function renderProductPreparationOptions() { const commentsSection = document.getElementById('productAddCommentsSection'); const commentsContainer = document.getElementById('productAddCommentOptions'); const commentsInput = document.getElementById('productAddComments'); const commentsHelp = document.getElementById('productAddCommentsHelp'); const mealsSection = document.getElementById('productAddMealsSection'); const mealsList = document.getElementById('productAddMealsList'); const commentOptions = activeProductMeta.productionCommentsAvailable ? splitProductOptionText(activeProductMeta.productionComments) : []; const mealOptions = splitProductOptionText(activeProductMeta.meals); if (commentsInput) commentsInput.value = ''; if (commentsSection && commentsContainer) { commentsContainer.innerHTML = ''; if (commentOptions.length) { commentsSection.hidden = false; commentsSection.style.display = ''; commentOptions.forEach(optionText => { const chip = document.createElement('button'); chip.type = 'button'; chip.className = 'product-comment-chip'; chip.textContent = optionText; chip.dataset.value = optionText; chip.addEventListener('click', () => { chip.classList.toggle('is-selected'); syncSelectedProductComments(); }); commentsContainer.appendChild(chip); }); if (commentsHelp) { commentsHelp.textContent = 'Selecciona una o varias opciones. Se guardarán como comentario de la partida.'; } } else { commentsSection.hidden = true; commentsSection.style.display = 'none'; } } if (mealsSection && mealsList) { mealsList.innerHTML = ''; if (mealOptions.length) { mealsSection.hidden = false; mealsSection.style.display = ''; mealOptions.forEach(mealText => { const item = document.createElement('div'); item.className = 'product-meal-item'; item.textContent = mealText; mealsList.appendChild(item); }); } else { mealsSection.hidden = true; mealsSection.style.display = 'none'; } } } function syncSelectedProductComments() { const commentsInput = document.getElementById('productAddComments'); const selected = Array.from(document.querySelectorAll('#productAddCommentOptions .product-comment-chip.is-selected')) .map(item => item.dataset.value || item.textContent.trim()) .filter(Boolean); if (commentsInput) { commentsInput.value = selected.join(', '); } } function closeProductAddModal() { const modal = document.getElementById('productAddModal'); if (!modal) return; modal.classList.remove('is-open'); modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; } function configureProductAddModeOptions(productUnit, productEquivalence) { const modeSelect = document.getElementById('productAddMode'); if (!modeSelect) return; const hasEquivalence = Number(productEquivalence || 0) > 0; const allowsFractional = unitAllowsFractional(productUnit); modeSelect.innerHTML = ''; const baseOption = document.createElement('option'); baseOption.value = allowsFractional ? 'measure' : 'unit'; baseOption.textContent = allowsFractional ? `Por ${productUnit || 'cantidad'}` : 'Por unidad completa'; modeSelect.appendChild(baseOption); if (hasEquivalence) { const pieceOption = document.createElement('option'); pieceOption.value = 'piece'; pieceOption.textContent = 'Por pieza'; modeSelect.appendChild(pieceOption); } modeSelect.value = baseOption.value; } function updateProductAddModeState() { const quantityInput = document.getElementById('productAddQuantity'); const modeSelect = document.getElementById('productAddMode'); const quantityHelp = document.getElementById('productAddQuantityHelp'); if (!quantityInput) return; const mode = modeSelect ? modeSelect.value : 'measure'; const unit = String(activeProductMeta.unit || ''); const hasEquivalence = Number(activeProductMeta.equivalence || 0) > 0; const allowsFractional = unitAllowsFractional(unit); if (mode === 'piece') { quantityInput.step = '1'; quantityInput.min = '1'; quantityInput.inputMode = 'numeric'; quantityInput.value = normalizeIntegerQuantity(quantityInput.value); if (quantityHelp) { quantityHelp.textContent = hasEquivalence ? `Captura piezas completas. El sistema calculará la cantidad usando ${activeProductMeta.equivalence} ${activeProductMeta.unit} por pieza.` : 'Captura piezas completas.'; } } else { quantityInput.step = allowsFractional ? '0.01' : '1'; quantityInput.min = allowsFractional ? '0.01' : '1'; quantityInput.inputMode = allowsFractional ? 'decimal' : 'numeric'; if (!allowsFractional) { quantityInput.value = normalizeIntegerQuantity(quantityInput.value); } if (quantityHelp) { quantityHelp.textContent = allowsFractional ? `Unidad = ${activeProductMeta.unit || 'cantidad'}. Ejemplo: 0.25, 0.50, 1.` : `Cantidad en ${activeProductMeta.unit || 'unidades'} completas.`; } } updateProductAddTotal(); } function getProductAddEffectiveQuantity() { const quantityInput = document.getElementById('productAddQuantity'); const modeSelect = document.getElementById('productAddMode'); const quantity = Number(quantityInput && quantityInput.value ? quantityInput.value : 0); const mode = modeSelect ? modeSelect.value : 'kg'; const equivalence = Number(activeProductMeta.equivalence || 0); if (mode === 'piece' && equivalence > 0) { return normalizeIntegerQuantity(quantity) * equivalence; } if (!unitAllowsFractional(activeProductMeta.unit)) { return normalizeIntegerQuantity(quantity); } return quantity; } function syncProductAddHiddenFields() { syncSelectedProductComments(); const form = document.getElementById('productAddForm'); if (!form) return; ensureHiddenInput(form, 'product_sku', activeProductMeta.sku); ensureHiddenInput(form, 'product_name', activeProductMeta.name); ensureHiddenInput(form, 'product_unit', activeProductMeta.unit); ensureHiddenInput(form, 'product_price', String(activeProductMeta.price || 0)); ensureHiddenInput(form, 'product_equivalence', String(activeProductMeta.equivalence || 0)); ensureHiddenInput(form, 'product_offered', activeProductMeta.offered ? '1' : '0'); ensureHiddenInput(form, 'product_ofertado', activeProductMeta.offered ? '1' : '0'); ensureHiddenInput(form, 'tax_type_id', String(activeProductMeta.taxTypeId || 0)); ensureHiddenInput(form, 'qty_mode', document.getElementById('productAddMode')?.value || ''); ensureHiddenInput(form, 'quantity_input', document.getElementById('productAddQuantity')?.value || '0'); ensureHiddenInput(form, 'effective_quantity', String(getProductAddEffectiveQuantity() || 0)); } function ensureHiddenInput(form, name, value) { let input = form.querySelector(`input[name="${name}"]`); if (!input) { input = document.createElement('input'); input.type = 'hidden'; input.name = name; form.appendChild(input); } input.value = value == null ? '' : String(value); } function updateProductAddTotal() { const effectiveQuantity = getProductAddEffectiveQuantity(); setText('productAddLineTotal', formatCurrency(activeProductPrice * effectiveQuantity)); const form = document.getElementById('productAddForm'); if (form) { ensureHiddenInput(form, 'effective_quantity', String(effectiveQuantity || 0)); } } function setText(id, text) { const element = document.getElementById(id); if (element) element.textContent = text; } function formatCurrency(value) { const number = Number.isFinite(value) ? value : 0; return `$${number.toFixed(2)}`; } window.addEventListener('resize', () => { window.requestAnimationFrame(updateCarousel); }); /* ========================================================= MODAL MAYOREO ========================================================= */ function initWholesaleModal() { const modal = document.getElementById('wholesaleModal'); if (!modal) return; document.querySelectorAll('.js-open-wholesale-modal').forEach(button => { button.addEventListener('click', event => { event.preventDefault(); openWholesaleModal(); }); }); const closeBtn = document.getElementById('wholesaleClose'); const backdrop = document.getElementById('wholesaleBackdrop'); if (closeBtn) closeBtn.onclick = closeWholesaleModal; if (backdrop) backdrop.onclick = closeWholesaleModal; document.addEventListener('keydown', event => { if (event.key === 'Escape') closeWholesaleModal(); }); } function openWholesaleModal() { const modal = document.getElementById('wholesaleModal'); if (!modal) return; modal.classList.add('is-open'); modal.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; } function closeWholesaleModal() { const modal = document.getElementById('wholesaleModal'); if (!modal) return; modal.classList.remove('is-open'); modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; } /* ========================================================= REGISTRO DE PROSPECTO - VALIDACIONES FRONTEND ========================================================= */ function initProspectRegisterForm() { const form = document.getElementById('prospectRegisterForm'); if (!form) return; const nameInput = document.getElementById('name'); const phoneInput = document.getElementById('phone'); const passwordInput = document.getElementById('password'); const confirmPasswordInput = document.getElementById('confirm_password'); if (nameInput) { nameInput.addEventListener('input', () => { nameInput.value = normalizeNameForRegister(nameInput.value); }); nameInput.addEventListener('blur', () => { nameInput.value = normalizeNameForRegister(nameInput.value).trim(); }); } if (phoneInput) { phoneInput.addEventListener('input', () => { phoneInput.value = phoneInput.value.replace(/\D/g, '').slice(0, 10); }); } if (passwordInput) { passwordInput.addEventListener('input', updatePasswordPolicyState); } if (confirmPasswordInput) { confirmPasswordInput.addEventListener('input', updatePasswordPolicyState); } form.addEventListener('submit', event => { const errors = []; const name = document.getElementById('name'); const phone = document.getElementById('phone'); const email = document.getElementById('email'); const password = document.getElementById('password'); const confirmPassword = document.getElementById('confirm_password'); if (name) name.value = normalizeNameForRegister(name.value).trim(); if (!name || !name.value.trim()) errors.push('El nombre es obligatorio.'); if (name && /\s{2,}/.test(name.value)) errors.push('El nombre debe usar solo un espacio entre palabras.'); if (name && name.value !== name.value.toUpperCase()) errors.push('El nombre debe estar en mayúsculas.'); if (!phone || !/^\d{10}$/.test(phone.value)) errors.push('El teléfono debe contener exactamente 10 números.'); if (!email || !email.validity.valid) errors.push('Ingresa un correo electrónico válido.'); if (!password || password.value.length < 6) { errors.push('La contraseña debe tener al menos 6 caracteres.'); } if (password && !/\d/.test(password.value)) { errors.push('La contraseña debe contener al menos un número.'); } if (!confirmPassword || !confirmPassword.value) { errors.push('Confirma tu contraseña.'); } if (password && confirmPassword && password.value !== confirmPassword.value) { errors.push('Las contraseñas no coinciden.'); } if (errors.length) { event.preventDefault(); showFormErrors(form, errors); } }); } function normalizeSingleSpaces(value) { return (value || '').replace(/\s+/g, ' ').trimStart(); } function normalizeNameForRegister(value) { return normalizeSingleSpaces(value).toUpperCase(); } function showFormErrors(form, errors) { let alert = form.querySelector('.register-client-alert'); if (!alert) { alert = document.createElement('div'); alert.className = 'alert alert-error register-client-alert field-full'; form.prepend(alert); } alert.innerHTML = errors.map(error => `
${error}
`).join(''); alert.scrollIntoView({ behavior: 'smooth', block: 'center' }); } function updatePasswordPolicyState() { const password = document.getElementById('password'); const confirmPassword = document.getElementById('confirm_password'); const hint = document.getElementById('passwordPolicyHint'); if (!password) return; const hasMinimumLength = password.value.length >= 6; const hasNumber = /\d/.test(password.value); const matches = !confirmPassword || !confirmPassword.value || password.value === confirmPassword.value; password.classList.toggle('is-valid-field', hasMinimumLength && hasNumber); password.classList.toggle('is-invalid-field', password.value.length > 0 && (!hasMinimumLength || !hasNumber)); if (confirmPassword) { confirmPassword.classList.toggle('is-valid-field', confirmPassword.value.length > 0 && matches); confirmPassword.classList.toggle('is-invalid-field', confirmPassword.value.length > 0 && !matches); } if (hint) { hint.classList.toggle('is-valid-hint', hasMinimumLength && hasNumber); } } /* ========================================================= CHECKOUT - ENTREGA / PICKUP / HORARIOS / PAYLOAD ========================================================= */ function initCheckoutForm() { const form = document.getElementById('checkoutForm'); if (!form) return; const deliveryFields = document.getElementById('deliveryAddressFields'); const deliveryCostHelp = document.getElementById('deliveryCostHelp'); const countrySelect = document.getElementById('countryAddressId'); const deliveryCostEl = document.getElementById('checkoutDeliveryCost'); const totalEl = document.getElementById('checkoutTotal'); const deliveryCostInput = document.getElementById('checkoutDeliveryCostInput'); const totalInput = document.getElementById('checkoutTotalInput'); const payloadInput = document.getElementById('checkoutPayload'); const minimumScheduleInput = document.getElementById('minimumSchedule'); const scheduledDate = document.getElementById('scheduledDate'); const scheduledTime = document.getElementById('scheduledTime'); const deliveryRadios = document.querySelectorAll('input[name="delivery_method"]'); const baseTotal = Number(totalEl?.dataset.baseTotal || totalInput?.value || 0); const minimumSchedule = minimumScheduleInput?.value ? new Date(minimumScheduleInput.value) : null; function selectedDeliveryMethod() { const checked = document.querySelector('input[name="delivery_method"]:checked'); return checked ? checked.value : 'delivery'; } function selectedColonyOption() { return countrySelect?.selectedOptions?.[0] || null; } function selectedDeliveryCost() { if (selectedDeliveryMethod() !== 'delivery') return 0; const option = selectedColonyOption(); return Number(option?.dataset.cost || 0); } function currentScheduledDateTime() { if (!scheduledDate?.value || !scheduledTime?.value) return null; return new Date(`${scheduledDate.value}T${scheduledTime.value}:00`); } function updateTimeOptions() { if (!scheduledDate || !scheduledTime || !minimumSchedule) return; const selectedDate = scheduledDate.value; const currentValue = scheduledTime.value; let firstEnabled = ''; Array.from(scheduledTime.options).forEach(option => { if (!option.value) return; const optionDate = new Date(`${selectedDate}T${option.value}:00`); const isDisabled = optionDate < minimumSchedule; option.disabled = isDisabled; option.hidden = isDisabled; if (!isDisabled && !firstEnabled) firstEnabled = option.value; }); if (currentValue) { const selectedOption = Array.from(scheduledTime.options).find(option => option.value === currentValue); if (selectedOption && !selectedOption.disabled) { scheduledTime.value = currentValue; return; } } scheduledTime.value = firstEnabled || ''; } function updateDeliveryUI() { const isDelivery = selectedDeliveryMethod() === 'delivery'; if (deliveryFields) deliveryFields.style.display = isDelivery ? '' : 'none'; deliveryFields?.querySelectorAll('input, select, textarea').forEach(input => { if (['street', 'number', 'country_address_id'].includes(input.name)) { input.required = isDelivery; } }); const cost = selectedDeliveryCost(); const total = baseTotal + cost; if (deliveryCostEl) deliveryCostEl.textContent = formatCurrency(cost); if (deliveryCostInput) deliveryCostInput.value = String(cost.toFixed(2)); if (totalEl) totalEl.textContent = formatCurrency(total); if (totalInput) totalInput.value = String(total.toFixed(2)); if (deliveryCostHelp && countrySelect) { const option = selectedColonyOption(); const zip = option?.dataset.zip || ''; const city = option?.dataset.city || ''; deliveryCostHelp.textContent = isDelivery ? `Costo de envío: ${formatCurrency(cost)}${city ? ' · ' + city : ''}${zip ? ' · CP ' + zip : ''}` : 'Recolección en tienda: sin cargo de envío.'; } buildCheckoutPayload(); } function buildCheckoutPayload() { const option = selectedColonyOption(); const deliveryMethod = selectedDeliveryMethod(); const scheduleDateTime = currentScheduledDateTime(); let cartItems = []; try { const cartScript = document.getElementById('checkoutCartItemsJson'); cartItems = cartScript ? JSON.parse(cartScript.textContent || '[]') : []; } catch (error) { cartItems = []; } const payload = { customer: { name: document.getElementById('checkoutFullName')?.value || '', phone: document.getElementById('checkoutPhone')?.value || '' }, delivery: { type: deliveryMethod, cost: selectedDeliveryCost(), address: deliveryMethod === 'delivery' ? { street: document.getElementById('checkoutStreet')?.value || '', number: document.getElementById('checkoutNumber')?.value || '', country_address_id: countrySelect?.value || '', city: option?.dataset.city || '', zip_code: option?.dataset.zip || '', zone_id: option?.dataset.zoneId || '', comments: document.getElementById('checkoutDeliveryComments')?.value || '' } : null }, schedule: { date: scheduledDate?.value || '', time: scheduledTime?.value || '', datetime: scheduleDateTime ? scheduleDateTime.toISOString() : '' }, items: cartItems, totals: { cart_base_total: baseTotal, delivery_cost: selectedDeliveryCost(), total: baseTotal + selectedDeliveryCost() }, notes: document.getElementById('checkoutNotes')?.value || '' }; if (payloadInput) payloadInput.value = JSON.stringify(payload); return payload; } function validateCheckout(event) { updateTimeOptions(); const isDelivery = selectedDeliveryMethod() === 'delivery'; if (isDelivery && countrySelect && !countrySelect.value) { event.preventDefault(); alert('Selecciona una colonia para entrega a domicilio.'); countrySelect.focus(); return; } const selectedDateTime = currentScheduledDateTime(); if (!selectedDateTime || (minimumSchedule && selectedDateTime < minimumSchedule)) { event.preventDefault(); alert('Selecciona un horario al menos 1 hora y 30 minutos posterior a la hora actual.'); scheduledTime?.focus(); return; } buildCheckoutPayload(); } deliveryRadios.forEach(radio => radio.addEventListener('change', updateDeliveryUI)); countrySelect?.addEventListener('change', updateDeliveryUI); scheduledDate?.addEventListener('change', () => { updateTimeOptions(); buildCheckoutPayload(); }); scheduledTime?.addEventListener('change', buildCheckoutPayload); ['checkoutFullName', 'checkoutPhone', 'checkoutStreet', 'checkoutNumber', 'checkoutDeliveryComments', 'checkoutNotes'].forEach(id => { const element = document.getElementById(id); if (element) element.addEventListener('input', buildCheckoutPayload); }); form.addEventListener('submit', validateCheckout); updateTimeOptions(); updateDeliveryUI(); }