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 = '
No hay ofertas vigentes.
';
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();
}