🏆
💡 Tip del hito
En mobile, nunca uses event delegation para elementos generados con innerHTML
El problema del tap que no respondía venía de una regla simple del DOM mobile: cuando regenerás el HTML con innerHTML= y usás un listener en document.addEventListener('click') con closest(), en Android WebView el touch a veces no propaga como click cuando hay pointer-events conflictivos en los hijos.
La solución definitiva: usar un <button> nativo con onclick directo que referencia una variable global por índice. Los botones nativos siempre disparan click en mobile, sin excepción. Guardá los datos en un array global (_topNombres[i]) y pasá el índice al onclick.
Sesión · Marzo 2026 · venta v111 · reportes v1.18 · GAS v6.9
const TAREAS = [
// ── CRÍTICAS (P1) ──
{ id:1, p:1, tipo:'bug', done:true, archivo:'copihue-reportes.html', titulo:'Tap en productos del top ✓', desc:'Resuelto en v1.18 — button nativo con prod30H inline. Sin delegation, sin data-nombre.' },
{ id:2, p:1, tipo:'bug', done:true, archivo:'venta.html', titulo:'Ingreso mercadería no se cierra afuera ✓', desc:'Resuelto en v90 — cerrarModalIngreso no responde al tap fuera, solo al botón ✕.' },
{ id:3, p:1, tipo:'bug', done:true, archivo:'venta.html', titulo:'Precio ajuste rápido $100 en $100 ✓', desc:'Resuelto en v92 — botones +/- con touchend + stopPropagation.' },
{ id:4, p:1, tipo:'bug', done:true, archivo:'venta.html', titulo:'Categoría VINOS en ingreso ✓', desc:'Resuelto en v91 — VINOS, CERVEZAS, FIAMBRES, PANADERIA agregados a CATEGORIAS.' },
{ id:5, p:1, tipo:'bug', done:false, archivo:'venta.html', titulo:'Luz verde conexión en proveedores — verificar', desc:'Implementado en v94 con puntoProv. Victor no confirmó que funcione — verificar.' },
// ── ALTAS (P2) ──
{ id:6, p:2, tipo:'mejora', done:true, archivo:'venta.html', titulo:'ELECTRÓNICA ignorada en reposición ✓', desc:'Implementado en v93. Victor no recibe más la lista de electrónica.' },
{ id:7, p:2, tipo:'feat', done:false, archivo:'venta.html', titulo:'Carrito de reposición para el super', desc:'Módulo: elegís producto → precio y vencimiento → hoja temporal. Al llegar a casa confirmás y pasa a Historial.' },
{ id:8, p:2, tipo:'feat', done:false, archivo:'venta.html', titulo:'Sugerir último vencimiento al ingresar', desc:'Al cargar producto existente, sugerir el último vencimiento registrado del mismo producto.' },
{ id:9, p:2, tipo:'feat', done:false, archivo:'venta.html', titulo:'Pool de ofertas activas visible en POS', desc:'Cuando se activa una oferta mostrar en venta.html el pool de productos del turno.' },
{ id:10, p:2, tipo:'feat', done:false, archivo:'copihue-reportes.html', titulo:'Filtro por vendedor', desc:'Lista desplegable de vendedores para filtrar tickets y métricas individuales.' },
{ id:11, p:2, tipo:'feat', done:true, archivo:'copihue-reportes.html', titulo:'Filtros de período avanzados ✓', desc:'Selector jerárquico: HOY → Calendario → S1-S6 → Meses → Años anteriores colapsables. v1.12.' },
{ id:12, p:2, tipo:'mejora', done:true, archivo:'copihue-reportes.html', titulo:'Top productos: toggle venta / ganancia ✓', desc:'Botón para cambiar orden del listado entre mayor venta y mayor ganancia.' },
{ id:36, p:2, tipo:'bug', done:true, archivo:'codegs.txt', titulo:'Ganancia real negativa en reportes ✓', desc:'GAS v6.9: qty histórico en gramos (ej: 800) se convertía mal multiplicando costo×800. Fix: div /1000 si nombre tiene (Ngr) y qty≥50.' },
{ id:37, p:2, tipo:'feat', done:true, archivo:'copihue-reportes.html', titulo:'Tabla 30 días por producto ✓', desc:'Toggle azul al final. Tabla horizontal scrolleable: producto fijo izq, 30 columnas día con intensidad de color, total fijo der.' },
{ id:38, p:2, tipo:'feat', done:true, archivo:'copihue-reportes.html', titulo:'Tap producto → expande 30 días inline ✓', desc:'v1.18: tap en top venta/ganancia expande fila con unidades por día, scroll horizontal. Button nativo, sin modal.' },
{ id:39, p:2, tipo:'mejora', done:true, archivo:'venta.html', titulo:'Cache busting automático reportes ✓', desc:'venta.html v111 linkea copihue-reportes.html?v=118. Cada versión fuerza descarga fresca en celular.' },
// ── PENDIENTES ALTOS ──
{ id:40, p:2, tipo:'feat', done:false, archivo:'codegs.txt', titulo:'GAS v6.9 — republicar en Apps Script', desc:'Fix ganancia real (qty gramos), fix costos históricos. Pendiente republicar para que tome efecto en reportes.' },
{ id:41, p:2, tipo:'feat', done:false, archivo:'venta.html', titulo:'Seguridad: repo privado + PIN acceso', desc:'GitHub repo → privado. PIN 4 dígitos en venta.html y reportes. Token secreto en GAS.' },
{ id:42, p:2, tipo:'feat', done:false, archivo:'venta.html', titulo:'Sync timestamp ofertas automático', desc:'Al apagar toggle en POS → GAS escribe ultima_modificacion. index.html compara cada 2min y recarga solo ofertas si cambió.' },
{ id:43, p:3, tipo:'feat', done:false, archivo:'codegs.txt', titulo:'GAS endpoint action=cliente', desc:'Hoja Clientes en Sheets: nombre|telefono|primera_compra|ultima_compra|visitas|gasto_total. upsertCliente por teléfono.' },
{ id:44, p:3, tipo:'bug', done:false, archivo:'copihue-reportes.html', titulo:'Panel 30 días no renderiza números en Android', desc:'9 intentos fallidos. El fondo y título se ven, los números coloreados no. Causas descartadas: pointer-events, clases CSS, inline styles, emojis, flex, grid, button→div role=button, 3 filas separadas. Pendiente: chrome://inspect para ver DOM real en Android. Hipótesis: overflow:hidden heredado o height:0 en el contenedor.' },
// ── MEDIAS (P3) ──
{ id:13, p:3, tipo:'feat', done:false, archivo:'copihue-reportes.html', titulo:'Gráfico comparativo precios por proveedor', desc:'En modal de producto mostrar gráfico histórico del precio más barato por proveedor.' },
{ id:14, p:3, tipo:'feat', done:false, archivo:'copihue-reportes.html', titulo:'Top productos por rotación', desc:'Ranking de productos más vendidos en unidades, separado del top por ganancia.' },
{ id:15, p:3, tipo:'feat', done:false, archivo:'venta.html', titulo:'Semáforo de vencimiento en carrito', desc:'Verde = +300 días, amarillo = 8-29 días, naranja = hasta 7 días, rojo = últimos 3 días.' },
{ id:16, p:3, tipo:'feat', done:false, archivo:'venta.html', titulo:'Gestión de clientes — fiado', desc:'Menú Clientes/Deudas. Hoja por cliente en Sheets creada desde venta.html.' },
{ id:17, p:3, tipo:'feat', done:true, archivo:'index.html', titulo:'Compartir horario por WhatsApp ✓', desc:'Implementado en v002 — canvas con día resaltado en naranja, botón 📲 Compartir horario.' },
{ id:18, p:3, tipo:'feat', done:false, archivo:'venta.html', titulo:'Notificación vendedor cuando se activa promo', desc:'Toast o badge en el POS cuando el sistema activa automáticamente una oferta.' },
// ── BAJAS (P4) ──
{ id:19, p:4, tipo:'feat', done:false, archivo:'codegs.txt', titulo:'FIFO automático doble vencimiento', desc:'Al cargar producto nuevo sugerir sacar lote anterior. Al vender descontar del más antiguo.' },
{ id:20, p:4, tipo:'mejora', done:false, archivo:'copihue-reportes.html', titulo:'Ganancia real vs estimada más clara', desc:'Indicador visual más prominente para distinguir datos reales de estimados.' },
{ id:21, p:4, tipo:'feat', done:false, archivo:'venta.html', titulo:'Historial de ajustes rápidos en POS', desc:'Panel para ver los últimos ajustes de stock/precio desde el mismo POS.' },
{ id:22, p:4, tipo:'mejora', done:false, archivo:'index.html', titulo:'Imagen del producto en cards de ofertas', desc:'Mostrar foto cuando existe en imagenes-productos/, placeholder si no.' },
{ id:23, p:4, tipo:'mejora', done:false, archivo:'copihue-reportes.html', titulo:'Exportar reporte del día a WhatsApp', desc:'Resumen del día compartible como cierre de caja.' },
{ id:24, p:4, tipo:'idea', done:false, archivo:'venta.html', titulo:'Modo oscuro/claro en el POS', desc:'Toggle de tema para distintas condiciones de luz del local.' },
// ── IDEAS FUTURAS (P5) ──
{ id:25, p:5, tipo:'idea', done:false, archivo:'index.html', titulo:'Martes Dulce / Domingo Snacks', desc:'Reutilizar motor Jueves Cervecero para eventos automáticos por categoría otros días.' },
{ id:26, p:5, tipo:'idea', done:false, archivo:'codegs.txt', titulo:'Sistema de fidelidad — puntos por compra', desc:'Acumular puntos por ticket. El cliente los ve y los canjea en mostrador.' },
{ id:27, p:5, tipo:'idea', done:false, archivo:'index.html', titulo:'Modo pedido — el cliente arma el carrito', desc:'El cliente arma su pedido desde el celu y vos lo ves en el POS en tiempo real.' },
{ id:28, p:5, tipo:'idea', done:false, archivo:'codegs.txt', titulo:'Alerta stock bajo por WhatsApp', desc:'GAS envía mensaje cuando un producto baja de X unidades.' },
{ id:29, p:5, tipo:'idea', done:false, archivo:'index.html', titulo:'Landing con horarios y novedades', desc:'Página de inicio más completa con horario, novedades de la semana y acceso al catálogo.' },
// ── HISTORIAL RESUELTOS ──
{ id:30, p:1, tipo:'bug', done:true, archivo:'venta.html', titulo:'Botón Cobrar desaparecía con 3+ productos ✓', desc:'Resuelto en v96 — panel-total movido dentro de panel-venta con position:sticky bottom.' },
{ id:31, p:2, tipo:'mejora',done:true, archivo:'venta.html', titulo:'Header colapsa automáticamente al vender ✓', desc:'Resuelto en v97.' },
{ id:32, p:2, tipo:'feat', done:true, archivo:'venta.html', titulo:'Sin stock → ⚠️ abre ajuste directo ✓', desc:'Resuelto en v98.' },
{ id:33, p:2, tipo:'mejora',done:true, archivo:'venta.html', titulo:'Badge versión sincronizado con title ✓', desc:'Resuelto en v98.' },
{ id:34, p:2, tipo:'feat', done:true, archivo:'venta.html', titulo:'Carrito persistente con backup en planilla ✓', desc:'Resuelto en v95.' },
{ id:35, p:2, tipo:'feat', done:true, archivo:'copihue-reportes.html', titulo:'Stock en top ganancia y modal producto ✓', desc:'Resuelto en v1.11.' },
];
const PRIORIDAD_LABEL = { 1:'🔴 P1 crítica', 2:'🟠 P2 alta', 3:'🟡 P3 media', 4:'🟢 P4 baja', 5:'⚪ P5 idea' };
const SECCIONES = [
{ p:1, label:'🔴 Críticas — arreglar ya' },
{ p:2, label:'🟠 Altas — próxima sesión' },
{ p:3, label:'🟡 Medias — esta semana' },
{ p:4, label:'🟢 Bajas — cuando haya tiempo' },
{ p:5, label:'⚪ Ideas futuras' },
];
// Done se basa en el campo done:true del array — no en localStorage
// Completados confirmados por Victor:
// 1=tap reportes, 2=cierre modal, 3=precio $100, 4=cat vinos
// 5=luz verde proveedores (implementado, pendiente confirmar visual)
// 6=electrónica ignorada en reposición ✓ (apareció lista sin electrónica)
// 12=toggle top venta/ganancia ✓
// 13=stock en top y modal ✓ (badge semáforo)
// 17=compartir horario WhatsApp ✓ (canvas con día resaltado)
// — venta.html fixes —
// cobrar desaparecía con 3+ productos ✓ (panel-total dentro de panel-venta v96)
// header colapsa al vender ✓ (v97)
// sin stock → botón ⚠️ abre ajuste directo ✓ (v98)
// versionTag sincronizado con title ✓ (v98)
// carrito persistente visibilitychange + backup planilla ✓ (v95)
// filtros período avanzados reportes ✓ (v1.9)
let done = TAREAS.filter(t => t.done).map(t => t.id);
let filtroActivo = 'todo';
function filtrar(f, btn) {
filtroActivo = f;
document.querySelectorAll('.filtro-btn').forEach(b => b.classList.remove('activo'));
btn.classList.add('activo');
render();
}
function toggleDone(id) {
const t = TAREAS.find(t => t.id === id);
if (t && t.done) return; // las marcadas como done fijas no se cambian
if (done.includes(id)) done = done.filter(d => d !== id);
else done.push(id);
render();
}
function render() {
const board = document.getElementById('board');
let html = '';
const tareasFiltradas = filtroActivo === 'todo'
? TAREAS
: TAREAS.filter(t => t.tipo === filtroActivo);
let totalPend = 0, totalP1 = 0, totalP2 = 0, totalDone = 0;
TAREAS.forEach(t => {
if (done.includes(t.id)) totalDone++;
else {
totalPend++;
if (t.p === 1) totalP1++;
if (t.p === 2) totalP2++;
}
});
document.getElementById('sTodo').textContent = totalPend;
document.getElementById('sP1').textContent = totalP1;
document.getElementById('sP2').textContent = totalP2;
document.getElementById('sDone').textContent = totalDone;
SECCIONES.forEach(sec => {
const items = tareasFiltradas.filter(t => t.p === sec.p);
if (!items.length) return;
html += `
🏆
💡 Tip del hito — Marzo 2026
En mobile, nunca uses event delegation para elementos generados con innerHTML
El tap que no respondía venía de una regla del DOM mobile: cuando regenerás HTML con innerHTML= y usás document.addEventListener('click') con closest(), en Android el touch a veces no propaga como click cuando hay conflictos de pointer-events en los hijos.
✅ Solución definitiva: usá un <button> nativo con onclick directo que referencia un array global por índice. Los botones nativos siempre disparan click en mobile, sin excepción. Guardá los datos en _topNombres[i] y pasá el índice al onclick.
venta v111
reportes v1.18
GAS v6.9
⚠️ GAS pendiente republicar