// ---- Claves de almacenamiento ----
const LS_KEYS = {
products: 'ib_products',
moves: 'ib_moves',
settings: 'ib_settings',
};
// ---- Estado ----
const state = {
products: [],
moves: [],
settings: { theme: 'dark' },
selectedTab: 'productos',
};
// ---- Helpers ----
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
const uid = () => Math.random().toString(36).slice(2, 10);
const todayStr = () => new Date().toISOString().slice(0,10);
const fmt = (n) => Number(n || 0).toLocaleString(undefined,
{ maximumFractionDigits: 4 });
const byId = (arr, id) => arr.find(x => x.id === id);
// ---- Persistencia ----
function saveAll() {
localStorage.setItem(LS_KEYS.products, JSON.stringify(state.products));
localStorage.setItem(LS_KEYS.moves, JSON.stringify(state.moves));
localStorage.setItem(LS_KEYS.settings, JSON.stringify(state.settings));
}
function loadAll() {
state.products = JSON.parse(localStorage.getItem(LS_KEYS.products) || '[]');
state.moves = JSON.parse(localStorage.getItem(LS_KEYS.moves) || '[]');
const s = JSON.parse(localStorage.getItem(LS_KEYS.settings) || '{}');
if (s.theme) state.settings.theme = s.theme;
}
// ---- Lógica de inventario ----
function saldoProducto(productId) {
return state.moves
.filter(m => m.productId === productId)
.reduce((acc, m) => acc + (m.tipo === 'Ingreso' ? m.cantidad : -m.cantidad),
0);
}
function groupByDate(productId, startDate=null) {
const map = new Map();
for (const m of state.moves) {
if (m.productId !== productId) continue;
if (startDate && m.fecha < startDate) continue;
const key = m.fecha;
if (!map.has(key)) map.set(key, { Ingreso:0, Salida:0, Merma:0 });
map.get(key)[m.tipo] += Number(m.cantidad);
}
return Array.from(map.entries()).sort((a,b) => a[0].localeCompare(b[0]));
}
// ---- Render de pestañas ----
function showTab(name) {
state.selectedTab = name;
$$('.tab-btn').forEach(btn => {
const active = btn.dataset.tab === name;
btn.classList.toggle('active', active);
btn.setAttribute('aria-selected', active);
});
$$('.tab').forEach(sec => sec.hidden = sec.id !== 'tab-' + name);
if (name === 'graficas') drawChart();
}
// ---- Render Productos ----
function renderProductos() {
['#mProducto','#filtroProducto','#gProducto'].forEach(sel => {
const el = $(sel);
el.innerHTML = '';
if (sel === '#filtroProducto') el.append(new Option('Todos', 'all'));
state.products.forEach(p => el.append(new Option(p.nombre, p.id)));
});
const cont = $('#productosList');
if (!state.products.length) {
cont.innerHTML = `<div class="empty">No hay productos. Agrega el primero en el
formulario.</div>`;
return;
}
cont.innerHTML = '';
state.products.forEach(p => {
const s = saldoProducto(p.id);
const div = document.createElement('div');
div.className = 'product-card';
div.innerHTML = `
<img class="product-thumb" src="${p.foto || ''}" alt="${p.nombre}" style="${!
p.foto ? 'filter:grayscale(50%) brightness(.8);' : ''}">
<div>
<div style="font-weight:800;">${p.nombre}</div>
<div style="color:var(--muted); font-size:13px;">${p.etiqueta || 'Sin
etiqueta'}</div>
<div style="margin-top:6px; display:flex; gap:8px; flex-wrap:wrap;">
<span class="stat">Saldo: <span style="color:var(--primary); font-
weight:900;">${fmt(s)}</span></span>
<span class="stat">Movs: <strong>$
{state.moves.filter(m=>m.productId===p.id).length}</strong></span>
</div>
</div>
<div style="display:grid; gap:8px;">
<button class="btn secondary" onclick="showTab('movimientos'); $
('#mProducto').value='${p.id}'">Agregar movimiento</button>
<button class="btn danger"
onclick="borrarProducto('${p.id}')">Eliminar</button>
</div>
`;
cont.append(div);
});
}
function borrarProducto(id) {
if (!confirm('¿Eliminar el producto y sus movimientos?')) return;
state.products = state.products.filter(p => p.id !== id);
state.moves = state.moves.filter(m => m.productId !== id);
saveAll();
renderAll();
}
// ---- Render Movimientos ----
function renderMovimientos() {
const filtroId = $('#filtroProducto').value || 'all';
const order = $('#ordenMov').value || 'desc';
let rows = [...state.moves];
if (filtroId !== 'all') rows = rows.filter(m => m.productId === filtroId);
rows.sort((a,b) => order === 'desc' ? b.fecha.localeCompare(a.fecha) :
a.fecha.localeCompare(b.fecha));
const wrap = $('#movimientosList');
if (!rows.length) {
wrap.innerHTML = `<div class="empty">No hay movimientos.</div>`;
return;
}
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th>Fecha</th><th>Producto</th><th>Tipo</th><th>Cantidad</th><th>Nota</
th><th></th>
</tr>
</thead>
<tbody>
${rows.map(r => {
const p = byId(state.products, r.productId);
return `
<tr>
<td>${r.fecha}</td>
<td>${p ? p.nombre : '—'}</td>
<td><span class="pill ${r.tipo.toLowerCase()}">${r.tipo}</span></td>
<td><strong>${fmt(r.cantidad)}</strong></td>
<td>${r.nota || ''}</td>
<td><button class="btn secondary" onclick="eliminarMovimiento('$
{r.id}')">Borrar</button></td>
</tr>
`;
}).join('')}
</tbody>
`;
wrap.innerHTML = '';
const list = document.createElement('div');
list.className = 'list';
list.append(table);
wrap.append(list);
}
function eliminarMovimiento(id) {
state.moves = state.moves.filter(m => m.id !== id);
saveAll();
renderAll();
}
// ---- Gráficas ----
function drawChart() {
const canvas = $('#chart');
const ctx = canvas.getContext('2d');
ctx.clearRect(0,0,canvas.width,canvas.height);
const productId = $('#gProducto').value;
if (!productId) return;
const rango = $('#gRango').value;
let start = null;
if (rango !== 'all') {
const days = Number(rango);
const d = new Date();
d.setDate(d.getDate() - days + 1);
start = d.toISOString().slice(0,10);
}
const data = groupByDate(productId, start);
if (!data.length) {
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--muted');
ctx.fillText('Sin datos para graficar.', 24, 30);
return;
}
const padding = { top: 30, right: 24, bottom: 60, left: 56 };
const w = canvas.width - padding.left - padding.right;
const h = canvas.height - padding.top - padding.bottom;
let maxY = Math.max(...data.flatMap(([,v]) => [v.Ingreso,v.Salida,v.Merma]), 1);
const styles = getComputedStyle(document.body);
ctx.strokeStyle = styles.getPropertyValue('--border');
ctx.beginPath();
ctx.moveTo(padding.left, padding.top);
ctx.lineTo(padding.left, padding.top + h);
ctx.lineTo(padding.left + w, padding.top + h);
ctx.stroke();
ctx.fillStyle = styles.getPropertyValue('--muted');
ctx.font = '12px ui-sans-serif';
const ticks = 5;
for (let i=0; i<=ticks; i++) {
const yVal = (maxY/ticks)*i;
const y = padding.top + h - (yVal/maxY)*h;
ctx.globalAlpha = 0.3;
ctx.begin