Products
📷 # Nombre Desc. Código Precio Desc.% Qty Vendidos Metal Familia Talla Acciones
Carrito AVONZA
Checkout
1
Shipping
2
Payment
3
Confirm
Shop
Adding to: All
Alinear:
Collections
All

Admin · Collection

Add New Product

📷 Click to upload photo

Name and Product # are required.

Admin · Page

Add Section

Add Section `; const win = window.open('','_blank','width=760,height=900,scrollbars=yes'); if(win){ win.document.write(html); win.document.close(); } } function ordenToggle(bodyId, header){ const body = document.getElementById(bodyId); if(!body) return; const chevron = header.querySelector('.orden-chevron'); const open = body.style.display === 'grid'; body.style.display = open ? 'none' : 'grid'; if(chevron) chevron.style.transform = open ? '' : 'rotate(180deg)'; } function coPlaceOrder(){ // Build order record const s = coData.shipping; const p = coData.payment; const opt = SHIPPING_OPTS.find(o=>o.id===s.shippingOpt)||SHIPPING_OPTS[0]; let subtotal = 0; const items = cart.map(c=>{ const pr = resolveCartItem(c); const price = parseFloat((pr?.price||'').replace(/[^0-9.]/g,''))||0; subtotal += price * c.qty; return { name: pr?.name||'', num: pr?.num||'', qty: c.qty, price: pr?.price||'' }; }); const shipCost = opt.price; const tax = (subtotal + shipCost) * TAX; const total = subtotal + shipCost + tax; const orderId = 'AVZ-' + Date.now().toString().slice(-6); const now = new Date(); const dateStr = now.toLocaleDateString('es-MX',{year:'numeric',month:'long',day:'numeric'}) + ' ' + now.toLocaleTimeString('es-MX',{hour:'2-digit',minute:'2-digit'}); ordenesLoad(); orders.push({ id: orderId, date: dateStr, ts: now.toISOString().slice(0,10), // YYYY-MM-DD for date filtering status: 'procesando', shipping: s, payment: p, shippingMethod: opt.name + ' — ' + opt.desc, items, subtotal: subtotal.toFixed(2), shipCost: shipCost.toFixed(2), tax: tax.toFixed(2), total: total.toFixed(2) }); ordenesSave(); // Send order + nota de venta PDF to backend (admin + dev + customer emails) const orderRecord = orders[orders.length-1]; fetch('api/order_complete.php',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({order: orderRecord}) }).catch(()=>{ /* api/ not deployed — silent */ }); // Register customer account if requested if(s.createAccount && s.email){ fetch('api/register_customer.php',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ email: s.email, firstName: s.firstName||'', lastName: s.lastName||'', phone: s.phone||'', password: s.accountPass||'' }) }).catch(()=>{}); } // Mark welcome discount as used so it can't be reused if(p&&p.discountApplied){ try{ localStorage.setItem('avz_discount_available','0'); }catch(e){} } // Deduct stock and increment sold for each purchased product cart.forEach(c=>{ const pr = resolveCartItem(c); if(!pr) return; const curQty = parseInt(pr.qty)||0; pr.qty = String(Math.max(0, curQty - c.qty)); pr.sold = (parseInt(pr.sold)||0) + c.qty; }); save(); // Refresh all product displays so AGOTADO appears immediately fillGrid(); const shop=document.getElementById('shopPanel'); if(shop&&shop.classList.contains('open')){ spRenderSidebar(); spRender(); } if(typeof ppRender==='function') ppRender(); // Success screen cart=[]; cartSave(); cartUpdateBadge(); document.getElementById('coInner').innerHTML=`
¡Orden Confirmada!

Gracias por tu compra. Tu confirmación fue enviada a ${esc(s.email)}.
Tu joyería está siendo preparada con cuidado.

Orden ${orderId}

`; document.getElementById('coStep3').classList.add('done'); [1,2,3].forEach(n=>document.getElementById('coStep'+n).classList.remove('active')); } function coBindEvents(){ /* events bound inline */ } /* ═══════════════════════════════════════ PRODUCTS PANEL ═══════════════════════════════════════ */ let ppNewRowVisible = false; let ppEditImgIdx = null; let ppNewImgData = null; function openProductsPanel(){ document.getElementById('productsPanel').classList.add('open'); if(window._updateNav) window._updateNav(); document.getElementById('ppSearch').value=''; ppRenderStats(); ppRender(); } function closeProductsPanel(){ document.getElementById('productsPanel').classList.remove('open'); if(window._updateNav) window._updateNav(); } function ppSetTab(tab){ const slotP=document.getElementById('ppTabSlotProducts'); const slotV=document.getElementById('ppTabSlotVendidos'); const btnP=document.getElementById('ppTabProducts'); const btnV=document.getElementById('ppTabVendidos'); if(tab==='products'){ slotP.style.display=''; slotV.style.display='none'; btnP.style.background='var(--t)'; btnP.style.color='#fff'; btnV.style.background='rgba(255,255,255,.15)'; btnV.style.color='rgba(255,255,255,.7)'; } else { slotP.style.display='none'; slotV.style.display='flex'; btnV.style.background='var(--t)'; btnV.style.color='#fff'; btnP.style.background='rgba(255,255,255,.15)'; btnP.style.color='rgba(255,255,255,.7)'; ppRenderVendidos(); } } function resetAllOrders(){ if(!confirm('¿Resetear TODAS las ventas y órdenes? Esta acción eliminará todo el historial y no se puede deshacer.')) return; orders=[]; try{ localStorage.removeItem('avz_orders'); }catch(e){} save(); ventasRender(); // Also re-render ordenes if visible ordenesLoad(); ordenesRender(); } function resetAllSales(){ if(!confirm('¿Resetear TODAS las ventas a 0? Esta acción no se puede deshacer.')) return; products.forEach(p=>{ p.sold=0; }); save(); // Re-render whichever vendidos view is visible ppRenderVendidos(); _renderVendidosTable(); ppRenderStats(); dashShow('vendidos'); } function _renderVendidosTable(){ // Works for both dashboard slot and products tab const wrap=document.getElementById('vendidosTableWrap')||document.getElementById('ppTabVendidosTable'); if(!wrap) return; const filterEl=document.getElementById('vendidosSkuFilter'); const sku=(filterEl?filterEl.value:'').toLowerCase().trim(); let ranked=[...products].sort((a,b)=>(parseInt(b.sold)||0)-(parseInt(a.sold)||0)); if(sku) ranked=ranked.filter(p=>(p.num||'').toLowerCase().includes(sku)||(p.name||'').toLowerCase().includes(sku)); const maxSold=ranked.length?(parseInt(ranked[0].sold)||1):1; if(!ranked.length){ wrap.innerHTML='
Sin resultados para "'+esc(sku)+'"
'; return; } if(!ranked.some(p=>(parseInt(p.sold)||0)>0) && !sku){ wrap.innerHTML='
Sin ventas registradas aún.
'; return; } wrap.innerHTML='' +'' +'' +'' +'' +'' +'' +'' +'' +'' +ranked.map((p,idx)=>{ const sold=parseInt(p.sold)||0; const pct=maxSold>0?Math.round((sold/maxSold)*100):0; const disc=parseInt(p.discount)||0; const barColor=idx===0?'#f39c12':idx===1?'#bdc3c7':idx===2?'#cd7f32':'var(--t)'; const medal=idx===0?'🥇':idx===1?'🥈':idx===2?'🥉':''; return '' +'' +'' +'' +'' +'' +'' +'' +''; }).join('') +'
#SKUProductoMetalPrecioStockVentas
'+(medal||(idx+1))+''+esc(p.num||'')+'
'+esc(p.name||'')+'
'+(disc?'-'+disc+'%':'')+'
'+esc(p.metal||'—')+''+esc(p.price||'—')+''+(parseInt(p.qty)||0)+'
' +'
' +''+sold+'' +'
'; } function ppRenderVendidos(){ const el=document.getElementById('ppTabSlotVendidos'); if(!el) return; const resetBtn=isMainAdmin()?'':''; el.innerHTML='
' +'
' +'
🏆 Más Vendidos
' +resetBtn +'
' +'

Por número de producto · orden descendente

' +'
' +'
' +'
'; _renderVendidosTable(); } function ppRenderStats(){ const total = products.length; const totalQty = products.reduce((s,p)=>s+(parseInt(p.qty)||0),0); const totalSold = products.reduce((s,p)=>s+(parseInt(p.sold)||0),0); const outOfStock = products.filter(p=>(parseInt(p.qty)||0)===0).length; document.getElementById('ppStats').innerHTML= '
' +'
'+total+'
Productos
' +'
'+totalQty+'
En Stock
' +'
'+totalSold+'
Total Vendidos
' +'
'+outOfStock+'
Agotados
' +'
'; } function ppRender(){ const q = (document.getElementById('ppSearch').value||'').toLowerCase().trim(); const tbody = document.getElementById('ppBody'); tbody.innerHTML=''; if(ppNewRowVisible) tbody.appendChild(ppBuildNewRow()); const filtered = q ? products.filter(p=>(p.name+p.num+(p.desc||'')).toLowerCase().includes(q)) : products; if(!filtered.length && !ppNewRowVisible){ const tr=document.createElement('tr'); tr.innerHTML=`No products found.`; tbody.appendChild(tr); return; } filtered.forEach(p=>{ const i = products.indexOf(p); const tr=document.createElement('tr'); const thumb = p.img ? `` : `
📷
`; tr.innerHTML=` ${thumb} ${currentUserRole==='vendedor'?'':``}
${p.sold||0} sold
${ppSizeCell(p,i)} `; tbody.appendChild(tr); }); if(!document.getElementById('ppImgInput')){ const inp=document.createElement('input'); inp.type='file';inp.accept='image/*';inp.id='ppImgInput';inp.style.display='none'; inp.onchange=function(e){ const file=e.target.files[0]; if(!file||ppEditImgIdx===null) return; compressImage(file).then(dataUrl=>{ products[ppEditImgIdx].img=dataUrl; save().then(()=>{ ppRender(); fillGrid(); }); }); }; document.body.appendChild(inp); } } function ppBuildNewRow(){ const tr = document.createElement('tr'); tr.className = 'pp-new-row'; // Photo cell const tdPhoto = document.createElement('td'); const ph = document.createElement('div'); ph.className='pp-thumb-ph'; ph.id='ppNewThumbPh'; ph.title='Add photo'; ph.textContent='📷'; ph.onclick = ()=>document.getElementById('ppNewImgInput').click(); const fileInp = document.createElement('input'); fileInp.type='file'; fileInp.id='ppNewImgInput'; fileInp.accept='image/*'; fileInp.style.display='none'; fileInp.onchange = ppNewImgPreview; const prevImg = document.createElement('img'); prevImg.id='ppNewImgPreview'; prevImg.style.cssText='display:none;width:54px;height:54px;object-fit:cover;border-radius:2px;cursor:pointer;border:1px solid var(--border)'; prevImg.onclick = ()=>document.getElementById('ppNewImgInput').click(); tdPhoto.append(ph, fileInp, prevImg); tr.appendChild(tdPhoto); // Simple text/number inputs: #, Name (Price comes after Description) [['pn_num','text','AVZ-001'],['pn_name','text','Product name']].forEach(([id,type,ph])=>{ const td=document.createElement('td'); const inp=document.createElement('input'); inp.id=id; inp.type=type; inp.placeholder=ph; td.appendChild(inp); tr.appendChild(td); }); // Description textarea (before Price) const tdDesc=document.createElement('td'); const ta=document.createElement('textarea'); ta.id='pn_desc'; ta.placeholder='Description…'; ta.style.cssText='min-width:140px;height:38px'; tdDesc.appendChild(ta); tr.appendChild(tdDesc); // Price const tdPrice=document.createElement('td'); const prInp=document.createElement('input'); prInp.id='pn_price'; prInp.type='text'; prInp.placeholder='$0'; tdPrice.appendChild(prInp); tr.appendChild(tdPrice); // Qty const tdQty=document.createElement('td'); const qInp=document.createElement('input'); qInp.id='pn_qty'; qInp.type='number'; qInp.min='0'; qInp.placeholder='0'; tdQty.appendChild(qInp); tr.appendChild(tdQty); // Sold placeholder const tdSold=document.createElement('td'); tdSold.innerHTML='0 sold'; tr.appendChild(tdSold); // Metal select const tdMetal=document.createElement('td'); const metalSel=document.createElement('select'); metalSel.id='pn_metal'; metalSel.className='pp-size-sel'; metalSel.style.minWidth='110px'; ['','Oro','Plata','Platino','Acero Inoxidable','Paladio','Titanio','Laton','Chapa de Oro','Cobre'].forEach(m=>{ const o=document.createElement('option'); o.value=m; o.textContent=m||'-- Metal --'; metalSel.appendChild(o); }); tdMetal.appendChild(metalSel); tr.appendChild(tdMetal); // Family select const tdFam=document.createElement('td'); const famSel=document.createElement('select'); famSel.id='pn_family'; FAMILIES.filter(f=>f.id!=='all').forEach(f=>{ const o=document.createElement('option'); o.value=f.id; o.textContent=f.icon+' '+f.label; famSel.appendChild(o); }); tdFam.appendChild(famSel); tr.appendChild(tdFam); // Size select const tdSize=document.createElement('td'); const sizeSel=document.createElement('select'); sizeSel.id='pn_size'; [['small','Small'],['medium','Medium'],['large','Large'],['wide','Wide']].forEach(([v,l])=>{ const o=document.createElement('option'); o.value=v; o.textContent=l; if(v==='medium') o.selected=true; sizeSel.appendChild(o); }); tdSize.appendChild(sizeSel); tr.appendChild(tdSize); // Actions const tdAct=document.createElement('td'); const saveBtn=document.createElement('button'); saveBtn.className='pp-save-new'; saveBtn.textContent='Save'; saveBtn.onclick=ppSaveNew; const cancelBtn=document.createElement('button'); cancelBtn.className='pp-cancel-new'; cancelBtn.textContent='Cancel'; cancelBtn.onclick=ppCancelNew; const wrap=document.createElement('div'); wrap.className='pp-row-actions'; wrap.append(saveBtn,cancelBtn); tdAct.appendChild(wrap); tr.appendChild(tdAct); return tr; } function ppNewImgPreview(e){ const file=e.target.files[0]; if(!file) return; compressImage(file).then(dataUrl=>{ ppNewImgData=dataUrl; const ph=document.getElementById('ppNewThumbPh'); const prev=document.getElementById('ppNewImgPreview'); if(ph) ph.style.display='none'; if(prev){ prev.src=ppNewImgData; prev.style.display='block'; } }); } function ppShowNewRow(){ ppNewRowVisible=true; ppNewImgData=null; ppRender(); const el=document.getElementById('pn_name'); if(el) el.focus(); } function ppCancelNew(){ ppNewRowVisible=false; ppNewImgData=null; ppRender(); } function ppSaveNew(){ const name=(document.getElementById('pn_name').value||'').trim(); const num=(document.getElementById('pn_num').value||'').trim(); if(!name||!num){ alert('Name and Product # are required.'); return; } if(products.some(p=>p.num===num)){ alert('Product # '+num+' already exists. Use a unique number.'); return; } products.unshift({ name, num, desc:(document.getElementById('pn_desc').value||'').trim(), price:(document.getElementById('pn_price').value||'').trim(), qty:(document.getElementById('pn_qty').value||'0').trim(), size:document.getElementById('pn_size').value, metal:(document.getElementById('pn_metal')?document.getElementById('pn_metal').value:''), family:document.getElementById('pn_family')?document.getElementById('pn_family').value:'other', sold:0, img:ppNewImgData }); ppNewRowVisible=false; ppNewImgData=null; save().then(()=>{ ppRenderStats(); ppRender(); fillGrid(); }); } let _saveTimer=null; function ppSizeCell(p, i){ const ringOptions = [4,5,6,7,8,9,10,11,12,13,14,15]; const cardOptions = [{v:'small',l:'Small'},{v:'medium',l:'Medium'},{v:'large',l:'Large'},{v:'wide',l:'Wide'}]; if((p.family||'other')==='anillos'){ return ''; } return ''; } function ppUpdateSizeCell(familySelect, i){ const td = familySelect.closest('tr').querySelectorAll('td'); // Size cell is the one after Family (second to last before delete) const sizeTd = familySelect.closest('td').nextElementSibling; if(!sizeTd) return; products[i].family = familySelect.value; sizeTd.innerHTML = ppSizeCell(products[i], i); } function ppUpdatePrice(i, code){ if(!products[i]) return; products[i].priceCode=code; products[i].price=code?_getPriceFromCode(code):''; save(); } function ppUpdate(i,field,val){ products[i][field]=val; clearTimeout(_saveTimer); _saveTimer=setTimeout(()=>save(),400); ppRenderStats(); fillGrid(); // Refresh shop panel if open so AGOTADO appears/disappears immediately const shop=document.getElementById('shopPanel'); if(shop && shop.classList.contains('open')){ spRenderSidebar(); spRender(); } } function ppDelete(i){ if(!confirm('Delete this product permanently?')) return; products.splice(i,1); save().then(()=>{ ppRenderStats(); ppRender(); fillGrid(); }); } function ppAddSale(i){ products[i].sold=(parseInt(products[i].sold)||0)+1; const qty=parseInt(products[i].qty)||0; if(qty>0) products[i].qty=String(qty-1); save(); ppRenderStats(); ppRender(); fillGrid(); } function ppRemoveSale(i){ const s=parseInt(products[i].sold)||0; if(s>0){ products[i].sold=s-1; save(); ppRenderStats(); ppRender(); fillGrid(); } } function ppChangePhoto(i){ ppEditImgIdx=i; document.getElementById('ppImgInput').click(); } /* ═══════════════════════════════════════ SHOP PANEL ═══════════════════════════════════════ */ const FAMILIES = [ { id:'all', label:'Todo', icon:'💎' }, { id:'anillos', label:'Anillos', icon:'💍' }, { id:'aretes', label:'Aretes', icon:'✨' }, { id:'collares', label:'Collares', icon:'📿' }, { id:'brasaletes', label:'Brasaletes', icon:'🪙' }, { id:'pulseras', label:'Pulseras', icon:'🌟' }, { id:'broches', label:'Broches', icon:'🌸' }, { id:'sets', label:'Sets', icon:'🎁' }, { id:'other', label:'Otros', icon:'🔮' }, ]; let spActive = 'all'; /* ─── METAL FILTER ─────────────────────────────────────────── */ let spMetalFilter = null; // null = show all function openShopByMetal(metals){ spMetalFilter = metals; spActive = 'all'; document.getElementById('shopPanel').classList.add('open'); if(window._updateNav) window._updateNav(); setTimeout(()=>{ spRenderSidebar(); spRender(); }, 50); } function openShopByMetalAndFamily(metals, family){ spMetalFilter = metals; spActive = family; document.getElementById('shopPanel').classList.add('open'); if(window._updateNav) window._updateNav(); setTimeout(()=>{ spRenderSidebar(); spRender(); }, 50); } function openShop(){ spMetalFilter = null; spActive = 'all'; document.getElementById('shopPanel').classList.add('open'); if(window._updateNav) window._updateNav(); // Use setTimeout to ensure isAdmin and CSS are fully applied before render setTimeout(()=>{ spRenderSidebar(); spRender(); if(isAdmin) spLoadLayout(); }, 50); if(window._updateNav) window._updateNav(); } function closeShop(){ document.getElementById('shopPanel').classList.remove('open'); if(window._updateNav) window._updateNav(); } function spRenderSidebar(){ const sidebar = document.getElementById('spSidebar'); const title = sidebar.querySelector('.sp-sidebar-title'); sidebar.innerHTML = ''; sidebar.appendChild(title); FAMILIES.forEach(fam=>{ const count = fam.id==='all' ? products.length : products.filter(p=>(p.family||'other')===fam.id).length; // hide empty families (except "all") unless admin if(!isAdmin && fam.id!=='all' && count===0) return; const btn = document.createElement('button'); btn.className = 'sp-family-btn' + (spActive===fam.id?' active':''); btn.innerHTML = `${fam.icon}${fam.label}${count}`; btn.onclick = ()=>{ spActive=fam.id; spMetalFilter=null; spRenderSidebar(); spRender(); }; sidebar.appendChild(btn); }); } function spRender(){ const famLabel=document.getElementById('spAdminFamLabel'); if(famLabel) famLabel.textContent=(FAMILIES.find(f=>f.id===spActive)||{label:'All'}).label; const q = (document.getElementById('spSearch').value||'').toLowerCase().trim(); const fam = FAMILIES.find(f=>f.id===spActive)||FAMILIES[0]; let headingText = fam.label; if(spMetalFilter){ if(spMetalFilter.includes('Plata')) headingText += ' de Plata'; else if(spMetalFilter.some(m=>m.startsWith('Oro')||m.startsWith('Chapa'))) headingText += ' de Oro'; } document.getElementById('spFamilyHeading').textContent = headingText; const panelTitle = document.getElementById('spPanelTitle'); if(panelTitle) panelTitle.textContent = headingText; let filtered = spActive==='all' ? [...products] : products.filter(p=>(p.family||'other')===spActive); if(spMetalFilter) filtered = filtered.filter(p=>spMetalFilter.includes(p.metal||'')); if(q) filtered = filtered.filter(p=>(p.name+(p.desc||'')+(p.num||'')).toLowerCase().includes(q)); document.getElementById('spCountLabel').textContent = filtered.length + ' piece' + (filtered.length!==1?'s':''); const grid = document.getElementById('spGrid'); grid.innerHTML = ''; if(!filtered.length){ grid.innerHTML='

No pieces in this collection yet.

'; return; } const posKey = 'avz_sp_pos_'+(spActive||'all'); let positions = {}; try{ positions=JSON.parse(localStorage.getItem(posKey)||'{}'); }catch(e){} // Auto-layout defaults: place cards in a grid if no saved position const DEFAULT_W = 220, GAP = 20, COLS = Math.max(1, Math.floor((grid.offsetWidth||900) / (DEFAULT_W+GAP))); let maxBottom = 0; filtered.forEach((p,fi)=>{ const i = products.indexOf(p); const card = document.createElement('div'); card.className = 'sp-card'; card.dataset.pidx = i; // Apply saved size const pos = positions[i]; const cardW = (pos && pos.w) ? pos.w : DEFAULT_W; card.style.width = cardW+'px'; // Position absolutely if saved position exists (both admin and public) const hasSavedPos = pos && pos.x != null && pos.y != null; if(isAdmin || hasSavedPos){ card.style.position='absolute'; const col = fi % COLS; const row = Math.floor(fi / COLS); const defaultX = col * (DEFAULT_W + GAP) + GAP; const defaultY = row * (340 + GAP) + GAP; const x = hasSavedPos ? pos.x : defaultX; const y = hasSavedPos ? pos.y : defaultY; card.style.left = x+'px'; card.style.top = y+'px'; const estBottom = y + (pos && pos.h ? pos.h : 380); if(estBottom > maxBottom) maxBottom = estBottom; } const outOfStock = (parseInt(p.qty)||0) === 0; const famOpts = FAMILIES.filter(f=>f.id!=='all').map(f=>'').join(''); const _famLabel = (FAMILIES.find(f=>f.id===(p.family||'other'))||{label:'Colección'}).label; const agotadoOverlay = outOfStock ? '
Agotado
' : ''; const discountBadge = (p.discount&&parseInt(p.discount)>0) ? '
-'+parseInt(p.discount)+'%
' : ''; const _isVid = p.img && (p.img.startsWith('data:video') || p.imgType==='video'); const imgHtml = p.img ? '
'+(_isVid ? '' : ''+esc(p.name)+'') +(isMainAdmin()?'
'+_famLabel+'
':'') +agotadoOverlay+discountBadge+'
' : '
'+(FAMILIES.find(f=>f.id===(p.family||'other'))||{icon:'💎'}).icon+agotadoOverlay+discountBadge+'
'; card.innerHTML = (isAdmin?'
✠ drag
':'')+ imgHtml+ '
'+ '
'+esc(p.num)+'
'+ (p.metal?'
'+esc(p.metal)+'
':'')+ '
'+esc(p.name)+'
'+ (p.desc||isAdmin?'
'+esc(p.desc||'')+'
':'')+ '
'+ '
'+_renderPrice(p)+'
'+ (outOfStock?'
Agotado
':'')+ (isAdmin?'
'+esc(String(p.qty||'0'))+'
':'')+ '
'+ (isMainAdmin()? '
'+ ''+ ''+ ''+ (p.img?'':'')+ ''+ '
': (isAdmin? '
'+ ''+ ''+ '
': '' ))+ '
'+ (isAdmin?'
'+ ''+ '
'+ '
'+ 'Tamaño:'+ [['S',160],['M',220],['L',280],['XL',340]].map(([lbl,w])=>{ const curW=parseInt(card.style.width)||220; const active=Math.abs(curW-w)<30; ''; }).join('')+ '
':''); if(isAdmin){ const handle=card.querySelector('.sp-card-drag-handle'); if(handle) initSpCardDrag(card,i,posKey,handle); // Wire resize corner const resizeHandle=card.querySelector('.sp-card-resize'); if(resizeHandle) initSpCardResize(card,i,posKey,resizeHandle); } card.style.cursor='pointer'; card.addEventListener('click',function(e){ if(e.target.classList.contains('sp-card-atc')) return; if(e.target.closest('.sp-admin-family')) return; if(e.target.closest('.sp-card-drag-handle')) return; if(e.target.getAttribute('contenteditable')==='true') return; const idx = parseInt(card.dataset.pidx); openProductDetail(idx); }); grid.appendChild(card); // Wrap content in inner div for proportional scale (admin AND public with saved layout) if(isAdmin || (positions && Object.values(positions).some(p=>p&&p.x!=null))){ const inner = document.createElement('div'); inner.className = 'sp-card-inner'; const children = Array.from(card.childNodes); children.forEach(c => inner.appendChild(c)); card.appendChild(inner); } }); // Switch grid to free-canvas if any card has saved position const hasSavedLayout = Object.values(positions).some(p=>p && p.x!=null); if(isAdmin || hasSavedLayout){ grid.style.display='block'; grid.style.position='relative'; // Two-pass scale: measure natural height, then apply uniformly const allCards = Array.from(grid.querySelectorAll('.sp-card')); let maxNatH = 0; allCards.forEach(card=>{ const inner=card.querySelector('.sp-card-inner'); if(!inner) return; inner.style.transform='none'; inner.style.width=SP_BASE_W+'px'; inner.style.height='auto'; inner.style.overflow='visible'; const h=inner.scrollHeight||inner.offsetHeight; if(h>maxNatH) maxNatH=h; }); SP_BASE_H = maxNatH || 480; allCards.forEach(card=>{ const pidx=parseInt(card.dataset.pidx); const savedW=(positions[pidx]&&positions[pidx].w)||DEFAULT_W; _spScaleCard(card, savedW); }); let maxBottom2=0; allCards.forEach(c=>{const b=(parseInt(c.style.top)||0)+c.offsetHeight;if(b>maxBottom2)maxBottom2=b;}); grid.style.minHeight=(maxBottom2+100)+'px'; } else { grid.style.display=''; grid.style.position=''; grid.style.minHeight=''; } } function _renderPrice(p){ const disc = parseInt(p.discount)||0; if(!disc || !p.price) return esc(p.price||''); const orig = parseFloat((p.price||'').replace(/[^0-9.]/g,''))||0; if(!orig) return esc(p.price||''); const discounted = (orig*(1-disc/100)).toFixed(2).replace(/\.00$/,''); return ''+esc(p.price)+'$'+discounted+''; } function _discountedPrice(p){ const disc = parseInt(p.discount)||0; if(!disc||!p.price) return p.price||''; const orig = parseFloat((p.price||'').replace(/[^0-9.]/g,''))||0; if(!orig) return p.price||''; const d = (orig*(1-disc/100)).toFixed(2).replace(/\.00$/,''); return '$'+d; } function spAddToCart(i, btn){ addToCart(i); btn.textContent='Added ✓'; btn.style.background='var(--gold)'; btn.style.color='var(--charcoal)'; setTimeout(()=>{ btn.textContent='Agregar al Carrito'; btn.style.background=''; btn.style.color=''; },1400); } function spSetFamily(i, fam){ products[i].family = fam; save(); spRenderSidebar(); spRender(); fillGrid(); // refresh main page grid too } /* ─── HELPER: open shop to a specific family ──────────────────── */ function spUpdateField(i, field, val){ if(!products[i]) return; products[i][field] = val.trim(); save(); spRenderSidebar(); if(field==='qty') { spRender(); fillGrid(); } } function spDeleteCard(i){ if(!confirm('Delete "'+products[i].name+'"?')) return; products.splice(i,1); save(); spRenderSidebar(); spRender(); } function openSearch(){ // Open shop panel and focus the search input openShop(); setTimeout(()=>{ const s=document.getElementById('spSearch'); if(s){ s.focus(); s.select(); } },200); } function openShopByFamily(fam){ openShop(); setTimeout(()=>{ spActive=fam; spRenderSidebar(); spRender(); },50); } function navToggleMobile(){ let drawer=document.getElementById('navDrawer'); let overlay=document.getElementById('navDrawerOverlay'); // Build drawer on first use if(!drawer){ overlay=document.createElement('div'); overlay.id='navDrawerOverlay'; overlay.onclick=navCloseDrawer; overlay.style.cssText='display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:5000'; document.body.appendChild(overlay); drawer=document.createElement('div'); drawer.id='navDrawer'; drawer.style.cssText='position:fixed;top:0;right:0;width:360px;max-width:90vw;bottom:0;background:#fff;z-index:5001;transform:translateX(100%);transition:transform .32s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column;overflow-y:auto;padding:40px 36px'; drawer.innerHTML=`
Avonza Jewelry
¿Quiénes somos?

Exclusiva línea de joyería en plata .925 que ofrece diseños de moda y tendencia, fabricados con materiales de excelente calidad, resaltando detalles para hacer cada pieza especial.

Contáctanos
📱 5564395204 ✉️ hola@avonza.mx
📍 Ciudad de México, México
Síguenos
f 📷
`; document.body.appendChild(drawer); } const open=drawer.style.transform==='translateX(0px)'||drawer.style.transform==='translateX(0)'; if(open){ navCloseDrawer(); } else { requestAnimationFrame(()=>{ drawer.style.transform='translateX(0)'; }); overlay.style.display='block'; } } function navCloseDrawer(){ const drawer=document.getElementById('navDrawer'); const overlay=document.getElementById('navDrawerOverlay'); if(drawer) drawer.style.transform='translateX(100%)'; if(overlay) overlay.style.display='none'; } function initNavDrag(){ const bar=document.getElementById('navMenuBar'); if(!bar) return; const items=Array.from(bar.querySelectorAll('.nav-menu-item')); let dragSrc=null; items.forEach(item=>{ item.setAttribute('draggable','true'); item.addEventListener('dragstart',function(e){ dragSrc=this; this.classList.add('nav-dragging'); e.dataTransfer.effectAllowed='move'; }); item.addEventListener('dragend',function(){ this.classList.remove('nav-dragging'); items.forEach(i=>i.classList.remove('nav-drag-over')); }); item.addEventListener('dragover',function(e){ e.preventDefault(); e.dataTransfer.dropEffect='move'; items.forEach(i=>i.classList.remove('nav-drag-over')); this.classList.add('nav-drag-over'); }); item.addEventListener('drop',function(e){ e.preventDefault(); if(dragSrc===this) return; this.classList.remove('nav-drag-over'); const all=Array.from(bar.querySelectorAll('.nav-menu-item')); const si=all.indexOf(dragSrc),ti=all.indexOf(this); if(si{ spActive=fam; spRenderSidebar(); spRender(); }, 50); } /* ─── SECTION DRAG-TO-RESIZE ─────────────────────────────────── */ function initSectionResize(sectionEl, handle, sec){ let startY = 0; let startH = 0; let startW = 0; let natH = 0; function getInner(){ return sectionEl.querySelector(':scope > div:not(.section-ctrl):not(.sec-resize-handle)'); } function applyScale(newH){ const inner = getInner(); if(!inner) return; const scale = newH / natH; if(sec.type === 'canvas'){ // For canvas: scale each element's position and size const els = sec.data.els || []; inner.style.height = newH + 'px'; inner.querySelectorAll('.canvas-el').forEach((node, i)=>{ if(!els[i]) return; const el = els[i]; node.style.top = (el.y * scale) + 'px'; node.style.left = (el.x * scale) + 'px'; node.style.width = (el.w * scale) + 'px'; node.style.height = (el.h * scale) + 'px'; // scale font size too const ta = node.querySelector('textarea'); if(ta && el.fontSize) ta.style.fontSize = (el.fontSize * scale) + 'px'; }); } else { // For other sections: scale uniformly with CSS transform inner.style.transformOrigin = 'top center'; inner.style.transform = 'scale(1, '+scale+')'; // Also scale images specifically so they don't distort inner.querySelectorAll('img').forEach(img=>{ img.style.transformOrigin = 'top center'; img.style.transform = 'scaleX('+(1/1)+')'; // keep x at 1 }); } sectionEl.style.height = newH + 'px'; sectionEl.style.overflow = 'hidden'; } // Apply saved size on load if(sec.data.size && sec.data.size !== 'normal'){ const saved = parseFloat(sec.data.size); if(!isNaN(saved)){ requestAnimationFrame(()=>{ const inner = getInner(); if(inner){ natH = inner.offsetHeight || saved; applyScale(saved); } }); } } function onMouseMove(e){ const clientY = e.clientY !== undefined ? e.clientY : (e.touches&&e.touches[0]?e.touches[0].clientY:startY); const dy = clientY - startY; const newH = Math.max(80, startH + dy); applyScale(newH); } function onMouseUp(){ handle.classList.remove('dragging'); document.body.style.userSelect = ''; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); document.removeEventListener('touchmove', onMouseMove); document.removeEventListener('touchend', onMouseUp); sec.data.size = sectionEl.style.height; // For canvas, also save the scaled positions permanently if(sec.type === 'canvas'){ const inner = getInner(); if(inner && natH > 0){ const finalH = parseFloat(sectionEl.style.height)||natH; const scale = finalH / natH; (sec.data.els||[]).forEach(el=>{ el.x = Math.round(el.x * scale); el.y = Math.round(el.y * scale); el.w = Math.round(el.w * scale); el.h = Math.round(el.h * scale); if(el.fontSize) el.fontSize = Math.round(el.fontSize * scale); }); natH = finalH; // reset baseline } } save(); } handle.addEventListener('mousedown', function(e){ if(!isAdmin) return; e.preventDefault(); e.stopPropagation(); startY = e.clientY; startH = sectionEl.offsetHeight; startW = sectionEl.offsetWidth; const inner = getInner(); natH = inner ? inner.scrollHeight || startH : startH; handle.classList.add('dragging'); document.body.style.userSelect = 'none'; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); handle.addEventListener('touchstart', function(e){ if(!isAdmin) return; e.stopPropagation(); startY = e.touches[0].clientY; startH = sectionEl.offsetHeight; const inner = getInner(); natH = inner ? inner.scrollHeight || startH : startH; handle.classList.add('dragging'); document.addEventListener('touchmove', onMouseMove, {passive:true}); document.addEventListener('touchend', onMouseUp); }, {passive:true}); } /* ─── IMAGE SECTION HELPERS ──────────────────────────────────── */ function secImageUpload(secId){ const inp=document.createElement('input'); inp.type='file'; inp.accept='image/*'; inp.onchange=function(e){ const file=e.target.files[0]; if(!file) return; compressImage(file).then(dataUrl=>{ const sec=sections.find(s=>s.id===secId); if(sec){ sec.data.img=dataUrl; save(); renderSections(); fillGrid(); } }); }; inp.click(); } function buildImageGrid(el, sec){ el.innerHTML=''; const cols=sec.data.cols||3; const imgs=sec.data.imgs||[]; imgs.forEach((img,i)=>{ // When not admin, skip empty cells entirely if(!img && !isMainAdmin()) return; const cell=document.createElement('div'); cell.className='sec-imagegrid-cell'; if(img){ const isVideo = typeof img === 'string' && (img.startsWith('data:video') || img.endsWith('.mp4')); if(isVideo){ cell.innerHTML=''; } else { cell.innerHTML='Gallery image '+(i+1)+''; } } else if(isMainAdmin()){ cell.innerHTML='
Photo '+(i+1)+'
'; } const links = sec.data.links||[]; const cellLink = links[i]||''; const FAMILIES_MAP = { 'anillos':'Anillos','aretes':'Aretes','collares':'Collares', 'brasaletes':'Brasaletes','pulseras':'Pulseras','broches':'Broches', 'sets':'Sets','other':'Otros','all':'Todo' }; // Cell click: navigate if linked (public), upload if empty (admin) if(cellLink && !isMainAdmin()){ cell.style.cursor='pointer'; if(cellLink.startsWith('http')||cellLink.startsWith('/')){ cell.onclick=()=>window.open(cellLink,'_blank'); } else { cell.onclick=()=>openShopByFamily(cellLink); } const lbl=document.createElement('div'); lbl.className='igrid-link-label'; lbl.textContent=cellLink.startsWith('http')?'Ver ↗':'Shop '+(FAMILIES_MAP[cellLink]||cellLink); cell.appendChild(lbl); } else if(cellLink && isMainAdmin()){ // In admin mode show link label but don't navigate on click const lbl=document.createElement('div'); lbl.className='igrid-link-label'; lbl.textContent=cellLink.startsWith('http')?'🔗 URL':'🔗 '+(FAMILIES_MAP[cellLink]||cellLink); cell.appendChild(lbl); } else if(!img && isMainAdmin()){ cell.style.cursor='pointer'; cell.onclick=()=>secGridMediaUpload(sec.id, i); } if(isMainAdmin()){ // Upload buttons — photo and video const upBtn=document.createElement('button'); upBtn.style.cssText='position:absolute;top:6px;left:6px;background:rgba(10,186,181,.9);color:#fff;border:none;font-family:Lato,sans-serif;font-size:9px;letter-spacing:.12em;text-transform:uppercase;padding:4px 8px;border-radius:2px;cursor:pointer;z-index:6'; upBtn.textContent='📷'; upBtn.title='Subir foto'; upBtn.onclick=e=>{ e.stopPropagation(); secGridImgOnlyUpload(sec.id, i); }; cell.appendChild(upBtn); const vidBtn=document.createElement('button'); vidBtn.style.cssText='position:absolute;top:6px;left:38px;background:rgba(142,68,173,.9);color:#fff;border:none;font-family:Lato,sans-serif;font-size:9px;letter-spacing:.12em;text-transform:uppercase;padding:4px 8px;border-radius:2px;cursor:pointer;z-index:6'; vidBtn.textContent='🎬'; vidBtn.title='Subir video MP4'; vidBtn.onclick=e=>{ e.stopPropagation(); secGridVideoUpload(sec.id, i, el, sec); }; cell.appendChild(vidBtn); // Link selector — collections + custom URL const linkWrap=document.createElement('div'); linkWrap.style.cssText='position:absolute;bottom:6px;left:6px;right:6px;z-index:6;display:flex;flex-direction:column;gap:3px'; const sel=document.createElement('select'); sel.className='igrid-link-sel'; sel.title='Link to collection'; sel.style.cssText='font-size:9px;padding:3px 5px;border:none;border-radius:2px;background:rgba(0,0,0,.6);color:#fff;cursor:pointer;width:100%'; [['','Sin link'],['brasaletes','Brasaletes'],['collares','Collares'], ['anillos','Anillos'],['aretes','Aretes'],['pulseras','Pulseras'], ['broches','Broches'],['sets','Sets'],['all','Todo'],['__url__','URL personalizada...'] ].forEach(([val,lbl])=>{ const o=document.createElement('option'); o.value=val; o.textContent=lbl; if(cellLink===val || (val==='__url__' && cellLink && !['brasaletes','collares','anillos','aretes','pulseras','broches','sets','all',''].includes(cellLink))) o.selected=true; sel.appendChild(o); }); const urlInput=document.createElement('input'); urlInput.type='text'; urlInput.placeholder='https://...'; urlInput.style.cssText='font-size:9px;padding:3px 5px;border:none;border-radius:2px;background:rgba(0,0,0,.6);color:#fff;width:100%;box-sizing:border-box;display:none'; const isCustomUrl = cellLink && !['brasaletes','collares','anillos','aretes','pulseras','broches','sets','all',''].includes(cellLink); if(isCustomUrl){ urlInput.style.display='block'; urlInput.value=cellLink; sel.value='__url__'; } sel.onchange=function(e){ e.stopPropagation(); if(this.value==='__url__'){ urlInput.style.display='block'; urlInput.focus(); } else { urlInput.style.display='none'; sec.data.links=sec.data.links||[]; sec.data.links[i]=this.value; save(); buildImageGrid(el,sec); } }; sel.onclick=e=>e.stopPropagation(); urlInput.onchange=urlInput.onblur=function(e){ e.stopPropagation(); const val=this.value.trim(); sec.data.links=sec.data.links||[]; sec.data.links[i]=val; save(); buildImageGrid(el,sec); }; urlInput.onclick=e=>e.stopPropagation(); linkWrap.appendChild(sel); linkWrap.appendChild(urlInput); cell.appendChild(linkWrap); const del=document.createElement('button'); del.className='sec-imagegrid-del'; del.textContent='×'; del.onclick=function(e){ e.stopPropagation(); sec.data.imgs.splice(i,1); if(sec.data.links) sec.data.links.splice(i,1); save(); buildImageGrid(el,sec); }; cell.appendChild(del); } el.appendChild(cell); }); // add cell (admin only) if(isMainAdmin()){ const add=document.createElement('div'); add.className='sec-imagegrid-add'; add.innerHTML='+'; add.onclick=()=>{ sec.data.imgs.push(null); save(); buildImageGrid(el,sec); secGridMediaUpload(sec.id, sec.data.imgs.length-1, el, sec); }; el.appendChild(add); // col controls const ctrl=document.createElement('div'); ctrl.className='igrid-col-ctrl'; [2,3,4].forEach(n=>{ const btn=document.createElement('button'); btn.className='igrid-col-btn'+(n===cols?' active':''); btn.textContent=n+' cols'; btn.onclick=function(e){ e.stopPropagation(); sec.data.cols=n; el.setAttribute('data-cols',n); el.querySelectorAll('.igrid-col-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); save(); }; ctrl.appendChild(btn); }); el.appendChild(ctrl); } } function secGridMediaUpload(secId, idx, el, sec){ const inp=document.createElement('input'); inp.type='file'; inp.accept='image/*,video/mp4,video/*'; inp.onchange=function(e){ const file=e.target.files[0]; if(!file) return; const isVideo=file.type.startsWith('video/'); if(isVideo){ const reader=new FileReader(); reader.onload=function(ev){ const s=sec||sections.find(x=>x.id===secId); if(!s) return; s.data.imgs[idx]=ev.target.result; save(); const container=el||document.querySelector('.sec-imagegrid[data-secid="'+secId+'"]'); if(container) buildImageGrid(container, s); }; reader.readAsDataURL(file); } else { compressImage(file).then(dataUrl=>{ const s=sec||sections.find(x=>x.id===secId); if(!s) return; s.data.imgs[idx]=dataUrl; save(); const container=el||document.querySelector('.sec-imagegrid[data-secid="'+secId+'"]'); if(container) buildImageGrid(container, s); }); } }; inp.click(); } function secGridImgOnlyUpload(secId, idx){ const inp=document.createElement('input'); inp.type='file'; inp.accept='image/*'; inp.onchange=function(e){ const file=e.target.files[0]; if(!file) return; compressImage(file).then(dataUrl=>{ const s=sections.find(x=>x.id===secId); if(!s) return; s.data.imgs[idx]=dataUrl; save(); const container=document.querySelector('.sec-imagegrid[data-secid="'+secId+'"]'); if(container) buildImageGrid(container, s); }); }; inp.click(); } function secGridVideoUpload(secId, idx, el, sec){ const inp=document.createElement('input'); inp.type='file'; inp.accept='video/mp4,video/*'; inp.onchange=function(e){ const file=e.target.files[0]; if(!file) return; const reader=new FileReader(); reader.onload=function(ev){ const s=sec||sections.find(x=>x.id===secId); if(!s) return; s.data.imgs[idx]=ev.target.result; save(); const container=el||document.querySelector('.sec-imagegrid[data-secid="'+secId+'"]'); if(container) buildImageGrid(container, s); }; reader.readAsDataURL(file); }; inp.click(); } function secGridImgUpload(secId, idx, el, sec){ const inp=document.createElement('input'); inp.type='file'; inp.accept='image/*'; inp.onchange=function(e){ const file=e.target.files[0]; if(!file) return; compressImage(file).then(dataUrl=>{ const s=sec||sections.find(x=>x.id===secId); if(!s) return; s.data.imgs[idx]=dataUrl; save(); const container=el||document.querySelector('.sec-imagegrid[data-secid="'+secId+'"]'); if(container) buildImageGrid(container, s); }); }; inp.click(); } /* ═══ EMBED DATA INTO HTML FOR SHARING ═══ */ function updateEmbeddedTag(){ try{ const tag = document.getElementById('avonza-data'); if(!tag) return; ordenesLoad(); const layoutSettings = (() => { try{ return JSON.parse(localStorage.getItem('avz_layout_settings')||'null'); }catch(e){ return null; } })(); tag.textContent = JSON.stringify({ products, sections, navItems, orders, adminUsers, priceCodes, layoutSettings }); // Always keep admin UI hidden in the saved snapshot ['adminBadge','navAddBtn','productsNavBtn','ordenesNavBtn','shareNavBtn','usuariosNavBtn','ventasNavBtn','adminNavBtns','btnPanelAdmin'].forEach(id=>{ const el=document.getElementById(id); if(el) el.style.display='none'; }); if(!isAdmin) document.body.classList.remove('admin-mode'); }catch(e){} } function saveHTMLFile(){ // Get the current HTML with embedded data updated updateEmbeddedTag(); const html = document.documentElement.outerHTML; const blob = new Blob(['\n'+html], {type:'text/html'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'avonza-jewelry.html'; a.click(); URL.revokeObjectURL(a.href); } /* ═══ CANVAS SECTION ═══════════════════════════════════════════ */ let _canvasDragEl=null, _canvasDragOX=0, _canvasDragOY=0; let _canvasResizeEl=null, _canvasResizeOX=0, _canvasResizeOY=0, _canvasResizeW=0, _canvasResizeH=0; let _selectedCanvasEl=null; function buildSectionInner(container, sec){ // Render just the inner content of a section type into a container div if(sec.type==='image'&&sec.data.img){ container.innerHTML=''; } else if(sec.type==='video'&&sec.data.video){ container.innerHTML=''; } else if(sec.type==='banner'){ container.innerHTML='

'+esc(sec.data.text||'')+'

'; } else if(sec.type==='text'){ container.innerHTML='

'+esc(sec.data.heading||'')+'

'+esc(sec.data.body||'')+'

'; } else if(sec.type==='imagegrid'){ const cols=sec.data.cols||3; const imgs=sec.data.imgs||[]; container.style.display='grid'; container.style.gridTemplateColumns='repeat('+cols+',1fr)'; container.style.gap='2px'; imgs.filter(i=>i).forEach(img=>{ const d=document.createElement('div'); d.style.cssText='aspect-ratio:1;overflow:hidden;background:#f0f0f0'; d.innerHTML=''; container.appendChild(d); }); } else { container.innerHTML='
'+sec.type+'
'; } } function buildCanvas(container, sec){ container.innerHTML=''; sec.data.els = sec.data.els||[]; sec.data.bg = sec.data.bg||'#ffffff'; // Admin toolbar if(isMainAdmin()){ const tb=document.createElement('div'); tb.className='canvas-toolbar'; tb.innerHTML=` BG: Click para seleccionar · Arrastrar para mover · Esquina para redimensionar`; container.appendChild(tb); } // Render elements sec.data.els.forEach((el,i)=>{ const node=createCanvasEl(el, i, sec, container); container.appendChild(node); }); // Click on canvas background to deselect container.addEventListener('mousedown',function(e){ if(e.target===container || e.target.classList.contains('canvas-toolbar')){ selectCanvasEl(null); } }); } function createCanvasEl(el, idx, sec, container){ const node=document.createElement('div'); node.className='canvas-el'; node.dataset.idx=idx; node.style.cssText=`left:${el.x||20}px;top:${el.y||20}px;width:${el.w||200}px;height:${el.h||150}px;z-index:${el.z||1}`; if(el.type==='image'){ const img=document.createElement('img'); img.className='canvas-el-img'; img.src=el.src||''; img.draggable=false; if(isMainAdmin()) img.ondblclick=()=>canvasReplaceImage(sec, idx, container); node.appendChild(img); } else if(el.type==='video'){ const vid=document.createElement('video'); vid.src=el.src||''; vid.style.cssText='width:100%;height:100%;object-fit:cover;display:block;pointer-events:none'; vid.autoplay=true; vid.muted=true; vid.loop=true; vid.playsInline=true; node.appendChild(vid); if(isMainAdmin()){ const repl=document.createElement('button'); repl.textContent='🎬 Cambiar'; repl.style.cssText='position:absolute;bottom:6px;left:6px;background:rgba(142,68,173,.9);color:#fff;border:none;font-size:9px;letter-spacing:.1em;text-transform:uppercase;padding:4px 8px;border-radius:2px;cursor:pointer;z-index:5'; repl.onclick=e=>{ e.stopPropagation(); canvasReplaceVideo(sec,idx,container); }; node.appendChild(repl); } } else if(el.type==='section'){ // Render a nested section inside the canvas element const nested=(sec.data.nestedSections||[]).find(s=>s.id===el.secId); node.style.overflow='hidden'; if(nested){ const inner=document.createElement('div'); inner.style.cssText='width:100%;height:100%;pointer-events:none'; buildSectionInner(inner, nested); node.appendChild(inner); if(isMainAdmin()){ const lbl=document.createElement('div'); lbl.style.cssText='position:absolute;top:4px;left:4px;background:rgba(39,174,96,.9);color:#fff;font-size:8px;letter-spacing:.12em;text-transform:uppercase;padding:2px 6px;border-radius:2px;pointer-events:none'; lbl.textContent='SECTION: '+nested.type; node.appendChild(lbl); } } else { node.style.background='#f0f0f0'; node.innerHTML='
Section
'; } } else if(el.type==='text'){ // drag bar at top if(isMainAdmin()){ const bar=document.createElement('div'); bar.className='canvas-el-drag-bar'; bar.innerHTML='⠿ Text'; // drag via bar bar.addEventListener('mousedown',e=>{ e.stopPropagation(); e.preventDefault(); selectCanvasEl(node); startCanvasDrag(e,node,sec,idx,container,bar); }); node.appendChild(bar); // text format toolbar (shows when selected) const ttb=document.createElement('div'); ttb.className='canvas-text-toolbar'; const fs=el.fontSize||24, fc=el.color||'#1A1A1A', ff=el.font||'Playfair Display'; const bold=el.bold?'active':'', italic=el.italic?'active':''; ttb.innerHTML=`
`; node.appendChild(ttb); } const ta=document.createElement('textarea'); ta.className='canvas-el-text'; ta.value=el.text||'Type here…'; const fs=el.fontSize||24, fc=el.color||'#1A1A1A', ff=el.font||'Playfair Display'; const fw=el.bold?'bold':'normal', fst=el.italic?'italic':'normal'; ta.style.cssText=`font-size:${fs}px;color:${fc};font-family:'${ff}',serif;font-weight:${fw};font-style:${fst};background:transparent;border:none;resize:none;width:100%;height:100%;outline:none;padding:6px 8px;box-sizing:border-box;line-height:1.4`; if(isMainAdmin()){ ta.addEventListener('input',()=>{ sec.data.els[idx].text=ta.value; save(); }); } else { ta.readOnly=true; } node.appendChild(ta); } if(isMainAdmin()){ // Delete btn const del=document.createElement('button'); del.className='canvas-el-del'; del.textContent='×'; del.onclick=e=>{ e.stopPropagation(); canvasDeleteEl(sec,idx,container); }; node.appendChild(del); // Resize handle const rh=document.createElement('div'); rh.className='canvas-resize-handle'; rh.addEventListener('mousedown',e=>{ e.stopPropagation(); e.preventDefault(); startCanvasResize(e,node,sec,idx); }); node.appendChild(rh); // Drag to move (for image elements; text uses drag bar) node.addEventListener('mousedown',e=>{ if(e.target.classList.contains('canvas-resize-handle')) return; if(e.target.classList.contains('canvas-el-del')) return; if(e.target.classList.contains('canvas-el-drag-bar')) return; if(e.target.tagName==='TEXTAREA') return; if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT'||e.target.tagName==='BUTTON') return; e.preventDefault(); selectCanvasEl(node); startCanvasDrag(e,node,sec,idx,container); }); node.addEventListener('click',e=>{ e.stopPropagation(); selectCanvasEl(node); }); } return node; } function selectCanvasEl(node){ if(_selectedCanvasEl) _selectedCanvasEl.classList.remove('selected'); _selectedCanvasEl=node; if(node) node.classList.add('selected'); } function startCanvasDrag(e, node, sec, idx, container){ _canvasDragEl=node; _canvasDragOX=e.clientX - node.offsetLeft; _canvasDragOY=e.clientY - node.offsetTop; const containerRect=container.getBoundingClientRect(); document.body.style.userSelect='none'; const onMove=ev=>{ const x=Math.max(0, ev.clientX - _canvasDragOX); const y=Math.max(0, ev.clientY - _canvasDragOY); node.style.left=x+'px'; node.style.top=y+'px'; sec.data.els[idx].x=x; sec.data.els[idx].y=y; }; const onUp=()=>{ document.body.style.userSelect=''; document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp); save(); }; document.addEventListener('mousemove',onMove); document.addEventListener('mouseup',onUp); } function startCanvasResize(e, node, sec, idx){ _canvasResizeEl=node; _canvasResizeOX=e.clientX; _canvasResizeOY=e.clientY; _canvasResizeW=node.offsetWidth; _canvasResizeH=node.offsetHeight; const onMove=ev=>{ const w=Math.max(60,_canvasResizeW+(ev.clientX-_canvasResizeOX)); const h=Math.max(40,_canvasResizeH+(ev.clientY-_canvasResizeOY)); node.style.width=w+'px'; node.style.height=h+'px'; sec.data.els[idx].w=w; sec.data.els[idx].h=h; }; const onUp=()=>{ document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp); save(); }; document.addEventListener('mousemove',onMove); document.addEventListener('mouseup',onUp); } function canvasBgChange(secId, color){ const sec=sections.find(s=>s.id===secId); if(!sec) return; sec.data.bg=color; const container=document.querySelector('.sec-canvas[data-secid="'+secId+'"]'); if(container) container.style.background=color; save(); } function canvasAddSection(secId){ // Show a picker overlay const types=[ ['image','🖼 Imagen / Foto'], ['video','🎬 Video MP4'], ['imagegrid','📐 Image Gallery'], ['text','✍ Bloque de Texto'], ['banner','💬 Quote / Banner'], ['divider','➖ Divider / Label'], ['grid','📦 Product Grid'] ]; const overlay=document.createElement('div'); overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:99999;display:flex;align-items:center;justify-content:center'; const box=document.createElement('div'); box.style.cssText='background:#fff;padding:32px;min-width:320px;border-radius:2px;box-shadow:0 8px 40px rgba(0,0,0,.3)'; box.innerHTML='
Elegir tipo de sección
' +'
' +types.map(([val,lbl])=>'').join('') +'
' +''; box.className='canvas-section-picker'; overlay.appendChild(box); overlay.onclick=function(e){ if(e.target===overlay) overlay.remove(); }; document.body.appendChild(overlay); } function canvasAddSectionType(secId, type, picker){ if(picker) picker.remove(); const sec=sections.find(s=>s.id===secId); if(!sec) return; const newSec={id:'sec_'+Date.now(),type:type,data:{}}; if(type==='canvas') newSec.data={bg:'#fff',els:[]}; if(type==='imagegrid') newSec.data={cols:3,imgs:[null,null,null],links:[]}; if(type==='grid') newSec.data={skus:[]}; if(type==='banner') newSec.data={text:'Timeless elegance, crafted for you.'}; if(type==='text') newSec.data={heading:'About Our Craft',body:'Each piece is handcrafted with care and intention.'}; if(type==='divider') newSec.data={text:'COLLECTION',fmt:{}}; if(type==='image'||type==='video') newSec.data={}; // Add as nested section element in canvas if(!sec.data.nestedSections) sec.data.nestedSections=[]; sec.data.nestedSections.push(newSec); // Also add a reference element in els sec.data.els.push({type:'section',secId:newSec.id,x:20,y:20,w:600,h:300,z:1}); save(); const container=document.querySelector('.sec-canvas[data-secid="'+secId+'"]'); if(container) buildCanvas(container,sec); } function canvasAddVideo(secId){ const inp=document.createElement('input'); inp.type='file'; inp.accept='video/mp4,video/*'; inp.onchange=function(e){ const file=e.target.files[0]; if(!file) return; const reader=new FileReader(); reader.onload=function(ev){ const sec=sections.find(s=>s.id===secId); if(!sec) return; sec.data.els.push({type:'video',src:ev.target.result,x:40,y:40,w:400,h:260,z:1}); save(); const container=document.querySelector('.sec-canvas[data-secid="'+secId+'"]'); if(container) buildCanvas(container,sec); }; reader.readAsDataURL(file); }; inp.click(); } function canvasReplaceVideo(sec, idx, container){ const inp=document.createElement('input'); inp.type='file'; inp.accept='video/mp4,video/*'; inp.onchange=function(e){ const file=e.target.files[0]; if(!file) return; const reader=new FileReader(); reader.onload=function(ev){ sec.data.els[idx].src=ev.target.result; save(); buildCanvas(container,sec); }; reader.readAsDataURL(file); }; inp.click(); } function canvasAddImage(secId){ const inp=document.createElement('input'); inp.type='file'; inp.accept='image/*'; inp.onchange=e=>{ const file=e.target.files[0]; if(!file) return; compressImage(file,1200,900,.88).then(dataUrl=>{ const sec=sections.find(s=>s.id===secId); if(!sec) return; sec.data.els=sec.data.els||[]; const idx=sec.data.els.length; sec.data.els.push({type:'image',src:dataUrl,x:40,y:40,w:300,h:220,z:idx+1}); save(); const container=document.querySelector('.sec-canvas[data-secid="'+secId+'"]'); if(container) buildCanvas(container,sec); }); }; inp.click(); } function canvasAddText(secId){ const sec=sections.find(s=>s.id===secId); if(!sec) return; sec.data.els=sec.data.els||[]; const idx=sec.data.els.length; sec.data.els.push({type:'text',text:'Your text here',x:60,y:60,w:260,h:80,z:idx+1,fontSize:24,color:'#1A1A1A',font:'Playfair Display'}); save(); const container=document.querySelector('.sec-canvas[data-secid="'+secId+'"]'); if(container) buildCanvas(container,sec); } function canvasReplaceImage(sec, idx, container){ const inp=document.createElement('input'); inp.type='file'; inp.accept='image/*'; inp.onchange=e=>{ const file=e.target.files[0]; if(!file) return; compressImage(file,1200,900,.88).then(dataUrl=>{ sec.data.els[idx].src=dataUrl; save(); buildCanvas(container,sec); }); }; inp.click(); } function canvasDeleteEl(sec, idx, container){ sec.data.els.splice(idx,1); save(); buildCanvas(container,sec); } function canvasTextStyle(secId, idx, prop, val){ const sec=sections.find(s=>s.id===secId); if(!sec) return; sec.data.els[idx][prop]=val; // Update textarea live const container=document.querySelector('.sec-canvas[data-secid="'+secId+'"]'); if(container){ const tas=container.querySelectorAll('.canvas-el'); if(tas[idx]){ const ta=tas[idx].querySelector('textarea'); if(ta){ if(prop==='fontSize') ta.style.fontSize=val+'px'; if(prop==='color') ta.style.color=val; if(prop==='font') ta.style.fontFamily="'"+val+"',serif"; } } } save(); } function canvasTextToggle(secId, idx, prop, btn){ const sec=sections.find(s=>s.id===secId); if(!sec) return; sec.data.els[idx][prop]=!sec.data.els[idx][prop]; btn.classList.toggle('active', sec.data.els[idx][prop]); const container=document.querySelector('.sec-canvas[data-secid="'+secId+'"]'); if(container){ const nodes=container.querySelectorAll('.canvas-el'); if(nodes[idx]){ const ta=nodes[idx].querySelector('textarea'); if(ta){ if(prop==='bold') ta.style.fontWeight=sec.data.els[idx][prop]?'bold':'normal'; if(prop==='italic') ta.style.fontStyle=sec.data.els[idx][prop]?'italic':'normal'; } } } save(); } /* ─── DIVIDER FORMAT ─────────────────────────────────────────── */ function divFmt(secId, prop, val){ const sec=sections.find(s=>s.id===secId); if(!sec) return; sec.data.fmt = sec.data.fmt||{}; sec.data.fmt[prop]=val; // Re-render the divider section for instant update const el=document.querySelector('.page-section[data-secid="'+secId+'"]'); if(el){ const oldInner=el.querySelector(':scope > div:not(.section-ctrl):not(.sec-resize-handle)'); if(oldInner) el.removeChild(oldInner); const df=sec.data.fmt; const dfs=df.size||10, dfc=df.color||'var(--t)', dff=df.font||'Lato'; const dfb=df.bold?'font-weight:bold;':'', dfi=df.italic?'font-style:italic;':''; const dftt=df.tt!==false?'text-transform:uppercase;letter-spacing:.35em;':'letter-spacing:.08em;'; const divStyle=`font-size:${dfs}px;color:${dfc};font-family:'${dff}',sans-serif;${dfb}${dfi}${dftt}`; // Just update the text and line styles live el.querySelectorAll('.div-text').forEach(t=>t.style.cssText=divStyle); el.querySelectorAll('.div-line').forEach(l=>l.style.background=df.lineColor||'var(--border)'); } save(); } function divFmtToggle(secId, prop, btn){ const sec=sections.find(s=>s.id===secId); if(!sec) return; sec.data.fmt=sec.data.fmt||{}; if(prop==='tt') sec.data.fmt.tt = sec.data.fmt.tt===false?true:false; else sec.data.fmt[prop]=!sec.data.fmt[prop]; btn.classList.toggle('active', prop==='tt'?sec.data.fmt.tt!==false:!!sec.data.fmt[prop]); divFmt(secId, prop, sec.data.fmt[prop]); } /* ─── NAV FADE ON SCROLL ──────────────────────────────────────── */ (function(){ const nav = document.getElementById('siteNav'); const TWO_SCROLLS = 240; let ticking = false; function isPanelOpen(){ return ['shopPanel','cartPanel','productsPanel','checkoutPanel'].some(id=>{ const el=document.getElementById(id); return el && (el.classList.contains('open') || el.style.display==='flex'); }); } function updateNav(){ if(!nav) return; if(isPanelOpen()){ nav.classList.remove('nav-hidden'); nav.style.opacity='1'; nav.style.pointerEvents='auto'; return; } const y = window.scrollY; if(y > TWO_SCROLLS){ nav.classList.add('nav-hidden'); } else { nav.classList.remove('nav-hidden'); nav.style.opacity=''; nav.style.pointerEvents=''; } nav.classList.toggle('nav-scrolled', y > 10); } window.addEventListener('scroll', function(){ if(!ticking){ requestAnimationFrame(function(){ updateNav(); ticking=false; }); ticking=true; } }, {passive:true}); // Expose so panel open/close can call it window._updateNav = updateNav; })(); function spUpdateField(i, field, val){ if(!products[i]) return; products[i][field] = val.trim(); save(); spRenderSidebar(); // refresh counts if qty changed } function spDeleteCard(i){ if(!confirm('Delete "'+products[i].name+'"?')) return; products.splice(i,1); save(); spRenderSidebar(); spRender(); } function openSearch(){ // Open shop panel and focus the search input openShop(); setTimeout(()=>{ const s=document.getElementById('spSearch'); if(s){ s.focus(); s.select(); } },200); } function openShopFamily(fam){ openShop(); setTimeout(()=>{ spActive=fam; spRenderSidebar(); spRender(); }, 50); } function spOpenAddProduct(){ // Fill family dropdown const sel = document.getElementById('spProdFamily'); sel.innerHTML = ''; FAMILIES.filter(f=>f.id!=='all').forEach(f=>{ const o=document.createElement('option'); o.value=f.id; o.textContent=f.icon+' '+f.label; if(f.id===spActive) o.selected=true; sel.appendChild(o); }); document.getElementById('spModalFamLabel').textContent = FAMILIES.find(f=>f.id===spActive)?.label||'Collection'; // Reset form const pcSelNew=document.getElementById('spProdPriceCode'); if(pcSelNew){ _populatePriceCodeSelect(pcSelNew,''); pcSelNew.onchange=function(){ const prev=document.getElementById('spProdPricePreview'); if(prev) prev.textContent=this.value?('Precio: '+_getPriceFromCode(this.value)):''; }; } ['spProdName','spProdNum','spProdDesc'].forEach(id=>{ const el=document.getElementById(id); if(el) el.value=''; }); const discEl=document.getElementById('spProdDiscount'); if(discEl) discEl.value=''; const discWrap=document.getElementById('discountFieldWrap'); if(discWrap) discWrap.style.display=currentUserRole==='vendedor'?'none':''; document.getElementById('spProdQty').value=''; const metalEl=document.getElementById('spProdMetal'); if(metalEl){ if(spMetalFilter && spMetalFilter.length===1) metalEl.value=spMetalFilter[0]; else if(spMetalFilter && spMetalFilter.includes('Oro')) metalEl.value='Oro'; else metalEl.value=''; } document.getElementById('spImgPreviewBox').innerHTML='📷 Click to upload photo'; document.getElementById('spProdErr').style.display='none'; _spProdImg=null; _spProdImgPromise=null; document.getElementById('spAddProductModal').classList.add('open'); setTimeout(()=>{ document.getElementById('spProdName').focus(); spUpdateSizeOptions(); }, 50); } function spCloseAddProduct(){ document.getElementById('spAddProductModal').classList.remove('open'); _spEditIdx = -1; document.getElementById('spModalTitle').textContent = 'Add New Product'; document.getElementById('spModalSaveBtn').textContent = 'Add to Collection'; } function spProdImgPreview(e){ const file=e.target.files[0]; if(!file) return; _spProdImgPromise=compressImage(file).then(dataUrl=>{ _spProdImg=dataUrl; const box=document.getElementById('spImgPreviewBox'); box.innerHTML=''; box.onclick=()=>document.getElementById('spProdImgInput')&&document.getElementById('spProdImgInput').click(); }); } let _spEditIdx = -1; function spResizeCard(posKey, card, w){ card.style.width=w+'px'; _spScaleCard(card, w); try{ let pos=JSON.parse(localStorage.getItem(posKey)||'{}'); const idx=card.dataset.pidx; if(!pos[idx]) pos[idx]={}; pos[idx].w=w; localStorage.setItem(posKey,JSON.stringify(pos)); }catch(e){} } let _spSnapEnabled = false; let _spSelected = new Set(); function spToggleSelect(card){ if(_spSelected.has(card)){ _spSelected.delete(card); card.classList.remove('sp-selected'); const cb=card.querySelector('.sp-select-cb'); if(cb) cb.checked=false; } else { _spSelected.add(card); card.classList.add('sp-selected'); const cb=card.querySelector('.sp-select-cb'); if(cb) cb.checked=true; } spUpdateDimPanel(); } function spClearSelection(){ _spSelected.forEach(card=>{ card.classList.remove('sp-selected'); const cb=card.querySelector('.sp-select-cb'); if(cb) cb.checked=false; }); _spSelected.clear(); spUpdateDimPanel(); } function spUpdateDimPanel(){ const panel=document.getElementById('spDimPanel'); const count=document.getElementById('spDimCount'); if(!panel) return; if(_spSelected.size===0){ panel.style.display='none'; } else { panel.style.display='flex'; count.textContent=_spSelected.size+' seleccionado'+(_spSelected.size!==1?'s':''); const ws=[..._spSelected].map(c=>c.offsetWidth); const allSame=ws.every(w=>w===ws[0]); const wInput=document.getElementById('spDimW'); if(wInput){ wInput.placeholder=allSame?String(ws[0]):'—'; if(allSame) wInput.value=ws[0]; } } } function spApplyDims(){ const wVal=parseInt(document.getElementById('spDimW').value); if(!wVal||isNaN(wVal)) return; const posKey='avz_sp_pos_'+(spActive||'all'); let pos={}; try{ pos=JSON.parse(localStorage.getItem(posKey)||'{}'); }catch(e){} _spSelected.forEach(card=>{ card.style.width=wVal+'px'; _spScaleCard(card, wVal); const pidx=card.dataset.pidx; if(!pos[pidx]) pos[pidx]={}; pos[pidx].w=wVal; }); localStorage.setItem(posKey,JSON.stringify(pos)); spUpdateDimPanel(); const grid=document.getElementById('spGrid'); if(grid){ const cards=grid.querySelectorAll('.sp-card'); let maxB=0; cards.forEach(c=>{ const b=(parseInt(c.style.top)||0)+c.offsetHeight; if(b>maxB) maxB=b; }); grid.style.minHeight=(maxB+100)+'px'; } } function spSaveLayout(){ // Collect current card positions & sizes from DOM const grid = document.getElementById('spGrid'); if(!grid) return; const posKey = 'avz_sp_pos_'+(spActive||'all'); let pos = {}; try{ pos=JSON.parse(localStorage.getItem(posKey)||'{}'); }catch(e){} grid.querySelectorAll('.sp-card').forEach(card=>{ const pidx = parseInt(card.dataset.pidx); if(!pos[pidx]) pos[pidx]={}; pos[pidx].x = parseInt(card.style.left)||0; pos[pidx].y = parseInt(card.style.top)||0; pos[pidx].w = card.offsetWidth; }); localStorage.setItem(posKey, JSON.stringify(pos)); // Save toolbar settings const settings = { cols: document.getElementById('spAlignCols').value, gap: document.getElementById('spAlignGap').value, w: document.getElementById('spAlignW').value, posKey, pos }; try{ localStorage.setItem('avz_layout_settings', JSON.stringify(settings)); }catch(e){} // Embed into HTML so it survives Compartir / logout updateEmbeddedTag(); // Flash feedback const btn = document.getElementById('spSaveLayoutBtn'); if(btn){ btn.textContent='✓ Guardado!'; btn.style.background='rgba(39,174,96,.5)'; setTimeout(()=>{ btn.textContent='💾 Guardar Layout'; btn.style.background='rgba(255,255,255,.15)'; },1800); } } function spLoadLayout(){ try{ const raw = localStorage.getItem('avz_layout_settings'); if(!raw) return; const s = JSON.parse(raw); const colsEl=document.getElementById('spAlignCols'); const gapEl =document.getElementById('spAlignGap'); const wEl =document.getElementById('spAlignW'); if(colsEl&&s.cols) colsEl.value=s.cols; if(gapEl&&s.gap) gapEl.value=s.gap; if(wEl&&s.w) wEl.value=s.w; }catch(e){} } function spAutoArrange(){ const cols = parseInt(document.getElementById('spAlignCols').value)||3; const gap = parseInt(document.getElementById('spAlignGap').value)||16; const w = parseInt(document.getElementById('spAlignW').value)||220; const grid = document.getElementById('spGrid'); if(!grid) return; const cards = Array.from(grid.querySelectorAll('.sp-card')); const posKey = 'avz_sp_pos_'+(spActive||'all'); let pos = {}; try{ pos=JSON.parse(localStorage.getItem(posKey)||'{}'); }catch(e){} // Calibrate SP_BASE_H at new width first let maxNatH=0; cards.forEach(card=>{ const inner=card.querySelector('.sp-card-inner'); if(!inner) return; inner.style.transform='none'; inner.style.width=SP_BASE_W+'px'; inner.style.height='auto'; inner.style.overflow='visible'; const h=inner.scrollHeight||inner.offsetHeight; if(h>maxNatH) maxNatH=h; }); if(maxNatH>0) SP_BASE_H=maxNatH; const scale = w/SP_BASE_W; const cardH = Math.round(SP_BASE_H*scale); // Place cards: each row Y is gap + sum of previous rows cards.forEach((card,idx)=>{ const col = idx%cols; const row = Math.floor(idx/cols); const x = gap + col*(w+gap); const y = gap + row*(cardH+gap); card.style.left=x+'px'; card.style.top=y+'px'; card.style.width=w+'px'; _spScaleCard(card, w); const pidx=parseInt(card.dataset.pidx); if(!pos[pidx]) pos[pidx]={}; pos[pidx].x=x; pos[pidx].y=y; pos[pidx].w=w; }); localStorage.setItem(posKey, JSON.stringify(pos)); const totalRows=Math.ceil(cards.length/cols); grid.style.minHeight=(gap+totalRows*(cardH+gap))+'px'; } function spSnapToGrid(){ _spSnapEnabled = !_spSnapEnabled; const btn = document.querySelector('[onclick="spSnapToGrid()"]'); if(btn){ btn.style.background = _spSnapEnabled ? 'rgba(10,186,181,.4)' : ''; btn.style.borderColor = _spSnapEnabled ? 'var(--t)' : ''; btn.textContent = _spSnapEnabled ? '⊡ Snap ON' : '⊡ Snap'; } } function _spSnapVal(v, snap){ return snap>0 ? Math.round(v/snap)*snap : v; } const SP_BASE_W = 220; let SP_BASE_H = 480; function _spScaleCard(card, w){ const inner = card.querySelector('.sp-card-inner'); if(!inner) return; const scale = w / SP_BASE_W; inner.style.transform = 'none'; inner.style.width = SP_BASE_W+'px'; if(isAdmin){ // Admin: lock to calibrated base height so all cards identical inner.style.height = SP_BASE_H+'px'; inner.style.overflow = 'hidden'; } else { // Public: let content flow naturally — no clipping inner.style.height = 'auto'; inner.style.overflow = 'visible'; } inner.style.transform = 'scale('+scale+')'; if(isAdmin){ card.style.height = Math.round(SP_BASE_H * scale)+'px'; } else { // Public: measure natural height after scale const naturalH = inner.scrollHeight || inner.offsetHeight || 380; card.style.height = Math.round(naturalH * scale)+'px'; } } // base inner width function initSpCardResize(card, i, posKey, handle){ handle.addEventListener('mousedown',function(e){ e.preventDefault(); e.stopPropagation(); const startX=e.clientX, startY=e.clientY; const startW=card.offsetWidth, startH=card.offsetHeight; const onMove=ev=>{ const newW=Math.max(100, startW+(ev.clientX-startX)); card.style.width=newW+'px'; _spScaleCard(card, newW); }; const onUp=ev=>{ document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp); try{ let pos=JSON.parse(localStorage.getItem(posKey)||'{}'); if(!pos[i]) pos[i]={}; pos[i].w=card.offsetWidth; localStorage.setItem(posKey,JSON.stringify(pos)); }catch(e){} const grid=document.getElementById('spGrid'); if(grid){ const cards=grid.querySelectorAll('.sp-card'); let maxB=0; cards.forEach(c=>{ const b=(parseInt(c.style.top)||0)+c.offsetHeight; if(b>maxB) maxB=b; }); grid.style.minHeight=(maxB+100)+'px'; } }; document.addEventListener('mousemove',onMove); document.addEventListener('mouseup',onUp); }); } function initSpCardDrag(card, i, posKey, handle){ handle.addEventListener('mousedown', function(e){ e.preventDefault(); const startX = e.clientX, startY = e.clientY; const startLeft = parseInt(card.style.left)||0; const startTop = parseInt(card.style.top)||0; card.style.zIndex = '50'; const onMove = (ev)=>{ const gap = _spSnapEnabled ? (parseInt(document.getElementById('spAlignGap').value)||16) : 1; const rawL = startLeft + ev.clientX - startX; const rawT = Math.max(0, startTop + ev.clientY - startY); card.style.left = _spSnapVal(rawL, _spSnapEnabled?gap:0)+'px'; card.style.top = _spSnapVal(rawT, _spSnapEnabled?gap:0)+'px'; }; const onUp = ()=>{ document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); card.style.zIndex = ''; try{ let pos = JSON.parse(localStorage.getItem(posKey)||'{}'); if(!pos[i]) pos[i]={}; pos[i].x = Math.max(0, parseInt(card.style.left)||0); pos[i].y = Math.max(0, parseInt(card.style.top)||0); localStorage.setItem(posKey, JSON.stringify(pos)); }catch(e){} const grid = document.getElementById('spGrid'); if(grid){ const cards = grid.querySelectorAll('.sp-card'); let maxB = 0; cards.forEach(c=>{ const b=(parseInt(c.style.top)||0)+c.offsetHeight; if(b>maxB) maxB=b; }); grid.style.minHeight = (maxB+100)+'px'; } }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); } function spUpdateSizeOptions(){ const fam = document.getElementById('spProdFamily').value; const sel = document.getElementById('spProdSize'); const lbl = document.getElementById('spSizeLabel'); if(fam === 'anillos'){ lbl.textContent = 'Talla'; sel.innerHTML = [4,5,6,7,8,9,10,11,12,13,14,15].map(n=>'').join(''); } else { lbl.textContent = 'Display Size'; sel.innerHTML = ''; } } function spEditProduct(i){ const p = products[i]; if(!p) return; _spEditIdx = i; const sel = document.getElementById('spProdFamily'); sel.innerHTML = ''; FAMILIES.filter(f=>f.id!=='all').forEach(f=>{ const o=document.createElement('option'); o.value=f.id; o.textContent=f.icon+' '+f.label; if(f.id===(p.family||'other')) o.selected=true; sel.appendChild(o); }); document.getElementById('spProdName').value = p.name||''; document.getElementById('spProdNum').value = p.num||''; const pcSel=document.getElementById('spProdPriceCode'); if(pcSel){ _populatePriceCodeSelect(pcSel, p.priceCode||''); pcSel.onchange=function(){ const prev=document.getElementById('spProdPricePreview'); if(prev) prev.textContent=this.value?('Precio: '+_getPriceFromCode(this.value)):''; }; const prev=document.getElementById('spProdPricePreview'); if(prev) prev.textContent=p.priceCode?('Precio: '+_getPriceFromCode(p.priceCode)):''; } const discEl2=document.getElementById('spProdDiscount'); if(discEl2) discEl2.value=p.discount||''; document.getElementById('spProdQty').value = p.qty||''; document.getElementById('spProdDesc').value = p.desc||''; const metalEl = document.getElementById('spProdMetal'); if(metalEl) metalEl.value = p.metal||''; const box = document.getElementById('spImgPreviewBox'); if(p.img){ box.innerHTML=''; box.onclick=()=>document.getElementById('spProdImgInput')&&document.getElementById('spProdImgInput').click(); _spProdImg = p.img; } else { box.innerHTML='📷 Click to upload photo'; _spProdImg = null; } _spProdImgPromise = null; document.getElementById('spModalTitle').textContent = 'Edit Product'; document.getElementById('spModalSaveBtn').textContent = 'Save Changes'; document.getElementById('spModalFamLabel').textContent = FAMILIES.find(f=>f.id===(p.family||'other'))?.label||'Collection'; document.getElementById('spProdErr').style.display='none'; document.getElementById('spAddProductModal').classList.add('open'); setTimeout(()=>{ document.getElementById('spProdName').focus(); spUpdateSizeOptions(); document.getElementById('spProdSize').value = p.size||'medium'; }, 60); } function spSaveProductModal(){ const name=(document.getElementById('spProdName').value||'').trim(); const num=(document.getElementById('spProdNum').value||'').trim(); const errEl = document.getElementById('spProdErr'); if(!name||!num){ errEl.textContent='Name and Product # are required.'; errEl.style.display='block'; return; } // Duplicate number check (skip current product when editing) const dupIdx = products.findIndex((p,i)=>p.num===num && i!==_spEditIdx); if(dupIdx>=0){ errEl.textContent='Product # '+num+' already exists. Use a unique number.'; errEl.style.display='block'; return; } const priceCode=(document.getElementById('spProdPriceCode').value||'').trim(); const price=priceCode?_getPriceFromCode(priceCode):''; const discount=currentUserRole==='vendedor'?'':(document.getElementById('spProdDiscount').value||'').trim(); const qty=(document.getElementById('spProdQty').value||'').trim(); const desc=(document.getElementById('spProdDesc').value||'').trim(); const size=(document.getElementById('spProdSize').value)||'medium'; const metal=(document.getElementById('spProdMetal').value||'').trim(); const img = _spProdImg || null; if(_spEditIdx >= 0){ const existing = products[_spEditIdx]; products[_spEditIdx] = Object.assign({}, existing, {name,num,priceCode,price,discount,qty,desc,size,family:fam,metal,img}); } else { products.unshift({name,num,priceCode,price,discount,qty,desc,size,family:fam,metal,sold:0,img}); } try{ const meta=products.map(p=>Object.assign({},p,{img:null})); localStorage.setItem('avz_meta',JSON.stringify(meta)); localStorage.setItem('avz_imgcount',String(products.length)); for(let i=products.length-1;i>=0;i--){ if(products[i].img) localStorage.setItem('avz_img_'+i,products[i].img); } }catch(e){} // Place new product at next available position matching layout if(_spEditIdx < 0){ const posKey = 'avz_sp_pos_'+(spActive||fam||'all'); try{ let pos = JSON.parse(localStorage.getItem(posKey)||'{}'); const layoutW = parseInt((JSON.parse(localStorage.getItem('avz_layout_settings')||'{}')).w)||220; const layoutGap = parseInt((JSON.parse(localStorage.getItem('avz_layout_settings')||'{}')).gap)||16; const layoutCols = parseInt((JSON.parse(localStorage.getItem('avz_layout_settings')||'{}')).cols)||3; // Find lowest card bottom to place below existing let maxY = 0; Object.values(pos).forEach(p=>{ if(p.y>maxY) maxY=p.y+350; }); const newIdx = products.length - 1; // just unshifted to front, so index 0 const col = 0; // place at start of next row pos[0] = { x: layoutGap, y: maxY + layoutGap, w: layoutW }; localStorage.setItem(posKey, JSON.stringify(pos)); }catch(e){} } spRenderSidebar(); spRender(); save(); } function spSaveProduct(){ const name=(document.getElementById('spProdName').value||'').trim(); const num=(document.getElementById('spProdNum').value||'').trim(); if(!name||!num){ document.getElementById('spProdErr').style.display='block'; return; } // Grab all values immediately (synchronous) const fam=(document.getElementById('spProdFamily').value)||spActive||'other'; const priceCode=(document.getElementById('spProdPriceCode').value||'').trim(); const price=priceCode?_getPriceFromCode(priceCode):''; const qty=(document.getElementById('spProdQty').value||'').trim(); const desc=(document.getElementById('spProdDesc').value||'').trim(); const size=(document.getElementById('spProdSize').value)||'medium'; const metal=(document.getElementById('spProdMetal').value||'').trim(); const img = _spProdImg || null; // Add product immediately products.unshift({name,num,priceCode,price,qty,desc,size,family:fam,metal,sold:0,img}); // Save to localStorage right now (never fails) try{ const meta=products.map(p=>Object.assign({},p,{img:null})); localStorage.setItem('avz_meta',JSON.stringify(meta)); localStorage.setItem('avz_imgcount',String(products.length)); // Save all images with shifted indices for(let i=products.length-1;i>=0;i--){ if(products[i].img) localStorage.setItem('avz_img_'+i,products[i].img); } }catch(e){} // For new products: place at matching layout size & position if(_spEditIdx < 0){ try{ const posKey='avz_sp_pos_'+(fam||'all'); let pos=JSON.parse(localStorage.getItem(posKey)||'{}'); const ls=JSON.parse(localStorage.getItem('avz_layout_settings')||'{}'); const lw=parseInt(ls.w)||220, lgap=parseInt(ls.gap)||16; let maxY=0; Object.values(pos).forEach(p=>{ if((p.y||0)+350>maxY) maxY=(p.y||0)+350; }); pos[0]={x:lgap, y:maxY+lgap, w:lw}; localStorage.setItem(posKey,JSON.stringify(pos)); }catch(e){} } // Close modal and refresh UI immediately spCloseAddProduct(); spActive=fam; spRenderSidebar(); spRender(); // IDB save in background save(); } function spOpenAddSection(){ document.getElementById('spSecType').value='grid'; document.getElementById('spCanvasProductOpts').style.display='none'; ['spCvName','spCvNum','spCvPrice'].forEach(id=>document.getElementById(id).value=''); document.getElementById('spCvQty').value=''; document.getElementById('spAddSectionModal').classList.add('open'); } function spCloseAddSection(){ document.getElementById('spAddSectionModal').classList.remove('open'); } /* Current canvas section being edited inside shop panel */ let _spCanvasSec = null; function spSaveSection(){ const type=document.getElementById('spSecType').value; if(type==='canvas'){ // Open full canvas editor inside shop panel spCloseAddSection(); spOpenCanvasEditor(); return; } const id='s'+Date.now(); const data={}; if(type==='hero') Object.assign(data,{eyebrow:'New Banner',title:'New Headline',em:'elegance',sub:'Your subtitle here'}); else if(type==='divider') data.text='Collection'; else if(type==='banner') data.text='Your message here'; else if(type==='text') Object.assign(data,{heading:'New Heading',body:'Your content here.'}); else if(type==='image') Object.assign(data,{img:null,alt:''}); else if(type==='imagegrid') Object.assign(data,{imgs:[],cols:3,links:[]}); sections.push({id,type,data}); save().then(()=>{ renderSections(); fillGrid(); spRenderSidebar(); spRender(); spCloseAddSection(); }); } /* ── CANVAS EDITOR inside Shop Panel ── */ function spOpenCanvasEditor(){ // Create a new canvas section const sec = {id:'s'+Date.now(), type:'canvas', data:{bg:'#ffffff',els:[]}}; _spCanvasSec = sec; // Also handle optional product const cvName=(document.getElementById('spCvName').value||'').trim(); const cvNum=(document.getElementById('spCvNum').value||'').trim(); if(cvName&&cvNum){ const fam=spActive==='all'?'other':spActive; products.unshift({name:cvName,num:cvNum, price:(document.getElementById('spCvPrice').value||'').trim(), qty:(document.getElementById('spCvQty').value||'').trim(), family:fam,sold:0,img:null,size:'medium'}); save(); } // Show canvas editor overlay inside shop panel const overlay = document.getElementById('spCanvasOverlay'); const container = document.getElementById('spCanvasContainer'); container.innerHTML=''; container.style.background=sec.data.bg; container.dataset.secid=sec.id; buildCanvas(container, sec); overlay.style.display='flex'; } function spCloseCanvasEditor(){ if(_spCanvasSec){ sections.push(_spCanvasSec); save().then(()=>{ renderSections(); fillGrid(); }); } _spCanvasSec=null; document.getElementById('spCanvasOverlay').style.display='none'; } /* ─── PRODUCT DETAIL MODAL ─── */ function openProductDetail(prodIdx){ const p = products[prodIdx]; if(!p) return; const fam = (FAMILIES.find(f=>f.id===(p.family||'other'))||{label:'Collection',icon:'💎'}); const maxQty = parseInt(p.qty)||0; const outOfStock = maxQty === 0; document.getElementById('pdImgSide').innerHTML = p.img ? ''+esc(p.name)+'' : '
'+fam.icon+'
'; document.getElementById('pdInfoSide').innerHTML = '
'+ '

'+fam.icon+' '+fam.label+(p.metal?' · '+esc(p.metal):'')+'

'+ '

'+esc(p.name)+'

'+ '

'+esc(p.num)+'

'+ (p.desc?'

'+esc(p.desc)+'

':'')+ '
'+ (p.price?'

'+(p.discount&&parseInt(p.discount)>0?''+esc(p.price)+''+esc(_discountedPrice(p))+'':esc(p.price))+'

':'')+ '
'+ '
'+ (outOfStock ? '

Agotado

' : '
'+ 'Cantidad'+ '
'+ ''+ '1'+ ''+ '
'+ ''+maxQty+' disponibles'+ '
' )+ ''+ '
'; document.getElementById('productDetailModal').classList.add('open'); document.getElementById('productDetailModal').style.display='flex'; document.body.style.overflow='hidden'; } function closeProductDetail(){ document.getElementById('productDetailModal').classList.remove('open'); document.getElementById('productDetailModal').style.display='none'; document.body.style.overflow=''; } function pdQtyChange(delta, max){ const el = document.getElementById('pdQtyVal'); if(!el) return; let v = parseInt(el.textContent)||1; v = Math.min(max, Math.max(1, v + delta)); el.textContent = v; } function pdAddToCart(prodIdx){ const el = document.getElementById('pdQtyVal'); const qty = el ? parseInt(el.textContent)||1 : 1; const p = products[prodIdx]; if(!p) return; const avail = parseInt(p.qty)||0; const toAdd = Math.min(qty, avail); for(let i=0; i{ const s=document.getElementById('spSearch'); if(s){ s.focus(); s.select(); } },200); } function openShopFamily(fam){ openShop(); setTimeout(()=>{ spActive=fam; spRenderSidebar(); spRender(); }, 50); } /* ═══ ADMIN DASHBOARD ═══ */ function openDashboard(){ const bpa=document.getElementById('btnPanelAdmin'); if(bpa) bpa.style.display='none'; const bps=document.getElementById('btnPublicarSecciones'); if(bps) bps.style.display='none'; const dash=document.getElementById('adminDashboard'); if(!dash) return; const lbl=document.getElementById('dashUserLabel'); const badge=document.getElementById('dashRoleBadge'); const roleLabels={administrador:'🔑 Administrador',supervisor:'👁 Supervisor',vendedor:'🛍 Vendedor'}; const roleColors={administrador:'var(--t)',supervisor:'#8e44ad',vendedor:'#e67e22'}; if(lbl) lbl.textContent=currentUserName||ADMIN.user; if(badge){ badge.textContent=roleLabels[currentUserRole]||currentUserRole; badge.style.background=roleColors[currentUserRole]||'var(--t)'; badge.style.color='#fff'; } const isPrim=currentUserRole==='administrador'; const isVendedor=currentUserRole==='vendedor'; const dnV=document.getElementById('dnVentas'); if(dnV) dnV.style.display=isPrim?'block':'none'; const dnU=document.getElementById('dnUsuarios'); if(dnU) dnU.style.display=isPrim?'block':'none'; const dnP=document.getElementById('dnPrecios'); if(dnP) dnP.style.display=isPrim?'block':'none'; const dnR=document.getElementById('dnRegistrados'); if(dnR) dnR.style.display=isPrim?'block':'none'; const dnNR=document.getElementById('dnNoRegistrados'); if(dnNR) dnNR.style.display=isPrim?'block':'none'; const dnO=document.getElementById('dnOrdenes'); if(dnO) dnO.style.display='block'; const dnVd=document.getElementById('dnVendidos'); if(dnVd) dnVd.style.display=isVendedor?'none':'block'; // Compartir: admin only const dcb=document.getElementById('dashCompartirBtn'); if(dcb) dcb.style.display=isPrim?'block':'none'; // Move panels into slots once _dashMovePanel('productsPanel','dashSlotProducts'); _dashMovePanel('ordenesPanel','dashSlotOrdenes'); _dashMovePanel('ventasPanel','dashSlotVentas'); dash.style.display='flex'; dashShow(isPrim?'products':'products'); } function _dashMovePanel(panelId, slotId){ const panel=document.getElementById(panelId); const slot=document.getElementById(slotId); if(!panel||!slot||slot.contains(panel)) return; panel.style.cssText='position:relative;display:flex;flex:1;flex-direction:column;overflow:hidden;background:#fff;top:auto;left:auto;right:auto;bottom:auto;z-index:auto;'; slot.appendChild(panel); } function closeDashboard(){ const dash=document.getElementById('adminDashboard'); if(dash) dash.style.display='none'; const bpa=document.getElementById('btnPanelAdmin'); if(bpa) bpa.style.display=(isAdmin&¤tUserRole!=='vendedor')?'block':'none'; const bps=document.getElementById('btnPublicarSecciones'); if(bps) bps.style.display=isMainAdmin()?'block':'none'; } let _dashSection='products'; let _dashInit={}; function dashShow(section){ _dashSection=section; ['products','vendidos','ordenes','ventas','precios','usuarios','registrados','noregistrados'].forEach(s=>{ const btn=document.getElementById('dn'+s.charAt(0).toUpperCase()+s.slice(1)); if(btn) btn.classList.toggle('dash-active',s===section); }); // Hide all slots ['Products','Vendidos','Ordenes','Ventas','Precios','Usuarios','Registrados','NoRegistrados'].forEach(s=>{ const slot=document.getElementById('dashSlot'+s); if(slot) slot.style.display='none'; }); // Show active slot const slotIdMap={registrados:'Registrados',noregistrados:'NoRegistrados'}; const slotId='dashSlot'+(slotIdMap[section]||(section.charAt(0).toUpperCase()+section.slice(1))); const active=document.getElementById(slotId); if(active) active.style.display=(section==='usuarios'||section==='vendidos'||section==='registrados'||section==='noregistrados')?'block':'flex'; if(section==='vendidos'){ const slot=document.getElementById('dashSlotVendidos'); if(!slot) return; slot.innerHTML='
' +'
' +'
🏆 Más Vendidos
' +(isMainAdmin()?'':'') +'
' +'

Por número de producto · orden descendente

' +'
' +'
' +'
'; _renderVendidosTable(); return; } if(section==='precios'){ _dashRenderPrecios(); return; } if(section==='registrados'){ _dashRenderRegistrados(); return; } if(section==='noregistrados'){ _dashRenderNoRegistrados(); return; } // Init content on first visit if(!_dashInit[section]){ _dashInit[section]=true; if(section==='products'){ ppRender(); ppRenderStats(); } else if(section==='ordenes'){ ordenesLoad(); _ordenesTab='activas'; ordenesRender(); const filt=document.getElementById('archiveFilters'); if(filt) filt.style.display='none'; const cfilt=document.getElementById('cancelFilters'); if(cfilt) cfilt.style.display='none'; ['tabActivas','tabArchivadas','tabCanceladas'].forEach((id,i)=>{ const el=document.getElementById(id); if(!el) return; el.style.background=i===0?'var(--t)':'rgba(255,255,255,.12)'; el.style.color=i===0?'#fff':'rgba(255,255,255,.7)'; }); } else if(section==='ventas'){ ordenesLoad(); ventasRender(); } else if(section==='usuarios'){ _dashRenderUsuarios(); } } else { if(section==='products'){ ppRender(); ppRenderStats(); } else if(section==='ventas'){ ventasRender(); } else if(section==='usuarios'){ _dashRenderUsuarios(); } } } /* ═══ REGISTRADOS / NO REGISTRADOS ═══ */ let _customersCache=[]; function _dashRenderRegistrados(){ const slot=document.getElementById('dashSlotRegistrados'); if(!slot) return; slot.innerHTML='
Cargando clientes...
'; fetch('api/register_customer.php?all=1').then(r=>r.json()).then(d=>{ _customersCache=d.customers||[]; _renderRegistradosTable(slot); }).catch(()=>{ slot.innerHTML='
📇 Clientes Registrados
' +'

No se pudo conectar con el servidor. El backend (carpeta api/) no está instalado o no responde.

'; }); } function _renderRegistradosTable(slot){ ordenesLoad(); let rows=''; if(!_customersCache.length){ rows='

Aún no hay clientes registrados.

'; } else { rows=_customersCache.map((c,i)=>{ const custOrders=(c.orders||[]).map(oid=>orders.find(o=>o.id===oid)).filter(Boolean); const totalSpent=custOrders.reduce((s,o)=>s+parseFloat(o.total||0),0); const ordersHtml=custOrders.length ?custOrders.map(o=>'#'+esc(o.id)+'' +''+esc(o.date||'')+'' +''+(o.items||[]).length+' artículo(s)' +'$'+parseFloat(o.total||0).toFixed(2)+'').join('') :'Sin compras registradas'; return '
' +'
' +'
' +'' +'
' +'
'+esc((c.firstName||'')+' '+(c.lastName||''))+'
' +'
'+esc(c.email)+(c.phone?' · '+esc(c.phone):'')+'
' +'
' +'
' +'
' +'
' +'
Total Gastado
' +'
$'+totalSpent.toFixed(2)+'
' +'
' +'
'+custOrders.length+' orden(es)
' +'' +'
' +'
' +'' +'
'; }).join(''); } slot.innerHTML='
' +'
📇 Clientes Registrados
' +'

'+_customersCache.length+' cliente(s) · Sin datos bancarios

' +rows +'
'; } function _toggleCustomerRow(i){ const row=document.getElementById('custRow'+i); const chev=document.getElementById('custChevron'+i); if(!row) return; const open=row.style.display==='block'; row.style.display=open?'none':'block'; if(chev) chev.style.transform=open?'':'rotate(90deg)'; } function deleteCustomer(id){ if(!confirm('¿Eliminar este cliente registrado? Esta acción no se puede deshacer.')) return; fetch('api/delete_customer.php',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({id}) }).then(r=>r.json()).then(d=>{ if(d.ok){ _dashRenderRegistrados(); } else alert(d.error||'No se pudo eliminar.'); }).catch(()=>alert('No se pudo conectar con el servidor.')); } function _dashRenderNoRegistrados(){ const slot=document.getElementById('dashSlotNoRegistrados'); if(!slot) return; ordenesLoad(); fetch('api/register_customer.php?all=1').then(r=>r.json()).then(d=>{ const registeredEmails=new Set((d.customers||[]).map(c=>(c.email||'').toLowerCase())); _renderNoRegistradosTable(slot, registeredEmails); }).catch(()=>{ _renderNoRegistradosTable(slot, new Set()); }); } function _renderNoRegistradosTable(slot, registeredEmails){ const guestOrders=orders.filter(o=>{ const email=((o.shipping&&o.shipping.email)||'').toLowerCase(); return email && !registeredEmails.has(email); }); // Group by email const byEmail={}; guestOrders.forEach(o=>{ const email=(o.shipping.email||'').toLowerCase(); if(!byEmail[email]) byEmail[email]={shipping:o.shipping, orders:[]}; byEmail[email].orders.push(o); }); const emails=Object.keys(byEmail); let rows=''; if(!emails.length){ rows='

No hay compras de invitados (no registrados).

'; } else { rows=emails.map((email,i)=>{ const g=byEmail[email]; const s=g.shipping; const totalSpent=g.orders.reduce((sum,o)=>sum+parseFloat(o.total||0),0); const ordersHtml=g.orders.map(o=>'#'+esc(o.id)+'' +''+esc(o.date||'')+'' +''+(o.items||[]).length+' artículo(s)' +'$'+parseFloat(o.total||0).toFixed(2)+'').join(''); return '
' +'
' +'
' +'' +'
' +'
'+esc((s.firstName||'')+' '+(s.lastName||''))+'
' +'
'+esc(email)+(s.phone?' · '+esc(s.phone):'')+'
' +'
' +'
' +'
' +'
' +'
Total Gastado
' +'
$'+totalSpent.toFixed(2)+'
' +'
' +'
'+g.orders.length+' orden(es)
' +'
' +'
' +'' +'
'; }).join(''); } slot.innerHTML='
' +'
👥 Compras sin Registro
' +'

'+emails.length+' cliente(s) invitado(s)

' +rows +'
'; } function _toggleGuestRow(i){ const row=document.getElementById('guestRow'+i); const chev=document.getElementById('guestChevron'+i); if(!row) return; const open=row.style.display==='block'; row.style.display=open?'none':'block'; if(chev) chev.style.transform=open?'':'rotate(90deg)'; } function _dashRenderPrecios(){ const slot=document.getElementById('dashSlotPrecios'); if(!slot) return; priceCodesLoad(); let codesHtml=''; if(!priceCodes.length){ codesHtml='

No hay códigos de precio aún.

'; } else { codesHtml=priceCodes.map((pc,i)=>'
' +''+esc(pc.code)+'' +''+esc(pc.label||'')+'' +'$'+esc(String(pc.price))+' MXN' +'' +'' +'
').join(''); } slot.innerHTML='
' +'

🏷 Códigos de Precio

' +'

Solo administradores pueden crear, editar o eliminar códigos

' +'
' +'
Nuevo Código de Precio
' +'
' +'
' +'
' +'
' +'
' +'
' +'
' +'
' +'' +'' +'
' +'
'+codesHtml+'
' +'
'; } function priceCodeSave(){ const code=(document.getElementById('pcCode').value||'').trim().toUpperCase(); const price=parseFloat(document.getElementById('pcPrice').value||'0'); const label=(document.getElementById('pcLabel').value||'').trim(); const err=document.getElementById('pcErr'); if(!code||isNaN(price)||price<0){ err.textContent='Código y precio son requeridos.'; err.style.display='block'; return; } priceCodesLoad(); if(priceCodes.some(pc=>pc.code===code)){ err.textContent='Ese código ya existe.'; err.style.display='block'; return; } priceCodes.push({code, price, label}); priceCodesSave(); updateEmbeddedTag(); err.style.display='none'; ['pcCode','pcPrice','pcLabel'].forEach(id=>{ const el=document.getElementById(id); if(el) el.value=''; }); _dashRenderPrecios(); } function priceCodeEdit(idx){ priceCodesLoad(); const pc=priceCodes[idx]; if(!pc) return; const newPrice=prompt('Nuevo precio para '+pc.code+' ('+pc.label+'):', pc.price); if(newPrice===null) return; const val=parseFloat(newPrice); if(isNaN(val)||val<0){ alert('Precio inválido'); return; } const newLabel=prompt('Nueva descripción:', pc.label||''); if(newLabel===null) return; priceCodes[idx].price=val; priceCodes[idx].label=newLabel; priceCodesSave(); updateEmbeddedTag(); _dashRenderPrecios(); } function priceCodeDelete(idx){ priceCodesLoad(); const pc=priceCodes[idx]; if(!pc) return; if(!confirm('¿Eliminar código "'+pc.code+'"? Los productos que lo usan quedarán sin precio.')) return; priceCodes.splice(idx,1); priceCodesSave(); updateEmbeddedTag(); _dashRenderPrecios(); } function _getPriceFromCode(code){ const pc=priceCodes.find(p=>p.code===code); return pc ? '$'+pc.price : ''; } function _populatePriceCodeSelect(selectEl, selectedCode){ priceCodesLoad(); selectEl.innerHTML=''; priceCodes.forEach(pc=>{ const o=document.createElement('option'); o.value=pc.code; o.textContent=pc.code+(pc.label?' — '+pc.label:'')+' ($'+pc.price+' MXN)'; if(pc.code===selectedCode) o.selected=true; selectEl.appendChild(o); }); } function _dashRenderUsuarios(){ const slot=document.getElementById('dashSlotUsuarios'); if(!slot) return; adminUsersLoad(); let usersHtml='
Usuarios Registrados
'; if(!adminUsers.length){ usersHtml+='

No hay usuarios adicionales.

'; } else { adminUsers.forEach(function(u,i){ const role=u.role||'administrador'; const selA=role==='administrador'?' selected':'', selS=role==='supervisor'?' selected':''; usersHtml+='
' +'
'+esc(u.first)+' '+esc(u.last)+'
' +'
@'+esc(u.user)+'
' +'
'+(u.email||'')+' '+(u.phone||'')+'
' +'
' +'' +'' +'
'; }); } slot.innerHTML='
' +'

Usuarios

' +'

Gestión de accesos

' +'
' +'
Nuevo Usuario
' +'
' +'
' +'
' +'
' +'
' +'
' +'
' +'
' +'
' +'
' +'' +'' +'
' +'
'+usersHtml+'
' +'
'; } function changeUserRole2(i,role){ adminUsersLoad(); if(!adminUsers[i]) return; adminUsers[i].role=role; adminUsersSave(); _dashRenderUsuarios(); _dashInit.usuarios=false; dashShow('usuarios'); } function deleteAdminUser2(i){ adminUsersLoad(); if(!confirm('¿Eliminar usuario "'+adminUsers[i].user+'"?')) return; adminUsers.splice(i,1); adminUsersSave(); _dashInit.usuarios=false; dashShow('usuarios'); } function saveNewAdminUser2(){ const user=(document.getElementById('nu_user2').value||'').trim(); const pass=(document.getElementById('nu_pass2').value||'').trim(); const first=(document.getElementById('nu_first2').value||'').trim(); const last=(document.getElementById('nu_last2').value||'').trim(); const email=(document.getElementById('nu_email2').value||'').trim(); const phone=(document.getElementById('nu_phone2').value||'').trim(); const role=(document.getElementById('nu_role2').value)||'administrador'; const err=document.getElementById('nu_err2'); if(!user||!pass||!first||!last){ if(err){err.textContent='Campos requeridos faltantes.';err.style.display='block';} return; } adminUsersLoad(); if(user===ADMIN.user||adminUsers.some(a=>a.user===user)){ if(err){err.textContent='Usuario ya existe.';err.style.display='block';} return; } adminUsers.push({user,pass,first,last,email,phone,role}); adminUsersSave(); _dashInit.usuarios=false; dashShow('usuarios'); } /* ═══ VENTAS ═══ */ let _ventasFrom = null; // YYYY-MM-DD let _ventasTo = null; let _ventasCalPickingFrom = true; let _ventasCalMonth1 = null; // Date object for left calendar let _ventasPeriod = 'all'; function openVentasPanel(){ ordenesLoad(); document.getElementById('ventasPanel').style.display='flex'; if(!_ventasCalMonth1) _ventasCalMonth1 = new Date(); ventasRenderCal(); ventasRender(); } function closeVentasPanel(){ document.getElementById('ventasPanel').style.display='none'; const drop=document.getElementById('ventasCalDrop'); if(drop) drop.style.display='none'; } function ventasToggleCal(){ const drop=document.getElementById('ventasCalDrop'); if(!drop) return; const visible=drop.style.display==='block'; drop.style.display=visible?'none':'block'; if(!visible){ if(!_ventasCalMonth1) _ventasCalMonth1=new Date(); ventasRenderCal(); } } function ventasCalClear(){ _ventasFrom=null; _ventasTo=null; _ventasPeriod='all'; _ventasCalPickingFrom=true; ventasRenderCal(); ventasUpdateLabel(); ventasHighlightQuick('all'); ventasRender(); } function ventasCalApply(){ const drop=document.getElementById('ventasCalDrop'); if(drop) drop.style.display='none'; ventasRender(); } function ventasQuick(period){ _ventasPeriod=period; _ventasFrom=null; _ventasTo=null; const now=new Date(); if(period==='month'){ _ventasFrom=now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0')+'-01'; _ventasTo=now.toISOString().slice(0,10); } else if(period==='quarter'){ const q=Math.floor(now.getMonth()/3); const qStart=new Date(now.getFullYear(),q*3,1); _ventasFrom=qStart.toISOString().slice(0,10); _ventasTo=now.toISOString().slice(0,10); } else if(period==='year'){ _ventasFrom=now.getFullYear()+'-01-01'; _ventasTo=now.toISOString().slice(0,10); } ventasHighlightQuick(period); ventasUpdateLabel(); ventasRenderCal(); ventasRender(); } function ventasHighlightQuick(active){ ['all','month','quarter','year'].forEach(k=>{ const btn=document.getElementById('vq'+k.charAt(0).toUpperCase()+k.slice(1)); if(!btn) return; btn.style.background=k===active?'rgba(10,186,181,.4)':'none'; btn.style.color=k===active?'#fff':'rgba(255,255,255,.7)'; }); } function ventasUpdateLabel(){ const lbl=document.getElementById('ventasCalLabel'); if(!lbl) return; if(_ventasFrom&&_ventasTo) lbl.textContent=_ventasFrom+' → '+_ventasTo; else if(_ventasFrom) lbl.textContent=_ventasFrom+' → ?'; else lbl.textContent='Rango personalizado'; const rl=document.getElementById('ventasRangeLabel'); if(!rl) return; if(_ventasFrom&&_ventasTo) rl.textContent='Del '+_ventasFrom+' al '+_ventasTo; else if(_ventasFrom) rl.textContent='Desde '+_ventasFrom+' — selecciona fecha fin'; else rl.textContent='Selecciona fecha inicio y fin'; } function ventasPickDay(dateStr){ if(_ventasCalPickingFrom||!_ventasFrom){ _ventasFrom=dateStr; _ventasTo=null; _ventasCalPickingFrom=false; _ventasPeriod='custom'; } else { if(dateStr<_ventasFrom){ _ventasTo=_ventasFrom; _ventasFrom=dateStr; } else _ventasTo=dateStr; _ventasCalPickingFrom=true; _ventasPeriod='custom'; } ventasHighlightQuick(''); ventasUpdateLabel(); ventasRenderCal(); } function ventasRenderCal(){ if(!_ventasCalMonth1) _ventasCalMonth1=new Date(); const m2=new Date(_ventasCalMonth1.getFullYear(),_ventasCalMonth1.getMonth()+1,1); document.getElementById('ventasCal1').innerHTML=ventasCalHTML(_ventasCalMonth1,-1); document.getElementById('ventasCal2').innerHTML=ventasCalHTML(m2,1); } function ventasCalHTML(monthDate, nav){ const y=monthDate.getFullYear(), m=monthDate.getMonth(); const monthNames=['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre']; const days=['Do','Lu','Ma','Mi','Ju','Vi','Sá']; const firstDay=new Date(y,m,1).getDay(); const daysInMonth=new Date(y,m+1,0).getDate(); let html='
' +'
' +(nav===-1?'':'') +''+monthNames[m]+' '+y+'' +(nav===1?'':'') +'
' +'
' +days.map(d=>'
'+d+'
').join('') +'
' +'
' +'
'; for(let d=1;d<=daysInMonth;d++){ const ds=y+'-'+String(m+1).padStart(2,'0')+'-'+String(d).padStart(2,'0'); const isFrom=ds===_ventasFrom, isTo=ds===_ventasTo; const inRange=_ventasFrom&&_ventasTo&&ds>_ventasFrom&&ds<_ventasTo; const today=new Date().toISOString().slice(0,10); const isToday=ds===today; let bg='none', color='var(--blk)', fw='400', border='none', borderRadius='0'; if(isFrom||isTo){ bg='var(--t)'; color='#fff'; fw='700'; borderRadius='2px'; } else if(inRange){ bg='rgba(10,186,181,.12)'; } else if(isToday){ border='1px solid var(--t)'; borderRadius='2px'; } html+='
'+d+'
'; } html+='
'; return html; } function ventasCalNav(dir){ if(!_ventasCalMonth1) _ventasCalMonth1=new Date(); _ventasCalMonth1=new Date(_ventasCalMonth1.getFullYear(),_ventasCalMonth1.getMonth()+dir,1); ventasRenderCal(); } function ventasFilterOrders(){ const delivered=orders.filter(o=>o.archived&&o.status==='entregado'); if(!_ventasFrom&&!_ventasTo&&_ventasPeriod==='all') return delivered; const from=_ventasFrom||null, to=_ventasTo||null; return delivered.filter(o=>{ const d=o.ts||''; if(!d) return true; if(from&&dto) return false; return true; }); } function ventasRender(){ const el=document.getElementById('ventasBody'); if(!el) return; ordenesLoad(); const ventas=ventasFilterOrders(); const IVA_RATE=0.16, ISR_RATE=0.0125; if(!ventas.length){ el.innerHTML='
No hay ventas en este período.
'; return; } let totalBruto=0,totalIVA=0,totalISR=0; ventas.forEach(o=>{ totalBruto+=parseFloat(o.total)||0; totalIVA+=parseFloat(o.tax)||0; totalISR+=((parseFloat(o.subtotal)||0)+(parseFloat(o.shipCost)||0))*ISR_RATE; }); const totalNeto=totalBruto-totalIVA-totalISR; const fmt=n=>'$'+n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,','); const byMonth={}; ventas.forEach(o=>{ const key=o.ts?o.ts.slice(0,7):'Sin fecha'; if(!byMonth[key]) byMonth[key]=[]; byMonth[key].push(o); }); const monthRows=Object.keys(byMonth).sort().reverse().map(month=>{ const mos=byMonth[month]; let mBruto=0,mIVA=0,mISR=0; mos.forEach(o=>{mBruto+=parseFloat(o.total)||0;mIVA+=parseFloat(o.tax)||0;mISR+=((parseFloat(o.subtotal)||0)+(parseFloat(o.shipCost)||0))*ISR_RATE;}); const mNeto=mBruto-mIVA-mISR; const parts=month.split('-'); const label=month==='Sin fecha'?'Sin fecha':new Date(parseInt(parts[0]),parseInt(parts[1])-1,1).toLocaleDateString('es-MX',{month:'long',year:'numeric'}); const rowsHtml=mos.map(o=> '' +''+esc(o.date||'')+'' +'#'+esc(o.id)+'' +''+esc(((o.shipping||{}).firstName||'')+' '+((o.shipping||{}).lastName||''))+'' +''+fmt(parseFloat(o.subtotal)||0)+'' +''+fmt(parseFloat(o.tax)||0)+'' +''+fmt(((parseFloat(o.subtotal)||0)+(parseFloat(o.shipCost)||0))*ISR_RATE)+'' +''+fmt(parseFloat(o.total)||0)+'' +''+fmt((parseFloat(o.total)||0)-(parseFloat(o.tax)||0)-((parseFloat(o.subtotal)||0)+(parseFloat(o.shipCost)||0))*ISR_RATE)+'' +'' ).join(''); return '
' +'
' +''+label+'' +'
' +'IVA: '+fmt(mIVA)+'' +'ISR: '+fmt(mISR)+'' +'Neto: '+fmt(mNeto)+'' +'
' +'
' +'' +'' +'' +'' +'' +'' +'' +'' +'' +'' +'' +''+rowsHtml+'' +'
FechaOrdenCompradorSubtotalIVA 16%ISR 1.25%TotalNeto
' +'
'; }).join(''); el.innerHTML= '
' +'
' +'
Ingresos Totales
' +'
'+fmt(totalBruto)+'
' +'
'+ventas.length+' venta'+(ventas.length!==1?'s':'')+'
' +'
' +'
' +'
IVA a Pagar (16%)
' +'
'+fmt(totalIVA)+'
' +'
Declarar ante el SAT
' +'
' +'
' +'
ISR Estimado (1.25%)
' +'
'+fmt(totalISR)+'
' +'
Retención aprox.
' +'
' +'
' +'
Ingreso Neto
' +'
'+fmt(totalNeto)+'
' +'
Después de impuestos
' +'
' +'
' +'
' +'⚠️ Nota fiscal: Los montos mostrados son estimados para el régimen simplificado de confianza (RESICO) en México. ' +'IVA 16% trasladado al consumidor final. ISR 1.25% retención aproximada sobre ingresos. ' +'Consulta a tu contador para cifras exactas y declaraciones oficiales ante el SAT.' +'
' +monthRows; } function openUsuariosPanel(){ if(currentUserRole!=='administrador'){ alert('Solo los administradores pueden gestionar usuarios.'); return; } adminUsersLoad(); renderUsuariosList(); document.getElementById('usuariosModal').style.display='flex'; } function closeUsuariosPanel(){ document.getElementById('usuariosModal').style.display='none'; } function renderUsuariosList(){ const el=document.getElementById('usuariosList'); if(!el) return; const isPrimAdmin = currentUserRole==='administrador'; if(!adminUsers.length){ el.innerHTML='

No hay usuarios adicionales.

'; return; } let html='
Usuarios Registrados
'; adminUsers.forEach(function(u,i){ const role=u.role||'administrador'; const roleColor=role==='administrador'?'var(--t)':role==='supervisor'?'#8e44ad':'#e67e22'; const roleLabel=role==='administrador'?'Administrador':role==='supervisor'?'Supervisor':'Vendedor'; const infoLine=(u.email||'')+(u.email&&u.phone?' · ':'')+(u.phone||''); let actions=''; if(isPrimAdmin){ const selA=role==='administrador'?' selected':''; const selS=role==='supervisor'?' selected':''; actions='' +''; } else { actions=''+roleLabel+''; } html+='
' +'
' +'
'+esc(u.first)+' '+esc(u.last)+'
' +'
@'+esc(u.user)+'
' +'
'+esc(infoLine)+'
' +'
' +'
'+actions+'
' +'
'; }); el.innerHTML=html; } function changeUserRole(i, role){ adminUsersLoad(); if(!adminUsers[i]) return; adminUsers[i].role = role; adminUsersSave(); renderUsuariosList(); } function saveNewAdminUser(){ if(currentUserRole!=='administrador'){ alert('Solo los administradores pueden crear usuarios.'); return; } const user=(document.getElementById('nu_user').value||'').trim(); const pass=(document.getElementById('nu_pass').value||'').trim(); const first=(document.getElementById('nu_first').value||'').trim(); const last=(document.getElementById('nu_last').value||'').trim(); const email=(document.getElementById('nu_email').value||'').trim(); const phone=(document.getElementById('nu_phone').value||'').trim(); const role=document.getElementById('nu_role').value||'administrador'; const err=document.getElementById('nu_err'); if(!user||!pass||!first||!last){ err.textContent='Usuario, contraseña, nombre y apellido son requeridos.'; err.style.display='block'; return; } adminUsersLoad(); if(user===ADMIN.user||adminUsers.some(a=>a.user===user)){ err.textContent='Ese nombre de usuario ya existe.'; err.style.display='block'; return; } adminUsers.push({user,pass,first,last,email,phone,role}); adminUsersSave(); err.style.display='none'; ['nu_user','nu_pass','nu_first','nu_last','nu_email','nu_phone'].forEach(id=>document.getElementById(id).value=''); document.getElementById('nu_role').value='administrador'; renderUsuariosList(); } function deleteAdminUser(i){ if(currentUserRole!=='administrador'){ alert('Solo los administradores pueden eliminar usuarios.'); return; } adminUsersLoad(); if(!confirm('\u00bfEliminar usuario "'+adminUsers[i].user+'"?')) return; adminUsers.splice(i,1); adminUsersSave(); renderUsuariosList(); }