carritoIA/templates/index.html

423 lines
18 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CarritoIA</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:.25rem;">
<h1>CarritoIA</h1>
<div style="display:flex; align-items:center; gap:.75rem;">
{% if session.get('usuario') == 'admin' %}
<a href="/admin" class="btn btn-secondary btn-sm">⚙️ Admin</a>
{% endif %}
<a href="/estadisticas" class="btn btn-secondary btn-sm">📊 Estadísticas</a>
<a href="/perfil" class="btn btn-secondary btn-sm">👤 Perfil</a>
<span style="font-size:.85rem; color:var(--text-muted);">{{ nombre }}</span>
<a href="/logout" class="btn btn-secondary btn-sm">Salir</a>
</div>
</div>
<p class="subtitle" id="subtitulo">Cargando predicciones...</p>
<div class="toolbar">
<button class="btn btn-primary btn-sm" onclick="marcarSemanal()">Marcar sugeridos esta semana</button>
<button class="btn btn-secondary btn-sm" onclick="desmarcarTodo()">Desmarcar todo</button>
<button class="btn btn-secondary btn-sm" onclick="toggleTodos()" id="btnToggle">Plegar todo</button>
</div>
<div class="layout">
<div id="columnaProductos">
<div class="no-datos" id="sinDatos" style="display:none;">
<p>No hay predicciones generadas todavia.</p>
<p>Ejecuta el pipeline en el servidor o usa el boton de abajo.</p>
</div>
</div>
<div class="panel">
<h2>Lista de la compra <span class="count-badge" id="contadorSeleccionados">0</span></h2>
<textarea id="listaGenerada" placeholder="Marca productos a la izquierda..."></textarea>
<div style="margin-top:.75rem; display:flex; gap:.5rem;">
<button class="btn btn-primary" onclick="copiarLista()">Copiar</button>
<button class="btn btn-secondary" onclick="limpiarLista()">Limpiar</button>
</div>
<h2>Anadir manualmente</h2>
<div class="add-row">
<input type="text" id="nuevoProducto" placeholder="Producto..."
onkeydown="if(event.key==='Enter') agregarManual()">
<button class="btn btn-secondary btn-sm" onclick="agregarManual()">Anadir</button>
</div>
<h2>Subir ticket PDF</h2>
<div class="drop-zone" id="dropZone"
ondragover="dzOver(event)" ondragleave="dzLeave()" ondrop="dzDrop(event)">
<input type="file" id="ticketPDF" accept="application/pdf"
onchange="leerPDF(this.files[0])">
<span class="drop-zone-icon">📄</span>
Arrastra el PDF aqui o haz clic para seleccionarlo
</div>
<div id="pdfEstado" style="font-size:.8rem; color:#8b949e; margin-top:.4rem;"></div>
<pre id="pdfTexto"></pre>
<h2>Subir foto del ticket 📷</h2>
<div class="drop-zone" id="dropZoneFoto"
ondragover="dzOver(event)" ondragleave="dzLeaveFoto()" ondrop="dzDropFoto(event)">
<input type="file" id="ticketFoto" accept="image/jpeg,image/png,image/webp"
capture="environment" onchange="subirFoto(this.files[0])">
<span class="drop-zone-icon">📷</span>
Foto del ticket (JPG, PNG, WEBP)
</div>
<div id="fotoEstado" style="font-size:.8rem; color:#8b949e; margin-top:.4rem;"></div>
<div id="fotoProductos" style="margin-top:.5rem;"></div>
<div style="margin-top:1.25rem; border-top:1px solid var(--border); padding-top:1rem;">
<button class="btn btn-secondary btn-sm" onclick="importarEmail()" id="btnImportarEmail">
📧 Importar tickets del correo
</button>
<div id="importarEmailEstado" style="font-size:.8rem; color:var(--text-muted); margin-top:.4rem;"></div>
</div>
<div style="margin-top:1rem; border-top:1px solid var(--border); padding-top:1rem;">
<button class="btn btn-secondary btn-sm" onclick="regenerar()" id="btnRegenerar">
Regenerar predicciones
</button>
<div id="regenerarEstado" style="font-size:.8rem; color:var(--text-muted); margin-top:.4rem;"></div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.min.js"></script>
<script>
// -----------------------------------------------------------------------
// Estado
// -----------------------------------------------------------------------
const seleccionados = new Set();
let productosManuales = [];
let predicciones = [];
// -----------------------------------------------------------------------
// Carga de datos desde la API
// -----------------------------------------------------------------------
window.addEventListener('load', async () => {
try {
const res = await fetch('/api/datos');
if (res.status === 401) { location.href = '/login'; return; }
const datos = await res.json();
predicciones = datos.predicciones || [];
if (!predicciones.length) {
document.getElementById('sinDatos').style.display = 'block';
document.getElementById('subtitulo').textContent = 'Sin datos todavia';
return;
}
if (datos.generado) {
document.getElementById('subtitulo').textContent =
'Generado el ' + datos.generado + ' — ' + predicciones.length + ' productos';
}
renderizarPredicciones();
} catch(e) {
document.getElementById('subtitulo').textContent = 'Error al cargar datos';
}
});
// -----------------------------------------------------------------------
// Renderizado
// -----------------------------------------------------------------------
function renderizarPredicciones() {
const contenedor = document.getElementById('columnaProductos');
contenedor.innerHTML = '';
const grupos = [
{ titulo: 'Compra semanal', badge: 'badge-rojo', label: 'cada semana', items: predicciones.filter(p => p.frecuencia_dias <= 8) },
{ titulo: 'Compra quincenal', badge: 'badge-naranja', label: 'cada 1-2 semanas', items: predicciones.filter(p => p.frecuencia_dias > 8 && p.frecuencia_dias <= 16) },
{ titulo: 'Compra mensual', badge: 'badge-verde', label: 'cada 2-4 semanas', items: predicciones.filter(p => p.frecuencia_dias > 16 && p.frecuencia_dias <= 35) },
{ titulo: 'Compra esporadica', badge: 'badge-gris', label: 'mas de un mes', items: predicciones.filter(p => p.frecuencia_dias > 35) },
];
grupos.forEach((grupo, idx) => {
if (!grupo.items.length) return;
const card = document.createElement('div');
card.className = 'card' + (idx >= 2 ? ' collapsed' : '');
card.innerHTML =
'<div class="card-header">' +
'<strong>' + grupo.titulo + '</strong>' +
'<span class="badge ' + grupo.badge + '">' + grupo.label + '</span>' +
'<span class="card-count">' + grupo.items.length + '</span>' +
'<span class="card-chevron">▾</span>' +
'</div>' +
'<div class="prod-list"></div>';
card.querySelector('.card-header').addEventListener('click', () => {
card.classList.toggle('collapsed');
actualizarBotonToggle();
});
contenedor.appendChild(card);
const lista = card.querySelector('.prod-list');
grupo.items.forEach(prod => lista.appendChild(crearItem(prod)));
});
}
function toggleTodos() {
const cards = [...document.querySelectorAll('#columnaProductos .card')];
const todosCollapsed = cards.every(c => c.classList.contains('collapsed'));
cards.forEach(c => c.classList.toggle('collapsed', !todosCollapsed));
actualizarBotonToggle();
}
function actualizarBotonToggle() {
const cards = [...document.querySelectorAll('#columnaProductos .card')];
const todosCollapsed = cards.length > 0 && cards.every(c => c.classList.contains('collapsed'));
document.getElementById('btnToggle').textContent = todosCollapsed ? 'Desplegar todo' : 'Plegar todo';
}
function crearItem(prod) {
const li = document.createElement('div');
li.className = 'prod-item';
li.dataset.nombre = prod.producto;
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.checked = seleccionados.has(prod.producto);
chk.addEventListener('change', () => toggleSeleccionado(prod.producto, chk.checked, span));
const span = document.createElement('span');
span.className = 'prod-nombre' + (chk.checked ? ' tachado' : '');
span.textContent = prod.producto;
const freq = document.createElement('span');
freq.className = 'prod-freq';
freq.textContent = '~' + prod.frecuencia_dias + 'd';
freq.title = 'Estimado: ' + prod.fecha_estimada;
li.appendChild(chk);
li.appendChild(span);
if (prod.es_estacional) {
const badge = document.createElement('span');
badge.className = 'badge-temporada';
badge.textContent = '🌱';
badge.title = 'Producto de temporada (meses: ' + prod.meses_temporada + ')';
li.appendChild(badge);
}
li.appendChild(freq);
li.addEventListener('click', e => {
if (e.target !== chk) { chk.checked = !chk.checked; toggleSeleccionado(prod.producto, chk.checked, span); }
});
return li;
}
// -----------------------------------------------------------------------
// Seleccion
// -----------------------------------------------------------------------
function toggleSeleccionado(nombre, checked, span) {
if (checked) { seleccionados.add(nombre); span && span.classList.add('tachado'); }
else { seleccionados.delete(nombre); span && span.classList.remove('tachado'); }
actualizarLista();
}
function marcarSemanal() {
predicciones.filter(p => p.frecuencia_dias <= 8).forEach(p => seleccionados.add(p.producto));
refrescarCheckboxes(); actualizarLista();
}
function desmarcarTodo() {
seleccionados.clear(); productosManuales = [];
refrescarCheckboxes(); actualizarLista();
}
function refrescarCheckboxes() {
document.querySelectorAll('.prod-item').forEach(li => {
const chk = li.querySelector('input[type="checkbox"]');
const span = li.querySelector('.prod-nombre');
const activo = seleccionados.has(li.dataset.nombre);
chk.checked = activo;
span.classList.toggle('tachado', activo);
});
}
// -----------------------------------------------------------------------
// Lista
// -----------------------------------------------------------------------
function actualizarLista() {
const todos = [...seleccionados, ...productosManuales];
document.getElementById('listaGenerada').value = todos.join('\n');
document.getElementById('contadorSeleccionados').textContent = todos.length;
}
function copiarLista() {
const txt = document.getElementById('listaGenerada').value;
if (!txt) return;
navigator.clipboard.writeText(txt)
.then(() => alert('Lista copiada al portapapeles'))
.catch(() => { const ta = document.getElementById('listaGenerada'); ta.select(); document.execCommand('copy'); });
}
function limpiarLista() {
seleccionados.clear(); productosManuales = [];
refrescarCheckboxes(); actualizarLista();
}
function agregarManual() {
const input = document.getElementById('nuevoProducto');
const nombre = input.value.trim();
if (!nombre) return;
productosManuales.push(nombre);
input.value = '';
actualizarLista();
}
// -----------------------------------------------------------------------
// Regenerar pipeline
// -----------------------------------------------------------------------
async function regenerar() {
const btn = document.getElementById('btnRegenerar');
const estado = document.getElementById('regenerarEstado');
btn.disabled = true;
estado.textContent = 'Ejecutando pipeline...';
try {
const res = await fetch('/api/regenerar', { method: 'POST' });
const data = await res.json();
if (data.ok) {
estado.textContent = '✅ ' + data.mensaje;
setTimeout(() => location.reload(), 1500);
} else {
estado.style.color = '#f97316';
estado.textContent = '❌ ' + data.mensaje;
if (data.detalle) {
estado.innerHTML += '<pre style="font-size:.7rem;white-space:pre-wrap;margin-top:.3rem;color:#f97316;">' + data.detalle + '</pre>';
}
}
} catch(e) {
estado.textContent = 'Error de conexion';
} finally {
btn.disabled = false;
}
}
// -----------------------------------------------------------------------
// Drag & Drop + lectura PDF
// -----------------------------------------------------------------------
function dzOver(e) { e.preventDefault(); document.getElementById('dropZone').classList.add('drag-over'); }
function dzLeave() { document.getElementById('dropZone').classList.remove('drag-over'); }
function dzDrop(e) {
e.preventDefault(); dzLeave();
const file = e.dataTransfer.files[0];
if (file && file.type === 'application/pdf') leerPDF(file);
}
async function leerPDF(file) {
if (!file) return;
const estado = document.getElementById('pdfEstado');
estado.style.color = 'var(--text-muted)';
estado.textContent = 'Leyendo ' + file.name + '...';
// Subir al servidor en paralelo
const form = new FormData();
form.append('pdf', file);
const subirPromise = fetch('/api/subir-pdf', { method: 'POST', body: form });
// Extraer texto localmente con pdf.js
const pdfData = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
let texto = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
texto += content.items.map(item => item.str).join(' ') + '\n';
}
const salida = document.getElementById('pdfTexto');
salida.style.display = 'block';
salida.textContent = texto.trim();
// Resultado de la subida
try {
const res = await subirPromise;
const data = await res.json();
if (data.ok) {
estado.textContent = '✅ ' + file.name + ' guardado en el servidor (' + pdf.numPages + ' pag.) — Pulsa "Regenerar predicciones" para actualizar.';
} else {
estado.style.color = '#f97316';
estado.textContent = '⚠️ ' + file.name + ' leido localmente pero no se pudo guardar en el servidor: ' + data.mensaje;
}
} catch(e) {
estado.style.color = '#f97316';
estado.textContent = '⚠️ ' + file.name + ' leido localmente pero no se pudo subir al servidor.';
}
}
// -----------------------------------------------------------------------
// Subir foto del ticket (OCR en servidor)
// -----------------------------------------------------------------------
function dzLeaveFoto() { document.getElementById('dropZoneFoto').classList.remove('drag-over'); }
function dzDropFoto(e) {
e.preventDefault(); dzLeaveFoto();
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) subirFoto(file);
}
async function subirFoto(file) {
if (!file) return;
const estado = document.getElementById('fotoEstado');
const productos = document.getElementById('fotoProductos');
estado.textContent = '⏳ Procesando con OCR... (puede tardar unos segundos)';
productos.innerHTML = '';
const form = new FormData();
form.append('imagen', file);
try {
const res = await fetch('/api/ocr-ticket', { method: 'POST', body: form });
const data = await res.json();
if (data.ok) {
estado.textContent = '✅ ' + data.mensaje + ' (fecha: ' + data.fecha + ') — Pulsa "Regenerar predicciones" para actualizar la lista.';
productos.innerHTML = '<details style="margin-top:.4rem;"><summary style="cursor:pointer;font-size:.8rem;color:var(--text-muted)">Ver productos detectados</summary><ul style="font-size:.8rem;margin:.4rem 0 0 1rem;color:var(--text-muted);">' +
data.productos.map(p => `<li>${p.cantidad}× ${p.producto}${p.precio_total.toFixed(2)} €</li>`).join('') +
'</ul></details>';
} else {
estado.textContent = '❌ ' + data.mensaje;
if (data.texto) {
productos.innerHTML = '<details style="margin-top:.4rem;"><summary style="cursor:pointer;font-size:.8rem;color:var(--text-muted)">Texto extraido (sin productos)</summary><pre style="font-size:.72rem;color:var(--text-muted);white-space:pre-wrap;">' + data.texto + '</pre></details>';
}
}
} catch(e) {
estado.textContent = '❌ Error de conexion';
}
}
// -----------------------------------------------------------------------
// Importar tickets desde correo
// -----------------------------------------------------------------------
async function importarEmail() {
const btn = document.getElementById('btnImportarEmail');
const estado = document.getElementById('importarEmailEstado');
btn.disabled = true;
estado.style.color = 'var(--text-muted)';
estado.textContent = '⏳ Conectando con el correo...';
try {
const res = await fetch('/api/importar-email', { method: 'POST' });
const data = await res.json();
if (data.ok) {
if (data.nuevos > 0) {
estado.textContent = '✅ ' + data.mensaje + ' — regenerando predicciones...';
await fetch('/api/regenerar', { method: 'POST' });
setTimeout(() => location.reload(), 1200);
} else {
estado.textContent = '✅ No hay tickets nuevos en el correo.';
}
} else {
estado.style.color = '#f97316';
estado.textContent = '❌ ' + data.mensaje;
if (data.detalle) {
estado.innerHTML += '<pre style="font-size:.7rem;white-space:pre-wrap;margin-top:.3rem;color:#f97316;">' + data.detalle + '</pre>';
}
}
} catch(e) {
estado.textContent = '❌ Error de conexion';
} finally {
btn.disabled = false;
}
}
</script>
</body>
</html>