From df9420b9549e544f7dc52b314787da3efe62bf03 Mon Sep 17 00:00:00 2001 From: Lucas Tettamanti <757326+lkzwieder@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:41:39 -0300 Subject: [PATCH] dashboard --- .../20260127100000_woo_orders_cache.sql | 90 +++ docker-compose.override.yaml | 59 ++ docker-compose.yaml | 4 +- package-lock.json | 117 ++++ package.json | 1 + public/app.js | 1 + public/components/home-dashboard.js | 622 ++++++++++++++++++ public/components/ops-shell.js | 11 +- public/components/orders-crud.js | 105 ++- public/index.html | 1 + public/lib/api.js | 16 +- public/lib/router.js | 8 +- scripts/migrate-woo-orders.mjs | 360 ++++++++++ src/modules/0-ui/controllers/testing.js | 23 +- src/modules/0-ui/handlers/stats.js | 43 ++ src/modules/0-ui/handlers/testing.js | 25 +- src/modules/1-intake/routes/simulator.js | 7 +- src/modules/4-woo-orders/ordersRepo.js | 408 ++++++++++++ src/modules/4-woo-orders/wooOrders.js | 315 ++++++--- 19 files changed, 2105 insertions(+), 111 deletions(-) create mode 100644 db/migrations/20260127100000_woo_orders_cache.sql create mode 100644 docker-compose.override.yaml create mode 100644 public/components/home-dashboard.js create mode 100644 scripts/migrate-woo-orders.mjs create mode 100644 src/modules/0-ui/handlers/stats.js create mode 100644 src/modules/4-woo-orders/ordersRepo.js diff --git a/db/migrations/20260127100000_woo_orders_cache.sql b/db/migrations/20260127100000_woo_orders_cache.sql new file mode 100644 index 0000000..5197a57 --- /dev/null +++ b/db/migrations/20260127100000_woo_orders_cache.sql @@ -0,0 +1,90 @@ +-- migrate:up + +-- Tabla de cache de pedidos de WooCommerce +-- Almacena pedidos localmente para estadísticas y listado rápido +CREATE TABLE woo_orders_cache ( + id SERIAL PRIMARY KEY, + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + woo_order_id BIGINT NOT NULL, + status VARCHAR(50), + total NUMERIC(12,2), + currency VARCHAR(10) DEFAULT 'ARS', + date_created TIMESTAMPTZ NOT NULL, + date_paid TIMESTAMPTZ, + + -- Filtros del dashboard + source VARCHAR(20) DEFAULT 'web', -- 'whatsapp' | 'web' + is_delivery BOOLEAN DEFAULT false, + is_cash BOOLEAN DEFAULT false, + + -- Cliente + customer_name VARCHAR(255), + customer_phone VARCHAR(50), + customer_email VARCHAR(255), + + -- Dirección de envío (para futuro mapa de calor) + shipping_address_1 VARCHAR(255), + shipping_address_2 VARCHAR(255), + shipping_city VARCHAR(100), + shipping_state VARCHAR(100), + shipping_postcode VARCHAR(20), + shipping_country VARCHAR(10) DEFAULT 'AR', + + -- Dirección de facturación + billing_address_1 VARCHAR(255), + billing_city VARCHAR(100), + billing_state VARCHAR(100), + billing_postcode VARCHAR(20), + + -- Raw para debugging + raw JSONB, + + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + + UNIQUE(tenant_id, woo_order_id) +); + +CREATE INDEX idx_woo_orders_tenant_date ON woo_orders_cache(tenant_id, date_created DESC); +CREATE INDEX idx_woo_orders_source ON woo_orders_cache(tenant_id, source); +CREATE INDEX idx_woo_orders_city ON woo_orders_cache(tenant_id, shipping_city); + +-- Tabla de detalle de items (productos por pedido) +-- Permite calcular stats por producto (kg vendidos, unidades, facturación) +CREATE TABLE woo_order_items ( + id SERIAL PRIMARY KEY, + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + woo_order_id BIGINT NOT NULL, + woo_product_id BIGINT, + + -- Datos del producto + product_name VARCHAR(255) NOT NULL, + sku VARCHAR(100), + + -- Cantidades y precios + quantity NUMERIC(10,3) NOT NULL, -- Soporta decimales para kg + unit_price NUMERIC(12,2), -- Precio unitario + line_total NUMERIC(12,2), -- quantity * unit_price + + -- Tipo de unidad (para stats de kg vs unidades) + sell_unit VARCHAR(20) DEFAULT 'unit', -- 'kg' | 'unit' | 'pack' + + created_at TIMESTAMPTZ DEFAULT NOW(), + + FOREIGN KEY (tenant_id, woo_order_id) + REFERENCES woo_orders_cache(tenant_id, woo_order_id) ON DELETE CASCADE +); + +CREATE INDEX idx_woo_items_order ON woo_order_items(tenant_id, woo_order_id); +CREATE INDEX idx_woo_items_product ON woo_order_items(tenant_id, woo_product_id); + +-- migrate:down + +DROP INDEX IF EXISTS idx_woo_items_product; +DROP INDEX IF EXISTS idx_woo_items_order; +DROP TABLE IF EXISTS woo_order_items; + +DROP INDEX IF EXISTS idx_woo_orders_city; +DROP INDEX IF EXISTS idx_woo_orders_source; +DROP INDEX IF EXISTS idx_woo_orders_tenant_date; +DROP TABLE IF EXISTS woo_orders_cache; diff --git a/docker-compose.override.yaml b/docker-compose.override.yaml new file mode 100644 index 0000000..c6e2e39 --- /dev/null +++ b/docker-compose.override.yaml @@ -0,0 +1,59 @@ +services: + app: + image: node:20-alpine + working_dir: /usr/src/app + command: sh -c "npm install && npm run dev" + ports: + - "3000:3000" + env_file: + - .env + environment: + - NODE_ENV=development + - PORT=3000 + - DATABASE_URL=postgres://${POSTGRES_USER:-botino}:${POSTGRES_PASSWORD:-botino}@db:5432/${POSTGRES_DB:-botino} + - REDIS_URL=redis://redis:6379 + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + restart: unless-stopped + + db: + image: postgres:16-alpine + env_file: + - .env + environment: + - POSTGRES_DB=${POSTGRES_DB:-botino} + - POSTGRES_USER=${POSTGRES_USER:-botino} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-botino} + ports: + - "5432:5432" + volumes: + - db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-botino}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + db_data: + redis_data: diff --git a/docker-compose.yaml b/docker-compose.yaml index c6e2e39..9c8342b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,8 +3,8 @@ services: image: node:20-alpine working_dir: /usr/src/app command: sh -c "npm install && npm run dev" - ports: - - "3000:3000" + expose: + - "3000" env_file: - .env environment: diff --git a/package-lock.json b/package-lock.json index 6a35e5d..0372d54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "csv-parse": "^6.1.0", "dotenv": "^17.2.3", "express": "^4.19.2", + "mysql2": "^3.16.2", "openai": "^6.15.0", "pg": "^8.16.3", "undici": "^7.16.0", @@ -1249,6 +1250,15 @@ "js-tokens": "^9.0.1" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1480,6 +1490,15 @@ "ms": "2.0.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1795,6 +1814,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1998,6 +2026,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -2073,6 +2107,27 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", + "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2199,6 +2254,54 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.16.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.2.tgz", + "integrity": "sha512-JsqBpYNy7pH20lGfPuSyRSIcCxSeAIwxWADpV64nP9KeyN3ZKpHZgjKXuBKsh7dH6FbOvf1bOgoVKjSUPXRMTw==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.3", + "named-placeholders": "^1.1.6", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.3" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2750,6 +2853,11 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", @@ -2882,6 +2990,15 @@ "node": ">= 10.x" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", diff --git a/package.json b/package.json index 2238de9..7fd888e 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "csv-parse": "^6.1.0", "dotenv": "^17.2.3", "express": "^4.19.2", + "mysql2": "^3.16.2", "openai": "^6.15.0", "pg": "^8.16.3", "undici": "^7.16.0", diff --git a/public/app.js b/public/app.js index 633fd08..fd21d20 100644 --- a/public/app.js +++ b/public/app.js @@ -1,4 +1,5 @@ import "./components/ops-shell.js"; +import "./components/home-dashboard.js"; import "./components/run-timeline.js"; import "./components/chat-simulator.js"; import "./components/conversation-inspector.js"; diff --git a/public/components/home-dashboard.js b/public/components/home-dashboard.js new file mode 100644 index 0000000..5461104 --- /dev/null +++ b/public/components/home-dashboard.js @@ -0,0 +1,622 @@ +import { api } from "../lib/api.js"; + +function formatCurrency(value) { + if (value == null) return "$0"; + return new Intl.NumberFormat("es-AR", { style: "currency", currency: "ARS", maximumFractionDigits: 0 }).format(value); +} + +function formatNumber(value) { + if (value == null) return "0"; + return new Intl.NumberFormat("es-AR", { maximumFractionDigits: 1 }).format(value); +} + +class HomeDashboard extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.stats = null; + this.loading = false; + this.charts = {}; + + this.shadowRoot.innerHTML = ` + +