Portafolio LATAM Code Labs
VinoTeca
Buscar Vino
Historial de Ventas
FechaClienteItemsTotalPago
Carrito
Total $0

Eventos de Cata

Wine Club Members

MiembroTierPreferenciaAsignaciónDirecciónEstado PagoAcciones

Proveedores & Distribuidores

Top 10 Vinos Más Vendidos
Revenue por Categoría
Añejamiento (Vinos por Añada)
Inventario por País
`); w.document.close(); w.print(); } function renderSalesHistory() { const el = document.getElementById('salesHistory'); const recent = [...DATA.sales].reverse().slice(0, 20); el.innerHTML = recent.map(s => ` ${s.date} ${s.customer} ${s.items.map(i => `${i.name} x${i.qty}`).join(', ')} ${fmt(s.total)} ${s.payment} `).join(''); } // ==================== TASTINGS ==================== function renderTastings() { const el = document.getElementById('tastingsGrid'); el.innerHTML = DATA.tastings.map(t => { const spotsLeft = t.capacity - t.attendees; const pct = Math.round(t.attendees / t.capacity * 100); const themeIcon = { 'Cata a Ciegas': 'fa-eye-slash', 'Región': 'fa-map-marker-alt', 'Varietal': 'fa-seedling', 'Maridaje': 'fa-utensils' }[t.theme] || 'fa-wine-glass'; return `
${t.date}
${t.name}
${t.theme}
Inscritos: ${t.attendees}/${t.capacity} (${pct}%)
${spotsLeft > 0 ? spotsLeft + ' lugares disponibles' : 'AGOTADO'}
Vinos: ${t.wines}
${t.notes}
${fmt(t.price)} /persona Revenue potencial: ${fmt(t.price * t.capacity)}
`; }).join(''); } function openTastingModal(t) { document.getElementById('tastingEditId').value = t ? t.id : ''; document.getElementById('tastingModalTitle').textContent = t ? 'Editar Cata' : 'Nueva Cata'; document.getElementById('tName').value = t ? t.name : ''; document.getElementById('tDate').value = t ? t.date : ''; document.getElementById('tTheme').value = t ? t.theme : 'Cata a Ciegas'; document.getElementById('tCapacity').value = t ? t.capacity : 20; document.getElementById('tPrice').value = t ? t.price : 45; document.getElementById('tAttendees').value = t ? t.attendees : 0; document.getElementById('tWines').value = t ? t.wines : ''; document.getElementById('tNotes').value = t ? t.notes : ''; openModal('tastingModal'); } function saveTasting() { const id = document.getElementById('tastingEditId').value; const t = { id: id || uid(), name: document.getElementById('tName').value.trim(), date: document.getElementById('tDate').value, theme: document.getElementById('tTheme').value, capacity: parseInt(document.getElementById('tCapacity').value), attendees: parseInt(document.getElementById('tAttendees').value), price: parseFloat(document.getElementById('tPrice').value), wines: document.getElementById('tWines').value.trim(), notes: document.getElementById('tNotes').value.trim(), }; if (!t.name || !t.date) return alert('Nombre y fecha son requeridos.'); if (id) { const idx = DATA.tastings.findIndex(x => x.id === id); if (idx >= 0) DATA.tastings[idx] = t; } else { DATA.tastings.push(t); } saveData(DATA); closeModal('tastingModal'); renderTastings(); } function editTasting(id) { const t = DATA.tastings.find(x => x.id === id); if (t) openTastingModal(t); } function deleteTasting(id) { if (!confirm('¿Eliminar esta cata?')) return; DATA.tastings = DATA.tastings.filter(x => x.id !== id); saveData(DATA); renderTastings(); } // ==================== WINE CLUB ==================== function renderClub() { const tiers = {Básico:0,Premium:0,'Grand Cru':0}; DATA.members.forEach(m => { if (tiers[m.tier] !== undefined) tiers[m.tier]++; }); const alDia = DATA.members.filter(m => m.payment === 'Al día').length; const totalBottles = DATA.members.reduce((s,m) => s + m.allocation, 0); document.getElementById('clubStats').innerHTML = `
Miembros
${DATA.members.length}
Grand Cru
${tiers['Grand Cru']}
Botellas/Mes
${totalBottles}
Pagos Al Día
${Math.round(alDia/DATA.members.length*100)}%
`; document.getElementById('clubTable').innerHTML = DATA.members.map(m => `
${m.name}
${m.email}
${m.tier} ${m.preference} ${m.allocation} botellas/mes ${m.address} ${paymentBadge(m.payment)}
`).join(''); } function openMemberModal(m) { document.getElementById('memberEditId').value = m ? m.id : ''; document.getElementById('memberModalTitle').textContent = m ? 'Editar Miembro' : 'Nuevo Miembro'; document.getElementById('mName').value = m ? m.name : ''; document.getElementById('mEmail').value = m ? m.email : ''; document.getElementById('mTier').value = m ? m.tier : 'Básico'; document.getElementById('mPref').value = m ? m.preference : 'Tinto'; document.getElementById('mAlloc').value = m ? m.allocation : 2; document.getElementById('mPayment').value = m ? m.payment : 'Al día'; document.getElementById('mAddress').value = m ? m.address : ''; openModal('memberModal'); } function saveMember() { const id = document.getElementById('memberEditId').value; const m = { id: id || uid(), name: document.getElementById('mName').value.trim(), email: document.getElementById('mEmail').value.trim(), tier: document.getElementById('mTier').value, preference: document.getElementById('mPref').value, allocation: parseInt(document.getElementById('mAlloc').value), payment: document.getElementById('mPayment').value, address: document.getElementById('mAddress').value.trim(), }; if (!m.name) return alert('El nombre es requerido.'); if (id) { const idx = DATA.members.findIndex(x => x.id === id); if (idx >= 0) DATA.members[idx] = m; } else { DATA.members.push(m); } saveData(DATA); closeModal('memberModal'); renderClub(); } function editMember(id) { const m = DATA.members.find(x => x.id === id); if (m) openMemberModal(m); } function deleteMember(id) { if (!confirm('¿Eliminar este miembro?')) return; DATA.members = DATA.members.filter(x => x.id !== id); saveData(DATA); renderClub(); } // ==================== SUPPLIERS ==================== function renderSuppliers() { document.getElementById('suppliersGrid').innerHTML = DATA.suppliers.map(s => `
${s.name}
${s.contact} · ${s.email} · ${s.phone}
Portafolio
${s.portfolio}
Lead Time
${s.leadTime} días
MOQ
${s.moq} cajas
Órdenes Pendientes
${s.pendingOrders}
`).join(''); } function openSupplierModal(s) { document.getElementById('supplierEditId').value = s ? s.id : ''; document.getElementById('supplierModalTitle').textContent = s ? 'Editar Proveedor' : 'Nuevo Proveedor'; document.getElementById('sName').value = s ? s.name : ''; document.getElementById('sContact').value = s ? s.contact : ''; document.getElementById('sEmail').value = s ? s.email : ''; document.getElementById('sPhone').value = s ? s.phone : ''; document.getElementById('sPortfolio').value = s ? s.portfolio : ''; document.getElementById('sLead').value = s ? s.leadTime : 14; document.getElementById('sMoq').value = s ? s.moq : 10; document.getElementById('sOrders').value = s ? s.pendingOrders : 0; openModal('supplierModal'); } function saveSupplier() { const id = document.getElementById('supplierEditId').value; const s = { id: id || uid(), name: document.getElementById('sName').value.trim(), contact: document.getElementById('sContact').value.trim(), email: document.getElementById('sEmail').value.trim(), phone: document.getElementById('sPhone').value.trim(), portfolio: document.getElementById('sPortfolio').value.trim(), leadTime: parseInt(document.getElementById('sLead').value), moq: parseInt(document.getElementById('sMoq').value), pendingOrders: parseInt(document.getElementById('sOrders').value), }; if (!s.name) return alert('El nombre es requerido.'); if (id) { const idx = DATA.suppliers.findIndex(x => x.id === id); if (idx >= 0) DATA.suppliers[idx] = s; } else { DATA.suppliers.push(s); } saveData(DATA); closeModal('supplierModal'); renderSuppliers(); } function editSupplier(id) { const s = DATA.suppliers.find(x => x.id === id); if (s) openSupplierModal(s); } function deleteSupplier(id) { if (!confirm('¿Eliminar este proveedor?')) return; DATA.suppliers = DATA.suppliers.filter(x => x.id !== id); saveData(DATA); renderSuppliers(); } // ==================== ANALYTICS ==================== function renderAnalytics() { // Stats const totalRevenue = DATA.sales.reduce((s,sale) => s + sale.total, 0); const avgTicket = DATA.sales.length ? totalRevenue / DATA.sales.length : 0; const totalBottlesSold = DATA.sales.reduce((s,sale) => s + sale.items.reduce((ss,i) => ss + i.qty, 0), 0); const inventoryValue = DATA.wines.reduce((s,w) => s + w.price * w.stock, 0); document.getElementById('analyticsStats').innerHTML = `
Revenue Total
${fmt(totalRevenue)}
${DATA.sales.length} ventas
Ticket Promedio
${fmt(avgTicket)}
Botellas Vendidas
${fmtInt(totalBottlesSold)}
Valor Inventario
${fmt(inventoryValue)}
`; // Top sellers const soldMap = {}; DATA.sales.forEach(sale => { sale.items.forEach(i => { soldMap[i.name] = (soldMap[i.name] || 0) + i.qty; }); }); const topSellers = Object.entries(soldMap).sort((a,b) => b[1] - a[1]).slice(0, 10); const maxSold = topSellers.length ? topSellers[0][1] : 1; document.getElementById('topSellers').innerHTML = topSellers.length ? `
${topSellers.map(([name,qty]) => `
${name}
${qty}
`).join('')}
` : '
Sin datos de ventas aún
'; // Revenue by category const catRevenue = {Tinto:0,Blanco:0,Rosado:0,Espumante:0}; DATA.sales.forEach(sale => { sale.items.forEach(i => { const wine = DATA.wines.find(w => w.name === i.name); if (wine) catRevenue[wine.type] = (catRevenue[wine.type] || 0) + i.price * i.qty; }); }); const totalCatRev = Object.values(catRevenue).reduce((a,b) => a+b, 0) || 1; document.getElementById('revenueByCategory').innerHTML = `
${Object.entries(catRevenue).map(([cat,rev]) => `
${cat}
${fmt(rev)}
`).join('')}
Tinto
Blanco
Rosado
Espumante
`; // Aging chart const vintageMap = {}; DATA.wines.forEach(w => { vintageMap[w.year] = (vintageMap[w.year] || 0) + w.stock; }); const vintages = Object.entries(vintageMap).sort((a,b) => a[0] - b[0]); const maxVintage = Math.max(...vintages.map(v => v[1]), 1); document.getElementById('agingChart').innerHTML = `
${vintages.map(([year,count]) => `
${year}
${count} bot.
`).join('')}
`; // Country chart const countryMap = {}; DATA.wines.forEach(w => { countryMap[w.country] = (countryMap[w.country] || 0) + w.stock; }); const countries = Object.entries(countryMap).sort((a,b) => b[1] - a[1]); const maxCountry = Math.max(...countries.map(c => c[1]), 1); const countryColors = {Argentina:'tinto',Chile:'blanco',Uruguay:'espumante',España:'rosado',Francia:'tinto',Italia:'blanco'}; document.getElementById('countryChart').innerHTML = `
${countries.map(([c,count]) => `
${c}
${count} bot.
`).join('')}
`; } // ==================== RENDER ALL ==================== function renderAll() { renderCellar(); renderCart(); renderPosSearch(); renderSalesHistory(); renderTastings(); renderClub(); renderSuppliers(); renderAnalytics(); } renderAll();