PASSION PARFUM

ESPACE PRIVÉ

PASSION PARFUM

INVENTAIRE LIVE • MAJ:
£1=
Marques


'; return h; } // =========================== // RENDERING // =========================== function renderAll() { renderStats(); renderBrandToggles(); if (S.tab === 'inventory') renderInventory(); else if (S.tab === 'restock') renderRestock(); else if (S.tab === 'summary') renderSummary(); else if (S.tab === 'history') renderHistory(); else if (S.tab === 'alerts') renderAlerts(); updateBadges(); } function renderStats() { const active = S.items.filter(i => S.activeBrands.has(i.brand) && !S.hiddenProducts.has(i.title)); let refs = active.length, inStock = 0, oos = 0, units = 0, stockVal = 0; active.forEach(i => { if (i.qty > 0) inStock++; else oos++; units += i.qty; const c = getCost(i); if (c) stockVal += c.cost * i.qty; }); const restockCount = Object.values(S.restock).reduce((s, v) => s + v, 0); const restockVal = getRestockItems().reduce((s, ri) => { const c = getCost(ri.item); return s + (c ? c.cost * ri.qty : 0); }, 0); const cards = [ { l: 'R\u00e9fs', v: refs, c: 'var(--text)' }, { l: 'En stock', v: inStock, c: 'var(--success)' }, { l: 'Rupture', v: oos, c: 'var(--danger)' }, { l: 'Unit\u00e9s', v: units, c: 'var(--gold)' }, { l: 'Valeur stock', v: stockVal.toFixed(0) + '\u20AC', c: 'var(--gold)' }, ]; if (restockCount > 0) cards.push({ l: 'Panier restock', v: restockVal.toFixed(0) + '\u20AC', c: 'var(--info)' }); $('stats-bar').innerHTML = cards.map(s => '
' + s.v + '
' + s.l + '
').join(''); } function renderBrandToggles() { $('brand-toggles').innerHTML = BRANDS.map(b => { const on = S.activeBrands.has(b.id); const cnt = S.items.filter(i => i.brand === b.id).length; if (cnt === 0) return ''; return ''; }).join(''); $('brand-toggles').querySelectorAll('.brand-pill').forEach(btn => { btn.addEventListener('click', () => { const bid = btn.dataset.brand; if (S.activeBrands.has(bid)) S.activeBrands.delete(bid); else S.activeBrands.add(bid); renderAll(); }); }); } function getFilteredItems() { return S.items.filter(i => { if (!S.activeBrands.has(i.brand)) return false; if (!S.showHidden && S.hiddenProducts.has(i.title)) return false; if (S.filterBrand !== 'all' && i.brand !== S.filterBrand) return false; if (S.filterStock === 'instock' && i.qty === 0) return false; if (S.filterStock === 'oos' && i.qty > 0) return false; if (S.filterStock === 'low' && (i.qty === 0 || i.qty > getThreshold(i.brand))) return false; if (S.filterSize !== 'all' && !i.size.includes(S.filterSize)) return false; if (S.search && !i.title.toLowerCase().includes(S.search.toLowerCase()) && !i.sku.toLowerCase().includes(S.search.toLowerCase())) return false; return true; }); } function sortItems(items) { const { key, dir } = S.sort; const m = dir === 'asc' ? 1 : -1; return [...items].sort((a, b) => { let va, vb; if (key === 'title') { va = a.title; vb = b.title; } else if (key === 'size') { va = a.size; vb = b.size; } else if (key === 'brand') { va = a.brand; vb = b.brand; } else if (key === 'stock') { va = a.qty; vb = b.qty; } else if (key === 'cost') { const ca = getCost(a); const cb = getCost(b); va = ca ? ca.cost : -1; vb = cb ? cb.cost : -1; } else if (key === 'pvc') { va = getRetailPrice(a); vb = getRetailPrice(b); } else if (key === 'margin') { const ca = getCost(a), pa = getRetailPrice(a); const cb = getCost(b), pb = getRetailPrice(b); va = ca && pa ? (pa - ca.cost) / pa : -1; vb = cb && pb ? (pb - cb.cost) / pb : -1; } else if (key === 'sku') { va = a.sku; vb = b.sku; } else { va = a.title; vb = b.title; } if (typeof va === 'string') return va.localeCompare(vb) * m; return (va - vb) * m; }); } function renderInventory() { const cols = [ { key: 'title', label: 'Produit', align: 'left' }, { key: 'size', label: 'Taille', align: 'center' }, { key: 'sku', label: 'SKU', align: 'left' }, { key: 'brand', label: 'Marque', align: 'center' }, { key: 'stock', label: 'Stock', align: 'center' }, { key: 'cost', label: 'Achat', align: 'center' }, { key: 'pvc', label: 'PVC', align: 'center' }, { key: 'margin', label: 'Marge', align: 'center' }, { key: '_restock', label: 'Restock', align: 'center' }, ]; // Populate size filter const sizes = [...new Set(S.items.map(i => i.size))].filter(s => s && s !== '1pc' && s !== 'kit').sort(); const sizeSelect = $('filter-size'); const curSize = sizeSelect.value; sizeSelect.innerHTML = '' + sizes.map(s => '').join(''); // Populate brand filter const brandSelect = $('filter-brand'); const curBrand = brandSelect.value; brandSelect.innerHTML = '' + BRANDS.filter(b => S.items.some(i => i.brand === b.id)).map(b => '').join(''); // Header $('inv-thead').innerHTML = cols.map(c => { const active = S.sort.key === c.key; const arrow = c.key === '_restock' ? '' : '' + (active ? (S.sort.dir === 'asc' ? '\u25B2' : '\u25BC') : '\u25B2') + ''; return '' + c.label + arrow + ''; }).join(''); // Attach sort listeners $('inv-thead').querySelectorAll('th[data-sort]').forEach(th => { th.addEventListener('click', () => { const k = th.dataset.sort; if (k === '_restock') return; if (S.sort.key === k) S.sort.dir = S.sort.dir === 'asc' ? 'desc' : 'asc'; else { S.sort.key = k; S.sort.dir = 'asc'; } renderInventory(); }); }); const filtered = sortItems(getFilteredItems()); $('result-count').textContent = filtered.length + ' r\u00e9f\u00e9rences'; // Body const tbody = $('inv-tbody'); tbody.innerHTML = ''; filtered.forEach((it, idx) => { const isH = S.hiddenProducts.has(it.title); const costInfo = getCost(it); const pvc = getRetailPrice(it) || (costInfo && costInfo.pvc ? costInfo.pvc : null); const margin = costInfo && pvc ? ((pvc - costInfo.cost) / pvc * 100).toFixed(0) : null; const bObj = BRAND_MAP[it.brand]; const qty = S.restock[it.vid] || 0; const threshold = getThreshold(it.brand); const stockCls = it.qty === 0 ? 'stock-out' : it.qty <= threshold ? 'stock-low' : 'stock-ok'; const tr = document.createElement('tr'); tr.className = (idx % 2 === 0 ? 'even' : 'odd') + (isH ? ' hidden-row' : ''); tr.innerHTML = '' + it.title + (bObj ? '' + bObj.name + '' : '') + '' + '' + it.size + '' + '' + (it.sku || '\u2014') + '' + '' + it.brand + '' + '' + it.qty + '' + '' + (costInfo ? costInfo.cost.toFixed(2) + '\u20AC' + (costInfo.gbp ? ' (\u00A3' + costInfo.gbp.toFixed(2) + ')' : '') : '\u2014') + '' + '' + (pvc ? pvc.toFixed(0) + '\u20AC' : '\u2014') + '' + '' + (margin ? '' + margin + '%' : '\u2014') + '' + '' + (costInfo && !isH ? '
' : '\u2014') + ''; tbody.appendChild(tr); }); // Restock interaction tbody.querySelectorAll('.qty-btn').forEach(btn => { btn.addEventListener('click', () => { const vid = btn.dataset.vid; const delta = parseInt(btn.dataset.delta); S.restock[vid] = Math.max(0, (S.restock[vid] || 0) + delta); if (S.restock[vid] === 0) delete S.restock[vid]; renderAll(); }); }); tbody.querySelectorAll('.qty-input').forEach(inp => { inp.addEventListener('change', () => { const vid = inp.dataset.vid; const val = Math.max(0, parseInt(inp.value) || 0); if (val === 0) delete S.restock[vid]; else S.restock[vid] = val; renderAll(); }); }); } function getRestockItems() { return Object.entries(S.restock).filter(([, v]) => v > 0).map(([vid, qty]) => { const item = S.items.find(i => String(i.vid) === String(vid)); return item ? { item, qty } : null; }).filter(Boolean); } function renderRestock() { const ris = getRestockItems(); const container = $('restock-content'); if (ris.length === 0) { container.innerHTML = '
\uD83D\uDED2
Ajoutez des quantit\u00e9s depuis l\'onglet Inventaire
'; return; } const brandGroups = {}; ris.forEach(ri => { if (!brandGroups[ri.item.brand]) brandGroups[ri.item.brand] = []; brandGroups[ri.item.brand].push(ri); }); let totalCost = 0, totalPvc = 0, totalQty = 0; ris.forEach(ri => { const c = getCost(ri.item); totalQty += ri.qty; if (c) totalCost += c.cost * ri.qty; const p = getRetailPrice(ri.item) || (c && c.pvc ? c.pvc : 0); if (p) totalPvc += p * ri.qty; }); let html = '
'; html += '
Panier restock'; html += '\u00A31=' + S.rate + '\u20AC'; html += '' + (S.mode === 'standard' ? '\u221240%' : '\u221250%') + '
'; html += '
'; Object.entries(brandGroups).forEach(([brand, items]) => { const bObj = BRAND_MAP[brand]; const bCost = items.reduce((s, ri) => { const c = getCost(ri.item); return s + (c ? c.cost * ri.qty : 0); }, 0); const bQty = items.reduce((s, ri) => s + ri.qty, 0); html += '
'; html += '
' + (bObj ? bObj.name : brand) + ''; html += '' + bQty + ' pcs \u00B7 ' + bCost.toFixed(2) + '\u20AC
'; html += '
'; html += ''; html += ''; html += '
'; html += ''; items.forEach((ri, i) => { const costInfo = getCost(ri.item); const unit = costInfo ? costInfo.cost : 0; const sub = unit * ri.qty; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
' + ri.item.title + '' + ri.item.size + '
' + (costInfo && costInfo.gbp ? '\u00A3' + costInfo.gbp.toFixed(2) : '') + '' + (unit ? unit.toFixed(2) + '\u20AC' : '\u2014') + '' + (sub ? sub.toFixed(2) + '\u20AC' : '\u2014') + '
'; }); // Summary card html += '
'; html += '
Articles
' + totalQty + '
'; html += '
Co\u00fbt achat
' + totalCost.toFixed(2) + '\u20AC
'; html += '
Valeur PVC
' + (totalPvc > 0 ? totalPvc.toFixed(0) + '\u20AC' : 'N/A') + '
'; html += '
Marge
' + (totalPvc > 0 ? ((totalPvc - totalCost) / totalPvc * 100).toFixed(0) + '%' : '\u2014') + '
' + (totalPvc > 0 ? '
+' + (totalPvc - totalCost).toFixed(0) + '\u20AC
' : '') + '
'; html += '
'; container.innerHTML = html; // Events container.querySelector('#clear-restock')?.addEventListener('click', () => { S.restock = {}; renderAll(); }); container.querySelectorAll('.restock-qty-btn').forEach(btn => { btn.addEventListener('click', () => { const vid = btn.dataset.vid; const delta = parseInt(btn.dataset.delta); S.restock[vid] = Math.max(0, (S.restock[vid] || 0) + delta); if (S.restock[vid] === 0) delete S.restock[vid]; renderAll(); }); }); container.querySelectorAll('.restock-qty-inp').forEach(inp => { inp.addEventListener('change', () => { const vid = inp.dataset.vid; const val = Math.max(0, parseInt(inp.value) || 0); if (val === 0) delete S.restock[vid]; else S.restock[vid] = val; renderAll(); }); }); container.querySelectorAll('.restock-rm').forEach(btn => { btn.addEventListener('click', () => { delete S.restock[btn.dataset.vid]; renderAll(); }); }); container.querySelectorAll('.restock-export').forEach(btn => { btn.addEventListener('click', () => { const brand = btn.dataset.brand; const items = getRestockItems().filter(ri => ri.item.brand === brand); if (!items.length) return; const w = window.open('', '_blank'); if (w) { w.document.write(buildPurchaseOrderHTML(brand, items)); w.document.close(); } }); }); container.querySelectorAll('.restock-copy').forEach(btn => { btn.addEventListener('click', () => { const brand = btn.dataset.brand; const items = getRestockItems().filter(ri => ri.item.brand === brand); if (!items.length) return; const bObj = BRAND_MAP[brand]; const sup = SUPPLIERS[brand] || {}; const cur = bObj && bObj.currency === 'GBP' ? '\u00A3' : '\u20AC'; let txt = 'PURCHASE ORDER \u2014 Passion Parfum Project\nDate: ' + new Date().toLocaleDateString('fr-FR') + '\nTo: ' + (sup.name || (bObj ? bObj.name : brand)) + '\n\n'; let tV = 0; items.forEach(ri => { const c = getCost(ri.item); const unit = c ? c.cost : 0; const sub = unit * ri.qty; tV += sub; txt += ri.item.title + ' | ' + ri.item.size + ' | x' + ri.qty + ' | ' + (unit ? cur + unit.toFixed(2) : '\u2014') + ' | ' + (sub ? cur + sub.toFixed(2) : '\u2014') + '\n'; }); txt += '\nTOTAL: ' + items.reduce((s, ri) => s + ri.qty, 0) + ' pcs \u2014 ' + cur + tV.toFixed(2); txt += '\n\nPassion Parfum Project\nPierre-Yves Jean\n26 Rue de la grande Bri\u00e8re, 78180 Montigny-le-Bretonneux\nSIRET: 93090392700013 | TVA: FR22930903927'; navigator.clipboard.writeText(txt).then(() => toast('Commande copi\u00e9e !')); }); }); container.querySelectorAll('.restock-csv').forEach(btn => { btn.addEventListener('click', () => { const brand = btn.dataset.brand; const items = getRestockItems().filter(ri => ri.item.brand === brand); if (!items.length) return; const headers = ['Marque', 'Produit', 'Taille', 'SKU', 'Qty', 'Prix unitaire EUR', 'Sous-total EUR']; const rows = items.map(ri => { const c = getCost(ri.item); return [(BRAND_MAP[brand] || {}).name || brand, ri.item.title, ri.item.size, ri.item.sku, ri.qty, c ? c.cost.toFixed(2) : '', c ? (c.cost * ri.qty).toFixed(2) : '']; }); exportCSV(rows, headers, 'restock-' + brand + '-' + new Date().toISOString().slice(0, 10) + '.csv'); }); }); } function renderSummary() { const container = $('summary-content'); const active = S.items.filter(i => S.activeBrands.has(i.brand) && !S.hiddenProducts.has(i.title)); let totalRefs = active.length, totalUnits = 0, totalCostVal = 0, totalPvcVal = 0; active.forEach(i => { totalUnits += i.qty; const c = getCost(i); if (c) totalCostVal += c.cost * i.qty; const p = getRetailPrice(i); if (p) totalPvcVal += p * i.qty; }); const avgMargin = totalPvcVal > 0 ? ((totalPvcVal - totalCostVal) / totalPvcVal * 100).toFixed(0) : '\u2014'; let html = '
'; html += '

Synth\u00e8se

'; html += '\u00A31=' + S.rate + '\u20AC'; html += '' + (S.mode === 'standard' ? '\u221240%' : '\u221250%') + ''; html += ''; html += '
'; // KPI cards html += '
'; html += '
R\u00e9f\u00e9rences
' + totalRefs + '
'; html += '
Unit\u00e9s en stock
' + totalUnits + '
'; html += '
Valeur au co\u00fbt
' + totalCostVal.toFixed(0) + '\u20AC
'; html += '
Valeur PVC
' + totalPvcVal.toFixed(0) + '\u20AC
'; html += '
Marge moyenne
' + avgMargin + (avgMargin !== '\u2014' ? '%' : '') + '
'; html += '
'; // Bar chart const brandData = BRANDS.filter(b => S.activeBrands.has(b.id)).map(b => { const bi = active.filter(i => i.brand === b.id); let val = 0; bi.forEach(i => { const c = getCost(i); if (c) val += c.cost * i.qty; }); return { ...b, refs: bi.length, units: bi.reduce((s, i) => s + i.qty, 0), costVal: val }; }).filter(b => b.refs > 0); const maxVal = Math.max(...brandData.map(b => b.costVal), 1); html += '
'; brandData.forEach(b => { const pct = (b.costVal / brandData.reduce((s, x) => s + x.costVal, 0) * 100) || 0; html += '
'; }); html += '
'; // Brand table html += ''; html += ''; html += ''; brandData.forEach((b, idx) => { let pvcVal = 0; active.filter(i => i.brand === b.id).forEach(i => { const p = getRetailPrice(i); if (p) pvcVal += p * i.qty; }); const mg = pvcVal > 0 ? ((pvcVal - b.costVal) / pvcVal * 100).toFixed(0) : '\u2014'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
MarqueSKUsUnit\u00e9sValeur co\u00fbtValeur PVCMarge
\u25CF ' + b.name + '' + b.refs + '' + b.units + '' + b.costVal.toFixed(0) + '\u20AC' + pvcVal.toFixed(0) + '\u20AC' + mg + (mg !== '\u2014' ? '%' : '') + '
'; container.innerHTML = html; container.querySelector('#export-summary')?.addEventListener('click', () => { const headers = ['Marque', 'R\u00e9fs', 'Unit\u00e9s', 'Valeur co\u00fbt EUR', 'Valeur PVC EUR', 'Marge %']; const rows = brandData.map(b => { let pvcVal = 0; active.filter(i => i.brand === b.id).forEach(i => { const p = getRetailPrice(i); if (p) pvcVal += p * i.qty; }); const mg = pvcVal > 0 ? ((pvcVal - b.costVal) / pvcVal * 100).toFixed(0) : ''; return [b.name, b.refs, b.units, b.costVal.toFixed(2), pvcVal.toFixed(2), mg]; }); exportCSV(rows, headers, 'synthese-' + new Date().toISOString().slice(0, 10) + '.csv'); }); } function renderHistory() { const container = $('history-content'); if (S.history.length < 2) { container.innerHTML = '
\uD83D\uDCC8
Pas assez de donn\u00e9es historiques. Revenez demain pour voir l\'\u00e9volution.
Un snapshot est sauvegard\u00e9 automatiquement \u00e0 chaque visite.
'; return; } const latest = S.history[0]; const prev = S.history[1]; const latestMap = {}; const prevMap = {}; latest.data.forEach(d => { latestMap[d.vid] = d.qty; }); prev.data.forEach(d => { prevMap[d.vid] = d.qty; }); const diffs = []; S.items.forEach(item => { const cur = latestMap[item.vid] !== undefined ? latestMap[item.vid] : item.qty; const old = prevMap[item.vid]; if (old !== undefined && old !== cur) { diffs.push({ item, cur, old, diff: cur - old }); } }); diffs.sort((a, b) => Math.abs(b.diff) - Math.abs(a.diff)); let html = '
'; html += '

Historique des stocks

'; html += '
'; html += '
'; html += 'Comparaison : ' + new Date(latest.date).toLocaleDateString('fr-FR') + ' vs ' + new Date(prev.date).toLocaleDateString('fr-FR') + ''; html += ' \u2014 ' + diffs.length + ' changement(s)
'; if (diffs.length === 0) { html += '
Aucun changement de stock d\u00e9tect\u00e9
'; } else { html += ''; html += ''; html += ''; diffs.forEach((d, idx) => { const bObj = BRAND_MAP[d.item.brand]; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
ProduitTailleMarqueAvantApr\u00e8sDiff
' + d.item.title + '' + d.item.size + '' + d.item.brand + '' + d.old + '' + d.cur + '' + (d.diff > 0 ? '+' : '') + d.diff + '
'; } // Snapshot list html += '

Snapshots sauvegard\u00e9s

'; html += '
'; S.history.slice(0, 30).forEach(h => { html += '
' + new Date(h.date).toLocaleDateString('fr-FR') + ' ' + new Date(h.date).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) + ' \u2014 ' + h.data.length + ' variantes
'; }); html += '
'; container.innerHTML = html; container.querySelector('#export-history')?.addEventListener('click', () => { if (diffs.length === 0) return toast('Aucun changement \u00e0 exporter'); const headers = ['Produit', 'Taille', 'Marque', 'Avant', 'Apr\u00e8s', 'Diff']; const rows = diffs.map(d => [d.item.title, d.item.size, d.item.brand, d.old, d.cur, d.diff]); exportCSV(rows, headers, 'historique-' + new Date().toISOString().slice(0, 10) + '.csv'); }); } function getAlerts() { const alerts = []; S.items.filter(i => S.activeBrands.has(i.brand) && !S.hiddenProducts.has(i.title)).forEach(i => { const threshold = getThreshold(i.brand); if (i.qty === 0) alerts.push({ item: i, type: 'oos', urgency: 2 }); else if (i.qty <= threshold) alerts.push({ item: i, type: 'low', urgency: 1 }); }); alerts.sort((a, b) => b.urgency - a.urgency || a.item.title.localeCompare(b.item.title)); return alerts; } function renderAlerts() { const container = $('alerts-content'); const alerts = getAlerts(); let html = '
'; // Left: Alerts list html += '
'; html += '

Alertes stock (' + alerts.length + ')

'; if (alerts.length === 0) { html += '
Aucune alerte \u2014 tous les produits sont au-dessus du seuil
'; } else { alerts.forEach(a => { const bObj = BRAND_MAP[a.item.brand]; html += '
'; html += '
' + (a.type === 'oos' ? '\u26A0' : '\u26A1') + '
'; html += '
' + a.item.title + ' ' + a.item.size + '
'; html += '
' + (bObj ? bObj.name : a.item.brand) + ' \u2014 ' + (a.type === 'oos' ? 'Rupture de stock' : 'Stock bas: ' + a.item.qty + ' unit\u00e9(s)') + '
'; html += '
' + a.item.qty + '
'; }); } html += '
'; // Right: Configuration html += '
'; html += '

Configuration

'; // General threshold html += '
Seuil d\'alerte global
'; html += '
Stock minimum'; html += '
'; // Brand thresholds html += '
Seuils par marque
'; BRANDS.filter(b => S.items.some(i => i.brand === b.id)).forEach(b => { const val = S.brandThresholds[b.id] || ''; html += '
' + b.name + ''; html += '
'; }); html += '
'; // Exchange rate html += '
Taux de change
'; html += '
GBP \u2192 EUR'; html += '
'; // Save button html += ''; // Export all inventory html += '
Export complet
'; html += '
'; html += '
'; container.innerHTML = html; // Config events container.querySelector('#save-config')?.addEventListener('click', () => { S.alertThreshold = Math.max(0, parseInt(container.querySelector('#cfg-threshold').value) || 3); S.rate = parseFloat(container.querySelector('#cfg-rate').value) || 1.15; $('rate-input').value = S.rate; container.querySelectorAll('.cfg-brand-threshold').forEach(inp => { const bid = inp.dataset.brand; const val = parseInt(inp.value); if (val > 0) S.brandThresholds[bid] = val; else delete S.brandThresholds[bid]; }); saveConfig(); renderAll(); toast('Configuration sauvegard\u00e9e'); }); container.querySelector('#export-full')?.addEventListener('click', () => { const headers = ['Produit', 'Taille', 'SKU', 'Marque', 'Vendeur', 'Stock', 'Prix achat EUR', 'PVC EUR', 'Marge %']; const rows = S.items.filter(i => S.activeBrands.has(i.brand)).map(i => { const c = getCost(i); const pvc = getRetailPrice(i); const margin = c && pvc ? ((pvc - c.cost) / pvc * 100).toFixed(0) : ''; return [i.title, i.size, i.sku, i.brand, i.vendor, i.qty, c ? c.cost.toFixed(2) : '', pvc ? pvc.toFixed(2) : '', margin]; }); exportCSV(rows, headers, 'inventaire-complet-' + new Date().toISOString().slice(0, 10) + '.csv'); }); } function updateBadges() { const restockCount = Object.values(S.restock).reduce((s, v) => s + v, 0); const rBadge = $('restock-badge'); if (restockCount > 0) { rBadge.textContent = restockCount; rBadge.style.display = 'inline-block'; } else { rBadge.style.display = 'none'; } const alertCount = getAlerts().length; const aBadge = $('alerts-badge'); if (alertCount > 0) { aBadge.textContent = alertCount; aBadge.style.display = 'inline-block'; } else { aBadge.style.display = 'none'; } } // =========================== // EVENT BINDINGS // =========================== function bindEvents() { // Tab navigation $('tab-nav').addEventListener('click', e => { const pill = e.target.closest('.pill'); if (!pill || !pill.dataset.tab) return; S.tab = pill.dataset.tab; $('tab-nav').querySelectorAll('.pill').forEach(p => p.classList.remove('active')); pill.classList.add('active'); document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active')); $('tab-' + S.tab).classList.add('active'); renderAll(); }); // Pricing mode $('pricing-toggle').addEventListener('click', e => { const pill = e.target.closest('.pill'); if (!pill || !pill.dataset.mode) return; S.mode = pill.dataset.mode; $('pricing-toggle').querySelectorAll('.pill').forEach(p => p.classList.remove('active')); pill.classList.add('active'); renderAll(); }); // Rate input $('rate-input').addEventListener('change', e => { S.rate = parseFloat(e.target.value) || 1.15; renderAll(); }); // Search $('search-input').addEventListener('input', e => { S.search = e.target.value; renderInventory(); renderStats(); }); // Filters $('filter-brand').addEventListener('change', e => { S.filterBrand = e.target.value; renderInventory(); renderStats(); }); $('filter-stock').addEventListener('change', e => { S.filterStock = e.target.value; renderInventory(); renderStats(); }); $('filter-size').addEventListener('change', e => { S.filterSize = e.target.value; renderInventory(); renderStats(); }); // Export inventory CSV $('export-inv-btn').addEventListener('click', () => { const filtered = sortItems(getFilteredItems()); const headers = ['Produit', 'Taille', 'SKU', 'Marque', 'Stock', 'Prix achat EUR', 'GBP', 'PVC EUR', 'Marge %']; const rows = filtered.map(i => { const c = getCost(i); const pvc = getRetailPrice(i) || (c && c.pvc ? c.pvc : null); const margin = c && pvc ? ((pvc - c.cost) / pvc * 100).toFixed(0) : ''; return [i.title, i.size, i.sku, i.brand, i.qty, c ? c.cost.toFixed(2) : '', c && c.gbp ? c.gbp.toFixed(2) : '', pvc ? pvc.toFixed(2) : '', margin]; }); exportCSV(rows, headers, 'inventaire-' + new Date().toISOString().slice(0, 10) + '.csv'); }); } // =========================== // BOOT // =========================== let booted = false; window.boot = boot; async function boot() { if (booted) return; booted = true; // Set timestamp const d = new Date(); $('last-update').textContent = d.toLocaleDateString('fr-FR') + ' ' + d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); // Load data loadShopifyData(); bindEvents(); renderAll(); // Async loads (non-blocking) await Promise.all([loadCosts(), loadConfig(), loadHistory()]); renderAll(); // Save daily snapshot saveSnapshot(); } // Auto-boot if already authenticated from session if(window.__autoStart) boot(); })();