From 1d3e4e9dd02d84fd5d8ea26a3cdf6b2cc6225ae1 Mon Sep 17 00:00:00 2001 From: juan-server Date: Thu, 3 Sep 2026 16:03:14 -0500 Subject: [PATCH] caja menor y libro diario --- Dockerfile | 15 ++ backend/app.py | 363 ++++++++++++++++++++++++++ backend/requirements.txt | 3 + db/cajaMenor.db | Bin 0 -> 16384 bytes db/libroDiario.db | Bin 0 -> 12288 bytes docker-compose.yml | 14 + fronted/acciones.js | 195 ++++++++++++++ fronted/averias.html | 28 ++ fronted/cajaMenor.html | 131 ++++++++++ fronted/cajaMenor.js | 136 ++++++++++ fronted/estilos.css | 492 +++++++++++++++++++++++++++++++++++ fronted/index.html | 46 ++++ fronted/libroDiario.html | 124 +++++++++ fronted/libroDiario.js | 114 ++++++++ fronted/pagosMensuales.html | 28 ++ fronted/transportadoras.html | 28 ++ google script/Code.gs | 193 ++++++++++++++ 17 files changed, 1910 insertions(+) create mode 100644 Dockerfile create mode 100644 backend/app.py create mode 100644 backend/requirements.txt create mode 100644 db/cajaMenor.db create mode 100644 db/libroDiario.db create mode 100644 docker-compose.yml create mode 100644 fronted/acciones.js create mode 100644 fronted/averias.html create mode 100644 fronted/cajaMenor.html create mode 100644 fronted/cajaMenor.js create mode 100644 fronted/estilos.css create mode 100644 fronted/index.html create mode 100644 fronted/libroDiario.html create mode 100644 fronted/libroDiario.js create mode 100644 fronted/pagosMensuales.html create mode 100644 fronted/transportadoras.html create mode 100644 google script/Code.gs diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5de2619 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Instalar dependencias +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copiar el código +COPY backend/ /app/backend/ +COPY fronted/ /app/fronted/ + +EXPOSE 5000 + +CMD ["python", "backend/app.py"] diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..8cf72f7 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,363 @@ +from flask import Flask, request, jsonify, send_from_directory +from flask_cors import CORS +import requests +import sqlite3 +import threading +import time +import os +from datetime import datetime + +# Servir archivos estáticos desde la carpeta fronted +app = Flask(__name__, static_folder="../fronted", static_url_path="") +CORS(app) + +# URL por defecto (se puede sobreescribir vía /config o por el body en /test_gs) +API_URL = "https://script.google.com/macros/s/AKfycbwSLQWgSxHy51_vVopRyKss0UlrqCuKpO8LhxaEBztzsk-Idc-_LnvjsT5dUEwTlb9r/exec" +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DB_DIR = os.path.join(BASE_DIR, "..", "db") +INTERVALO_SYNC = 600 # 10 minutos en segundos + +# ============ CONFIGURACIÓN DE MÓDULOS ============ +# Cada módulo tiene su PROPIA base de datos SQLite y su hoja de Google Sheets +MODULOS = { + "cajaMenor": { + "db": os.path.join(DB_DIR, "cajaMenor.db"), + "tabla": "movimientos", + "hoja_sheets": "CajaMenor" + }, + "libroDiario": { + "db": os.path.join(DB_DIR, "libroDiario.db"), + "tabla": "movimientos", + "hoja_sheets": "libroDiario" + } +} + +# ============ BASE DE DATOS ============ +def get_db(modulo): + """Abre la base de datos del módulo indicado""" + os.makedirs(DB_DIR, exist_ok=True) + conn = sqlite3.connect(modulo["db"]) + conn.row_factory = sqlite3.Row + return conn + +def inicializar_db(): + for modulo in MODULOS.values(): + conn = get_db(modulo) + conn.execute(f""" + CREATE TABLE IF NOT EXISTS {modulo["tabla"]} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fecha TEXT NOT NULL, + tipo TEXT NOT NULL, + descripcion TEXT NOT NULL, + monto INTEGER NOT NULL, + sincronizado INTEGER DEFAULT 0, + borrado INTEGER DEFAULT 0, + creado_en TEXT DEFAULT CURRENT_TIMESTAMP + ) + """) + conn.commit() + conn.close() + + # Migración para DBs existentes: agregar columna borrado si no existe + for modulo in MODULOS.values(): + conn = get_db(modulo) + columnas = [c[1] for c in conn.execute(f"PRAGMA table_info({modulo['tabla']})").fetchall()] + if "borrado" not in columnas: + conn.execute(f"ALTER TABLE {modulo['tabla']} ADD COLUMN borrado INTEGER DEFAULT 0") + conn.commit() + conn.close() + +# ============ SERVIR EL FRONTEND ============ +@app.route("/") +def index(): + return send_from_directory(app.static_folder, "index.html") + +@app.route("/") +def archivos_estaticos(archivo): + return send_from_directory(app.static_folder, archivo) + +# ============ LEER MOVIMIENTOS (desde SQLite - instantáneo) ============ +@app.route("/leer", methods=["GET"]) +def leer(): + nombre = request.args.get("hoja", "cajaMenor") + modulo = MODULOS.get(nombre) + if not modulo: + return jsonify({"error": f"Módulo '{nombre}' no existe"}), 400 + + conn = get_db(modulo) + filas = conn.execute( + f"SELECT * FROM {modulo['tabla']} WHERE borrado = 0 ORDER BY fecha DESC, id DESC" + ).fetchall() + conn.close() + + datos = [ + { + "id": fila["id"], + "Fecha": fila["fecha"], + "Tipo": fila["tipo"], + "Descripción": fila["descripcion"], + "Monto": fila["monto"], + "sincronizado": fila["sincronizado"] + } + for fila in filas + ] + return jsonify({"datos": datos}) + +# ============ GUARDAR MOVIMIENTO (SQLite - instantáneo) ============ +@app.route("/guardar", methods=["POST"]) +def guardar(): + body = request.json + nombre = body.get("hoja", "cajaMenor") + modulo = MODULOS.get(nombre) + if not modulo: + return jsonify({"error": f"Módulo '{nombre}' no existe"}), 400 + + datos = body.get("datos", {}) + + conn = get_db(modulo) + cursor = conn.execute( + f"INSERT INTO {modulo['tabla']} (fecha, tipo, descripcion, monto) VALUES (?, ?, ?, ?)", + ( + datos.get("Fecha", ""), + datos.get("Tipo", ""), + datos.get("Descripción", ""), + int(float(datos.get("Monto", 0))) + ) + ) + conn.commit() + nuevo_id = cursor.lastrowid + conn.close() + + return jsonify({"ok": True, "id": nuevo_id, "mensaje": "Guardado localmente, se sincronizará con Google Sheets"}) + +# ============ ACTUALIZAR MOVIMIENTO ============ +@app.route("/actualizar", methods=["PUT"]) +def actualizar(): + body = request.json + nombre = body.get("hoja") + modulo = MODULOS.get(nombre) + if not modulo: + return jsonify({"error": f"Módulo '{nombre}' no existe"}), 400 + + datos = body.get("datos", {}) + id_reg = body.get("id") + + conn = get_db(modulo) + fila = conn.execute(f"SELECT sincronizado FROM {modulo['tabla']} WHERE id = ?", (id_reg,)).fetchone() + if not fila: + conn.close() + return jsonify({"error": "Registro no encontrado"}), 404 + + conn.execute( + f"UPDATE {modulo['tabla']} SET fecha=?, tipo=?, descripcion=?, monto=? WHERE id=?", + ( + datos.get("Fecha", ""), + datos.get("Tipo", ""), + datos.get("Descripción", ""), + int(float(datos.get("Monto", 0))), + id_reg + ) + ) + # Si ya había subido a Sheets, marcar pendiente para que el sync lo actualice allá + if fila["sincronizado"] == 1: + conn.execute(f"UPDATE {modulo['tabla']} SET sincronizado = 0 WHERE id = ?", (id_reg,)) + + conn.commit() + conn.close() + return jsonify({"ok": True}) + +# ============ BORRAR MOVIMIENTO ============ +@app.route("/borrar", methods=["DELETE"]) +def borrar(): + body = request.json + nombre = body.get("hoja") + modulo = MODULOS.get(nombre) + if not modulo: + return jsonify({"error": f"Módulo '{nombre}' no existe"}), 400 + + id_reg = body.get("id") + + conn = get_db(modulo) + fila = conn.execute(f"SELECT sincronizado FROM {modulo['tabla']} WHERE id = ?", (id_reg,)).fetchone() + if not fila: + conn.close() + return jsonify({"error": "Registro no encontrado"}), 404 + + if fila["sincronizado"] == 1: + # Ya está en Sheets: soft delete, el sync lo borrará allá + conn.execute(f"UPDATE {modulo['tabla']} SET borrado = 1, sincronizado = 0 WHERE id = ?", (id_reg,)) + else: + # Nunca subió a Sheets: borrado definitivo + conn.execute(f"DELETE FROM {modulo['tabla']} WHERE id = ?", (id_reg,)) + + conn.commit() + conn.close() + return jsonify({"ok": True}) + +# ============ SINCRONIZACIÓN CON GOOGLE SHEETS (background) ============ +def sincronizar_tabla(nombre_modulo, modulo): + """Sube los pendientes de un módulo a su hoja de Google Sheets""" + conn = get_db(modulo) + pendientes = conn.execute( + f"SELECT * FROM {modulo['tabla']} WHERE sincronizado = 0" + ).fetchall() + + if not pendientes: + conn.close() + return 0 + + ids_ok = [] + for fila in pendientes: + # Determinar la acción: borrar / actualizar / nuevo + if fila["borrado"] == 1: + accion = "borrar" + datos = None + else: + # Si la última columna Sheets ya la tiene, es actualizar; si no, nuevo + # (el Apps Script busca por id; si no existe, agrega fila nueva) + accion = "actualizar" + datos = { + "Fecha": fila["fecha"], + "Tipo": fila["tipo"], + "Descripción": fila["descripcion"], + "Monto": str(fila["monto"]) + } + + payload = { + "hoja": modulo["hoja_sheets"], + "accion": accion, + "id": fila["id"], + "datos": datos + } + + try: + respuesta = requests.post(API_URL, json=payload, allow_redirects=True, timeout=30) + print(f"[SYNC] POST {nombre_modulo} id={fila['id']} status={respuesta.status_code} content-type={respuesta.headers.get('content-type','?')}") + if respuesta.status_code != 200 or "html" in respuesta.headers.get("content-type", "").lower(): + print(f"[SYNC] Respuesta inesperada (HTML?): {respuesta.text[:300]}") + continue + resultado = respuesta.json() + if resultado.get("ok"): + if fila["borrado"] == 1: + # Borrado exitoso en Sheets → eliminar definitivamente de SQLite + conn.execute(f"DELETE FROM {modulo['tabla']} WHERE id = ?", (fila["id"],)) + conn.commit() + print(f"[SYNC] {nombre_modulo}: id {fila['id']} borrado de Google Sheets ({resultado.get('mensaje', '')})") + else: + ids_ok.append(fila["id"]) + else: + print(f"[SYNC] Apps Script respondió error: {resultado}") + except Exception as e: + print(f"[SYNC] Error subiendo {nombre_modulo} id {fila['id']}: {e}") + + if ids_ok: + placeholders = ",".join("?" * len(ids_ok)) + conn.execute(f"UPDATE {modulo['tabla']} SET sincronizado = 1 WHERE id IN ({placeholders})", ids_ok) + conn.commit() + print(f"[SYNC {datetime.now().strftime('%H:%M:%S')}] {nombre_modulo}: {len(ids_ok)} registros subidos a Google Sheets") + + conn.close() + return len(pendientes) - len(ids_ok) # cuántos quedaron pendientes + +def sincronizar_pendientes(): + """Sincroniza todos los módulos con Google Sheets""" + total_pendientes = 0 + for nombre, modulo in MODULOS.items(): + total_pendientes += sincronizar_tabla(nombre, modulo) + + if total_pendientes == 0: + print(f"[SYNC {datetime.now().strftime('%H:%M:%S')}] Todo sincronizado") + return total_pendientes + +def ciclo_sincronizacion(): + """Hilo que corre la sincronización cada INTERVALO_SYNC segundos""" + while True: + time.sleep(INTERVALO_SYNC) + sincronizar_pendientes() + +# ============ FORZAR SINCRONIZACIÓN MANUAL ============ +@app.route("/sincronizar", methods=["POST"]) +def sincronizar_ahora(): + pendientes = sincronizar_pendientes() + return jsonify({"ok": True, "pendientes": pendientes}) + +# ============ MARCAR TODO COMO PENDIENTE (re-subir a Sheets) ============ +@app.route("/reset_sync", methods=["POST"]) +def reset_sync(): + """Marca todos los registros activos como no sincronizados. + Útil después de limpiar la hoja de Google para re-subir todo desde cero.""" + body = request.json or {} + nombre = body.get("hoja") + if nombre: + modulo = MODULOS.get(nombre) + if not modulo: + return jsonify({"error": f"Módulo '{nombre}' no existe"}), 400 + modulos = {nombre: modulo} + else: + modulos = MODULOS + + total = 0 + for modulo in modulos.values(): + conn = get_db(modulo) + cursor = conn.execute(f"UPDATE {modulo['tabla']} SET sincronizado = 0 WHERE borrado = 0") + total += cursor.rowcount + conn.commit() + conn.close() + + return jsonify({"ok": True, "marcados": total, "mensaje": "Ahora llama a /sincronizar para re-subir todo"}) + +# ============ CONFIGURAR URL DE GOOGLE SHEETS ============ +@app.route("/config", methods=["POST"]) +def configurar_url(): + """Permite cambiar la URL de Apps Script en runtime.""" + global API_URL + body = request.json or {} + url = body.get("api_url") + if not url: + return jsonify({"error": "Falta api_url en el body"}), 400 + API_URL = url + return jsonify({"ok": True, "api_url": API_URL}) + +# ============ PROBAR CONEXIÓN DIRECTA A GOOGLE SHEETS ============ +@app.route("/test_gs", methods=["POST"]) +def test_gs(): + """Endpoint para probar manualmente una URL de Apps Script. + Si envías api_url en el body, se usa esa URL; si no, la global. + """ + body = request.json or {} + url = body.get("api_url", API_URL) + payload = { + "hoja": body.get("hoja", "CajaMenor"), + "accion": body.get("accion", "nuevo"), + "id": body.get("id", "777"), + "datos": body.get("datos", { + "Fecha": "2026-09-03", + "Tipo": "Ingreso", + "Descripción": "Prueba backend", + "Monto": "1" + }) + } + try: + respuesta = requests.post(url, json=payload, allow_redirects=True, timeout=30) + return jsonify({ + "url_usada": url, + "status": respuesta.status_code, + "content_type": respuesta.headers.get("content-type"), + "primeros_300_chars": respuesta.text[:300], + "json_parseable": respuesta.headers.get("content-type", "").lower().startswith("application/json") + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# ============ INICIO ============ +if __name__ == "__main__": + inicializar_db() + # Lanzar el hilo de sincronización + hilo = threading.Thread(target=ciclo_sincronizacion, daemon=True) + hilo.start() + print(f"🚀 Servidor iniciado. Sincronización con Google Sheets cada {INTERVALO_SYNC // 60} minutos") + print(f"📦 Módulos activos: {', '.join(MODULOS.keys())}") + # Primera sincronización al arrancar (por si quedó algo pendiente de antes) + sincronizar_pendientes() + app.run(host="0.0.0.0", port=5000, debug=True, use_reloader=False) + diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..690a3ce --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +flask +flask-cors +requests diff --git a/db/cajaMenor.db b/db/cajaMenor.db new file mode 100644 index 0000000000000000000000000000000000000000..21e8bbd7ad42e8342901c90a475df5bf4fa0bf15 GIT binary patch literal 16384 zcmeI&!EVzq7zc2B8BAf_YZy&XYZ8kogRF6{%b`wh9Cd|2tWV=5P$##AnJAc(m2zpx{4awxU zvZY6E|9Ix?G>?wxb$AoNOpP-=ny4sKy1Knxi=aBPkb#Y3{T%G9d$X`b_vK-09+-(O z{g+F5(=5LQY~4EX*!s02vXk0_t*6bC6FR&}BA?I9OXVn+^Di=u<-)F0%VS~iIE@nN znwf1v==AYE{F`!R`?iroLK?p}r6cqK0SG_<0uX=z1Rwwb2tWV=5P-ma=L(Ur0~N-~TtGJ#+y92tWV=5P$##AOHaf MKmY;|fWR_=zd3UDCjbBd literal 0 HcmV?d00001 diff --git a/db/libroDiario.db b/db/libroDiario.db new file mode 100644 index 0000000000000000000000000000000000000000..112c10b64cc0271150bd3b45e7f630e02602ab67 GIT binary patch literal 12288 zcmeI&(M#Jv90%~bRyH%V`X;{pqzo17$l5WePs`jnifQfQ71pPcrY8oPTziQd_$GtF z{;>T&{s}v_RcpHH=05^)^@5P$##AOHafKmY;|cme|7cKGwH za+!a3@~}TtiT)BNu}X8DO@)QyKzLG+^xpe|raEZv1_xB8C z<0rEl8U!E!0SG_<0uX=z1Rwwb2tWV=%Pdgm4%^w?cAVO3)vdlgbnAz1t*uO^({M;9 zLv2RpO7+A0PO7>`Z)!)c>wnh&w`}8fnFom4AOHafKmY;|fB*y_009U<00I#BAAu4t KmYt~|Xnp}R*QQJW literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..390e632 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + app-luisa: + build: + context: . + dockerfile: Dockerfile + container_name: app-luisa + ports: + - "9007:5000" + volumes: + # Montamos el código para que los cambios se reflejen sin reconstruir + - ./backend:/app/backend + - ./fronted:/app/fronted + - ./db:/app/db + restart: unless-stopped diff --git a/fronted/acciones.js b/fronted/acciones.js new file mode 100644 index 0000000..4f6ef71 --- /dev/null +++ b/fronted/acciones.js @@ -0,0 +1,195 @@ +// ============ ACCIONES: MENÚ ⋮ Y MODALES (compartido) ============ +// Requiere que la página tenga: +// - variable global NOMBRE_HOJA +// - función global cargarMovimientos() +// - elemento #opcionesTipo con