diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..641225b --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Copia este archivo como .env y rellena los valores +SECRET_KEY=pon-aqui-una-clave-larga-y-aleatoria +ADMIN_PASSWORD=tu-password-de-admin \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8930940 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Datos sensibles — nunca al repo +config.ini +users.json +*.pdf +tickets/ + +# Generados +datos.js +datos.json +lista_compra_estimado.csv +lista_compra_estimado.csv +*.csv +__pycache__/ +*.pyc +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d4cfe55 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Crear carpetas que necesita la app +RUN mkdir -p tickets + +EXPOSE 5000 + +CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5000", "--timeout", "120", "app:app"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..305beb4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + + autocompra: + build: . + container_name: autocompra + restart: unless-stopped + environment: + SECRET_KEY: "${SECRET_KEY}" + ADMIN_PASSWORD: "${ADMIN_PASSWORD}" + volumes: + # Persistir tickets, datos generados y configuracion fuera del contenedor + - ./tickets:/app/tickets + - ./datos.json:/app/datos.json + - ./users.json:/app/users.json + - ./config.ini:/app/config.ini + - ./lista_compra_estimado.csv:/app/lista_compra_estimado.csv + ports: + - "8088:5000" \ No newline at end of file diff --git a/generar_lista.py b/generar_lista.py index f68c4c6..7da8fff 100644 --- a/generar_lista.py +++ b/generar_lista.py @@ -106,5 +106,14 @@ with open('datos.js', 'w', encoding='utf-8') as f: json.dump(resultado, f, ensure_ascii=False, indent=2) f.write(';\n') -print(f"✓ {len(resultado)} predicciones escritas en datos.js") -print(f" Abre index.html en el navegador para ver la lista.") +# Escribir datos.json (usado por la app Flask) +datos_json = { + 'generado': ts, + 'total': len(resultado), + 'predicciones': resultado, +} +with open('datos.json', 'w', encoding='utf-8') as f: + json.dump(datos_json, f, ensure_ascii=False, indent=2) + +print(f"✓ {len(resultado)} predicciones escritas en datos.js y datos.json") +print(f" Abre index.html en el navegador o arranca app.py para ver la lista.") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5411509 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask>=3.0 +gunicorn>=21.0 +werkzeug>=3.0 +PyPDF2>=3.0 +pandas>=2.0 \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..6410677 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,313 @@ +/* ============================================================ + Lista de la Compra Inteligente — Dark Theme + ============================================================ */ + +:root { + --bg: #0d1117; + --bg-card: #161b22; + --bg-hover: #1c2128; + --bg-input: #0d1117; + --border: #30363d; + --text: #e6edf3; + --text-muted: #8b949e; + --primary: #388bfd; + --primary-h: #58a6ff; + + /* badges */ + --badge-sem-bg: #3d2b1f; + --badge-sem-tx: #f0883e; + --badge-qui-bg: #2d2816; + --badge-qui-tx: #d29922; + --badge-men-bg: #162518; + --badge-men-tx: #3fb950; + --badge-esp-bg: #21262d; + --badge-esp-tx: #8b949e; +} + +*, *::before, *::after { box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + max-width: 960px; + margin: 0 auto; + padding: 1rem; + background: var(--bg); + color: var(--text); + min-height: 100vh; +} + +h1 { font-size: 1.6rem; margin: 0 0 .25rem; color: var(--text); } +h2 { + font-size: .85rem; + margin: 1.5rem 0 .5rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: .08em; + font-weight: 600; +} + +.subtitle { color: var(--text-muted); font-size: .85rem; margin-bottom: 1.5rem; } + +/* ---- Layout ---- */ +.layout { + display: grid; + grid-template-columns: 1fr 300px; + gap: 1.5rem; + align-items: start; +} +@media (max-width: 700px) { + .layout { grid-template-columns: 1fr; } +} + +/* ---- Cards ---- */ +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem 1.25rem; + margin-bottom: 1rem; +} +.card-header { + display: flex; + align-items: center; + gap: .5rem; + margin-bottom: .75rem; +} +.card-header strong { color: var(--text); font-size: .95rem; } + +/* ---- Badges ---- */ +.badge { + display: inline-block; + padding: .2rem .6rem; + border-radius: 20px; + font-size: .72rem; + font-weight: 600; +} +.badge-rojo { background: var(--badge-sem-bg); color: var(--badge-sem-tx); } +.badge-naranja { background: var(--badge-qui-bg); color: var(--badge-qui-tx); } +.badge-verde { background: var(--badge-men-bg); color: var(--badge-men-tx); } +.badge-gris { background: var(--badge-esp-bg); color: var(--badge-esp-tx); } + +/* ---- Producto item ---- */ +.prod-item { + display: flex; + align-items: center; + gap: .6rem; + padding: .45rem .25rem; + border-bottom: 1px solid var(--border); + cursor: pointer; + border-radius: 4px; + transition: background .1s; +} +.prod-item:last-child { border-bottom: none; } +.prod-item:hover { background: var(--bg-hover); } +.prod-item input[type="checkbox"] { + width: 15px; + height: 15px; + cursor: pointer; + flex-shrink: 0; + accent-color: var(--primary); +} +.prod-nombre { flex: 1; font-size: .92rem; color: var(--text); } +.prod-nombre.tachado { text-decoration: line-through; color: var(--text-muted); } +.prod-freq { font-size: .72rem; color: var(--text-muted); white-space: nowrap; } + +/* ---- Panel lateral ---- */ +.panel { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem 1.25rem; + position: sticky; + top: 1rem; +} +.panel h2 { margin-top: 0; } + +#listaGenerada { + width: 100%; + height: 220px; + font-size: .88rem; + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + padding: .6rem; + resize: vertical; + font-family: inherit; +} +#listaGenerada:focus { outline: none; border-color: var(--primary); } + +/* ---- Botones ---- */ +.btn { + display: inline-flex; + align-items: center; + gap: .35rem; + padding: .5rem 1rem; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: .88rem; + font-weight: 500; + transition: background .15s; +} +.btn-primary { background: var(--primary); color: #fff; } +.btn-primary:hover { background: var(--primary-h); } +.btn-secondary { + background: #21262d; + color: var(--text); + border: 1px solid var(--border); + margin-left: .4rem; +} +.btn-secondary:hover { background: #30363d; } +.btn-sm { padding: .3rem .7rem; font-size: .8rem; } + +/* ---- Toolbar ---- */ +.toolbar { + display: flex; + gap: .5rem; + flex-wrap: wrap; + margin-bottom: 1rem; +} + +/* ---- Input añadir manual ---- */ +.add-row { display: flex; gap: .5rem; margin-top: .5rem; } +.add-row input { + flex: 1; + padding: .42rem .7rem; + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + font-size: .88rem; +} +.add-row input:focus { outline: none; border-color: var(--primary); } + +/* ---- Sin datos ---- */ +.no-datos { + text-align: center; + padding: 2rem; + color: var(--text-muted); + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; +} +.no-datos code { + display: block; + background: var(--bg); + color: var(--primary-h); + border: 1px solid var(--border); + padding: .5rem 1rem; + border-radius: 6px; + margin: .75rem auto; + width: fit-content; + font-size: .9rem; +} + +/* ---- Drag & drop zona ticket ---- */ +.drop-zone { + border: 2px dashed var(--border); + border-radius: 8px; + padding: 1.2rem; + text-align: center; + color: var(--text-muted); + font-size: .85rem; + cursor: pointer; + transition: border-color .2s, background .2s; + margin-top: .5rem; + position: relative; +} +.drop-zone:hover, +.drop-zone.drag-over { + border-color: var(--primary); + background: #0d2044; + color: var(--text); +} +.drop-zone input[type="file"] { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; + width: 100%; + height: 100%; +} +.drop-zone-icon { font-size: 1.5rem; display: block; margin-bottom: .3rem; } + +#pdfTexto { + white-space: pre-wrap; + background: var(--bg); + color: var(--text-muted); + border: 1px solid var(--border); + padding: .75rem; + border-radius: 6px; + font-size: .78rem; + max-height: 250px; + overflow-y: auto; + display: none; + margin-top: .75rem; + font-family: 'Courier New', monospace; +} + +/* ---- Count badge ---- */ +.count-badge { + background: var(--primary); + color: #fff; + border-radius: 50%; + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: .7rem; + font-weight: 700; + margin-left: .4rem; + vertical-align: middle; +} + +/* ---- Scrollbar oscura ---- */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: var(--bg); } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } + +/* ---- Login page ---- */ +.login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background: var(--bg); +} +.login-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 2rem; + width: 100%; + max-width: 360px; +} +.login-card h1 { font-size: 1.4rem; margin-bottom: .25rem; } +.login-card label { + display: block; + font-size: .82rem; + color: var(--text-muted); + margin: .75rem 0 .25rem; + font-weight: 600; +} +.login-card input { + width: 100%; + padding: .5rem .75rem; + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + font-size: .92rem; +} +.login-card input:focus { outline: none; border-color: var(--primary); } +.alert { + background: #3d1f1f; + color: #f47f7f; + border: 1px solid #7d2020; + border-radius: 6px; + padding: .5rem .75rem; + font-size: .85rem; + margin-top: .5rem; +} \ No newline at end of file diff --git a/templates/admin_usuarios.html b/templates/admin_usuarios.html new file mode 100644 index 0000000..cce4020 --- /dev/null +++ b/templates/admin_usuarios.html @@ -0,0 +1,41 @@ + + +
+ + +Cargando predicciones...
+ + + +Inicia sesion para continuar
+ {% if error %} +