diff --git a/.env.development b/.env.development index ac7c399..2ed6fad 100644 --- a/.env.development +++ b/.env.development @@ -1,54 +1,50 @@ # Archivo de variables de entorno para docker-compose.yml COMPOSE_PROJECT_NAME=suitecoffee_dev -# Entorno de desarrollo +# ======================================================= +# Runtime NODE_ENV=development MANSO_PORT=1010 # MVP Manso Microservicio -> services/manso/src/index.mjs APP_PORT=3030 # Microservicio APP-> services/app/src/index.mjs AUTH_PORT=4040 # Microservicio AUTH -> services/auth/src/index.mjs PLUGINS_PORT=5050 # Microservicio PLUGINS-> services/plugins/src/index.mjs +# ======================================================= -# ===== DB principal (metadatos de SuiteCoffee) ===== -# Usa el alias de red del servicio 'db' (compose: aliases [dev-db]) -DB_HOST=dev-db -DB_NAME=dev_suitecoffee_core -DB_PORT=5432 -DB_USER=dev-user-suitecoffee -DB_PASS=dev-pass-suitecoffee + +# ======================================================= +# Configuración de Dases de Datos CORE_DB_HOST=dev-db CORE_DB_NAME=dev_suitecoffee_core CORE_DB_PORT=5432 CORE_DB_USER=dev-user-suitecoffee CORE_DB_PASS=dev-pass-suitecoffee -# ===== DB tenants (Tenants de SuiteCoffee) ===== -TENANTS_HOST=dev-tenants -TENANTS_DB=dev_suitecoffee_tenants -TENANTS_PORT=5432 -TENANTS_USER=suitecoffee -TENANTS_PASS=suitecoffee - TENANTS_DB_HOST=dev-tenants TENANTS_DB_NAME=dev_suitecoffee_tenants TENANTS_DB_PORT=5432 TENANTS_DB_USER=suitecoffee TENANTS_DB_PASS=suitecoffee -# Authentik PostgreSQL Setup + AK_HOST_DB=ak-db AK_PG_DB=authentik AK_PG_USER=authentik AK_PG_PASS=gOWjL8V564vyh1aXUcqh4o/xo7eObraaCVZezPi3iw2LzPlU +# ======================================================= -# Authentik Cookies + + +# ======================================================= +# Authentik + # Authentik Cookies AUTHENTIK_COOKIE__DOMAIN=dev.sso.suitecoffee.uy AUTHENTIK_SECURITY__CSRF_TRUSTED_ORIGINS=https://dev.sso.suitecoffee.uy,https://dev.suitecoffee.uy -# Authentik Security + # Authentik Security AUTHENTIK_SECRET_KEY=11zMsUL57beO+okjeGh7OB3lQdGUWII+VaATHs/zsw1+6KMSTyGfAY0yHpq3C442+3CwrZ/KtjgHBfbv -# Authentik Bootstrap + # Authentik Bootstrap AUTHENTIK_BOOTSTRAP_PASSWORD=info.suitecoffee@gmail.com AUTHENTIK_BOOTSTRAP_EMAIL=info.suitecoffee@gmail.com @@ -58,4 +54,5 @@ AUTHENTIK_EMAIL__USERNAME=info.suitecoffee@gmail.com AUTHENTIK_EMAIL__PASSWORD=Succulent-Sanded7 AUTHENTIK_EMAIL__USE_TLS=true # Or false if not using TLS AUTHENTIK_EMAIL__USE_SSL=false # Or true if using SSL directly -AUTHENTIK_EMAIL__FROM=info.suitecoffee@gmail.com \ No newline at end of file +AUTHENTIK_EMAIL__FROM=info.suitecoffee@gmail.com +# ======================================================= \ No newline at end of file diff --git a/.env.production b/.env.production deleted file mode 100644 index e9c13a3..0000000 --- a/.env.production +++ /dev/null @@ -1,50 +0,0 @@ -# Archivo de variables de entorno para docker-compose.yml -COMPOSE_PROJECT_NAME=suitecoffee_ - -# Entorno de desarrollo -NODE_ENV=production - -APP_PORT=3000 # Microservicio APP-> services/app/src/index.mjs -AUTH_PORT=4000 # Microservicio AUTH -> services/auth/src/index.mjs -PLUGIN_PORT=5000 # Microservicio PLUGINS-> services/plugins/src/index.mjs - -# tenants - postgres -TENANTS_DB_NAME=suitecoffee_tenants -TENANTS_DB_USER=suitecoffee -TENANTS_DB_PASS=suitecoffee - -# db primaria - postgres -DB_NAME=suitecoffee_core -DB_USER=suitecoffee -DB_PASS=suitecoffee - -# Authentik PostgreSQL Setup -AK_HOST_DB=ak-db -AK_PG_DB=authentik -AK_PG_USER=authentik -AK_PG_PASS=gOWjL8V564vyh1aXUcqh4o/xo7eObraaCVZezPi3iw2LzPlU - -# Authentik Cookies -AUTHENTIK_COOKIE__DOMAIN=sso.suitecoffee.uy -AUTHENTIK_SECURITY__CSRF_TRUSTED_ORIGINS=https://sso.suitecoffee.uy,https://suitecoffee.uy - -# Authentik Security -AUTHENTIK_SECRET_KEY=11zMsUL57beO+okjeGh7OB3lQdGUWII+VaATHs/zsw1+6KMSTyGfAY0yHpq3C442+3CwrZ/KtjgHBfbv -AUTHENTIK_ERROR_REPORTING__ENABLED=true - -# Authentik Email - -AUTHENTIK_EMAIL__HOST=smtp.gmail.com # SMTP Host Emails are sent to -AUTHENTIK_EMAIL__PORT=465 -AUTHENTIK_EMAIL__PASSWORD=Succulent-Sanded7 - -AUTHENTIK_EMAIL__USE_TLS=false # Use StartTLS -AUTHENTIK_EMAIL__USE_SSL=true # Use SSL -AUTHENTIK_EMAIL__TIMEOUT=10 - -# Email address authentik will send from, should have a correct @domain -AUTHENTIK_EMAIL__FROM=info.suitecoffee@gmail.com - -# Authentik Bootstrap -AUTHENTIK_BOOTSTRAP_PASSWORD=info.suitecoffee@gmail.com -AUTHENTIK_BOOTSTRAP_EMAIL=info.suitecoffee@gmail.com \ No newline at end of file diff --git a/authentik-media/public/application-icons/SuiteCoffee.png b/authentik-media/public/application-icons/SuiteCoffee.png new file mode 100644 index 0000000..a0529b4 Binary files /dev/null and b/authentik-media/public/application-icons/SuiteCoffee.png differ diff --git a/authentik-media/public/application-icons/SuiteCoffee_TB_1.png b/authentik-media/public/application-icons/SuiteCoffee_TB_1.png new file mode 100644 index 0000000..56965b8 Binary files /dev/null and b/authentik-media/public/application-icons/SuiteCoffee_TB_1.png differ diff --git a/authentik-media/public/application-icons/SuiteCoffee_TM_1.png b/authentik-media/public/application-icons/SuiteCoffee_TM_1.png new file mode 100644 index 0000000..8cb2bcd Binary files /dev/null and b/authentik-media/public/application-icons/SuiteCoffee_TM_1.png differ diff --git a/backup.sql b/backup.sql new file mode 100644 index 0000000..139597f --- /dev/null +++ b/backup.sql @@ -0,0 +1,2 @@ + + diff --git a/compose.dev.yaml b/compose.dev.yaml index 322f6d8..34e22f0 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -10,10 +10,12 @@ services: volumes: - ./services/app:/app:rw - ./services/app/node_modules:/app/node_modules + - ./packages:/packages env_file: - ./services/app/.env.development environment: NODE_ENV: development # <- fuerza el entorno para que el loader tome .env.development + NODE_OPTIONS: --preserve-symlinks # la resolución por symlinks (y que @suitecoffee/db encuentre pg instalado en services/app/node_modules expose: - ${APP_PORT} networks: @@ -28,10 +30,12 @@ services: volumes: - ./services/plugins:/app:rw - ./services/plugins/node_modules:/app/node_modules + - ./packages:/packages env_file: - ./services/plugins/.env.development environment: NODE_ENV: development # <- fuerza el entorno para que el loader tome .env.development + NODE_OPTIONS: --preserve-symlinks expose: - ${PLUGINS_PORT} networks: @@ -46,10 +50,12 @@ services: volumes: - ./services/auth:/app:rw - ./services/auth/node_modules:/app/node_modules + - ./packages:/packages env_file: - ./services/auth/.env.development environment: NODE_ENV: development # <- fuerza el entorno para que el loader tome .env.development + NODE_OPTIONS: --preserve-symlinks expose: - ${AUTH_PORT} networks: @@ -60,9 +66,9 @@ services: dbCore: image: postgres:16 environment: - POSTGRES_DB: ${DB_NAME} - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASS} + POSTGRES_DB: ${CORE_DB_NAME} + POSTGRES_USER: ${CORE_DB_USER} + POSTGRES_PASSWORD: ${CORE_DB_PASS} volumes: - suitecoffee-db:/var/lib/postgresql/data networks: diff --git a/compose.yaml b/compose.yaml index c9063ab..3223461 100644 --- a/compose.yaml +++ b/compose.yaml @@ -36,7 +36,7 @@ services: dbCore: condition: service_healthy ak: - condition: service_started + condition: service_healthy healthcheck: test: ["CMD-SHELL", "curl -fsS http://localhost:${AUTH_PORT}/health || exit 1"] interval: 10s @@ -50,7 +50,7 @@ services: environment: TZ: America/Montevideo healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] + test: ["CMD-SHELL", "pg_isready -U ${CORE_DB_USER} -d ${CORE_DB_NAME}"] interval: 5s timeout: 3s retries: 20 diff --git a/packages/api/api.mjs b/packages/api/api.mjs new file mode 100644 index 0000000..83fcbd9 --- /dev/null +++ b/packages/api/api.mjs @@ -0,0 +1,5 @@ +// @suitecoffee/api/api.mjs +// packages/api/api.mjs +// Punto de entrada general del paquete de api. + +export { default as apiv1 } from './v1/apiv1.mjs'; \ No newline at end of file diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 0000000..48292e1 --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,16 @@ +{ + "name": "@suitecoffee/api", + "version": "1.0.0", + "type": "module", + "main": "./api.mjs", + "exports": { + ".": { + "import": "./api.mjs", + "default": "./api.mjs" + }, + "./package.json": "./package.json" + }, + "files": [ + ".api.mjs" + ] +} diff --git a/packages/api/v1/apiv1.mjs b/packages/api/v1/apiv1.mjs new file mode 100644 index 0000000..af1c831 --- /dev/null +++ b/packages/api/v1/apiv1.mjs @@ -0,0 +1,21 @@ +// packages/api/v1/apiv1.mjs +import { Router } from 'express'; + +// Sub-routers (cada uno define sus propios paths absolutos) +import comandasApiRouter from './routes/comandas.mjs'; +// import productosApiRouter from './routes/productos.mjs'; // cuando exista +// import clientesApiRouter from './routes/clientes.mjs'; // etc. + +const apiv1 = Router(); + +// Monta routers (no pongas prefijo aquí porque ya lo tienen adentro) +apiv1.use(comandasApiRouter); +// apiv1.use(productosApiRouter); +// apiv1.use(clientesApiRouter); + + +export default apiv1; + +// (Opcional) re-export para tests puntuales +// export { comandasApiRouter }; +// export { productosApiRouter }; diff --git a/packages/api/v1/repositories/comandasRepo.mjs b/packages/api/v1/repositories/comandasRepo.mjs new file mode 100644 index 0000000..ec2afdd --- /dev/null +++ b/packages/api/v1/repositories/comandasRepo.mjs @@ -0,0 +1,111 @@ +// packages/api/v1/repositories/comandasRepo.mjs + +import { withTenantClient } from './db.mjs'; +import { loadColumns, loadPrimaryKey } from '../routes/utils/schemaInspector.mjs'; + +const TABLE = 'comandas'; +const VALID_IDENT = /^[a-z_][a-z0-9_]*$/i; + +export async function listComandas({ schema, abierta, limit }) { + return withTenantClient(schema, async (db) => { + const max = Math.min(parseInt(limit || 200, 10), 1000); + const { rows } = await db.query( + `SELECT * FROM public.f_comandas_resumen($1, $2)`, + [abierta, max] + ); + return rows; + }); +} + +export async function getDetalleItems({ schema, id }) { + return withTenantClient(schema, async (db) => { + const { rows } = await db.query( + `SELECT id_det_comanda, id_producto, producto_nombre, + cantidad, pre_unitario, subtotal, observaciones + FROM public.v_comandas_detalle_items + WHERE id_comanda = $1::int + ORDER BY id_det_comanda`, + [id] + ); + return rows; + }); +} + +export async function abrirComanda({ schema, id }) { + return withTenantClient(schema, async (db) => { + const st = await db.query(`SELECT eliminada FROM public.${q(TABLE)} WHERE id_comanda = $1`, [id]); + if (!st.rowCount) return null; + if (st.rows[0].eliminada === true) { + const err = new Error('Comanda eliminada. Debe restaurarse antes de abrir.'); + err.http = { status: 409 }; + throw err; + } + const { rows } = await db.query(`SELECT public.f_abrir_comanda($1) AS data`, [id]); + return rows[0]?.data || null; + }); +} + +export async function cerrarComanda({ schema, id }) { + return withTenantClient(schema, async (db) => { + const { rows } = await db.query(`SELECT public.f_cerrar_comanda($1) AS data`, [id]); + return rows[0]?.data || null; + }); +} + +export async function restaurarComanda({ schema, id }) { + return withTenantClient(schema, async (db) => { + const { rows } = await db.query(`SELECT public.f_restaurar_comanda($1) AS data`, [id]); + return rows[0]?.data || null; + }); +} + +export async function eliminarComanda({ schema, id }) { + return withTenantClient(schema, async (db) => { + const { rows } = await db.query(`SELECT public.f_eliminar_comanda($1) AS data`, [id]); + return rows[0]?.data || null; + }); +} + +export async function patchComanda({ schema, id, payload }) { + return withTenantClient(schema, async (db) => { + const columns = await loadColumns(db, TABLE); + const updatable = new Set( + columns + .filter(c => + !c.is_primary && + !c.is_identity && + !(String(c.column_default || '').startsWith('nextval(')) + ) + .map(c => c.column_name) + ); + + const sets = []; + const params = []; + let idx = 1; + for (const [k, v] of Object.entries(payload || {})) { + if (!VALID_IDENT.test(k)) continue; + if (!updatable.has(k)) continue; + sets.push(`${q(k)} = $${idx++}`); + params.push(v); + } + if (!sets.length) return { error: 'Nada para actualizar' }; + + const pks = await loadPrimaryKey(db, TABLE); + if (pks.length !== 1) { + const err = new Error('PK compuesta no soportada'); + err.http = { status: 400 }; + throw err; + } + params.push(id); + + const { rows } = await db.query( + `UPDATE ${q(TABLE)} SET ${sets.join(', ')} WHERE ${q(pks[0])} = $${idx} RETURNING *`, + params + ); + return rows[0] || null; + }); +} + +function q(ident) { + return `"${String(ident).replace(/"/g, '""')}"`; +} diff --git a/packages/api/v1/repositories/db.mjs b/packages/api/v1/repositories/db.mjs new file mode 100644 index 0000000..c4c2549 --- /dev/null +++ b/packages/api/v1/repositories/db.mjs @@ -0,0 +1,29 @@ +// packages/api/v1/repositories/db.mjs + +import { poolTenants } from '@suitecoffee/db'; + +const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + +export async function withTenantClient(req, fn, { trx = false } = {}) { + const schema = req?.tenant?.schema; + if (!schema || !VALID_IDENT.test(schema)) { + throw new Error('Schema de tenant no resuelto/ inválido'); + } + const client = await poolTenants.connect(); + try { + if (trx) await client.query('BEGIN'); + await client.query(`SET LOCAL search_path = "${schema}", public`); + const result = await fn(client); + if (trx) await client.query('COMMIT'); + return result; + } catch (e) { + if (trx) await client.query('ROLLBACK'); + throw e; + } finally { + client.release(); + } +} + +export async function tquery(req, sql, params = [], opts = {}) { + return withTenantClient(req, (c) => c.query(sql, params), opts); +} diff --git a/packages/api/v1/routes/comandas.mjs b/packages/api/v1/routes/comandas.mjs new file mode 100644 index 0000000..223e19e --- /dev/null +++ b/packages/api/v1/routes/comandas.mjs @@ -0,0 +1,50 @@ +// packages/api/v1/routes/comandas.mjs + +import { Router } from 'express'; + +import { tenantContext } from '@suitecoffee/middlewares'; +import { listarComandas, detalleComanda, actualizarComanda, eliminarComanda } from './handlers/comandasHand.mjs'; + +const comandasApiRouter = Router(); + +comandasApiRouter.use(tenantContext); + +// Colección +comandasApiRouter.route('/comandas').get(listarComandas); + +// Recurso +comandasApiRouter.route('/comandas/:id').get(detalleComanda) + .put(actualizarComanda) + .delete(eliminarComanda); + +export default comandasApiRouter; + + +// ---------------------------------------------------------- +// API Comandas +/* +Escalabilidad: si luego agregás PUT /comandas/:id o DELETE /comandas/:id, +lo hacés en la misma ruta encadenando métodos: + +router + .route('/comandas/:id') + .get(detalleComanda) + .put(actualizarComanda) + .delete(eliminarComanda); + + Middleware común: podrías usar .all(requireAuth) o .all(validarTenant) si necesitás autenticación o contexto del tenant. +*/ +// ---------------------------------------------------------- +/* +router.route('/comandas').get(listarComandas); // GET /comandas + +// router.route('/comandas/:id').get(detalleComanda); // GET /comandas/:id +// router.route('/comandas/:id/abrir').post(abrirComanda); // POST /comandas/:id/abrir +// router.route('/comandas/:id/cerrar').post(cerrarComanda); // POST /comandas/:id/cerrar + +// Recurso +router.route('/comandas/:id') +.get(detalleComanda) // GET /comandas/:id +.put(actualizarComanda) // PUT /comandas/:id (accion: abrir|cerrar|restaurar) o patch genérico +.delete(eliminarComanda); // DELETE /comandas/:id -> borrado lógico (eliminada=true) +*/ diff --git a/packages/api/v1/routes/handlers/comandasHand.mjs b/packages/api/v1/routes/handlers/comandasHand.mjs new file mode 100644 index 0000000..f2a892f --- /dev/null +++ b/packages/api/v1/routes/handlers/comandasHand.mjs @@ -0,0 +1,91 @@ +// packages/api/v1/routes/handlers/comandas.js + +import { + listComandas, + getDetalleItems, + abrirComanda, + cerrarComanda, + restaurarComanda, + eliminarComanda as eliminarComandaRepo, + patchComanda +} from '../../repositories/comandasRepo.mjs'; + +const asBoolean = (v) => { + const s = (v ?? '').toString().trim().toLowerCase(); + return s === 'true' ? true : s === 'false' ? false : null; +}; + +export async function listarComandas(req, res, next) { + try { + const abierta = asBoolean(req.query.abierta); + const limit = req.query.limit; + const rows = await listComandas({ schema: req.tenant.schema, abierta, limit }); + res.json(rows); + } catch (e) { next(e); } +} + +export async function detalleComanda(req, res, next) { + try { + const id = parseId(req.params.id); + const rows = await getDetalleItems({ schema: req.tenant.schema, id }); + res.json(rows); + } catch (e) { next(e); } +} + +export async function actualizarComanda(req, res, next) { + try { + const id = parseId(req.params.id); + const { accion, ...patch } = req.body || {}; + + if (accion === 'abrir') { + const data = await abrirComanda({ schema: req.tenant.schema, id }); + return data ? res.json(data) : res.status(404).json({ error: 'Comanda no encontrada' }); + } + if (accion === 'cerrar') { + const data = await cerrarComanda({ schema: req.tenant.schema, id }); + return data ? res.json(data) : res.status(404).json({ error: 'Comanda no encontrada' }); + } + if (accion === 'restaurar') { + const data = await restaurarComanda({ schema: req.tenant.schema, id }); + return data ? res.json(data) : res.status(404).json({ error: 'Comanda no encontrada' }); + } + + const result = await patchComanda({ schema: req.tenant.schema, id, payload: patch }); + if (!result) return res.status(404).json({ error: 'Comanda no encontrada' }); + if (result?.error) return res.status(400).json({ error: result.error }); + res.json(result); + } catch (e) { + if (e?.http?.status) return res.status(e.http.status).json({ error: e.message }); + // PG codes comunes + if (e?.code === '23503') return res.status(409).json({ error: 'Violación de clave foránea', detail: e.detail }); + if (e?.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail }); + if (e?.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail }); + if (e?.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail }); + next(e); + } +} + +export async function eliminarComanda(req, res, next) { + try { + const id = parseId(req.params.id); + const data = await eliminarComandaRepo({ schema: req.tenant.schema, id }); + return data ? res.json(data) : res.status(404).json({ error: 'Comanda no encontrada' }); + } catch (e) { + if (e?.http?.status) return res.status(e.http.status).json({ error: e.message }); + if (e?.code === '23503') return res.status(409).json({ error: 'Violación de clave foránea', detail: e.detail }); + if (e?.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail }); + if (e?.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail }); + if (e?.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail }); + next(e); + } +} + +function parseId(value) { + const id = Number(value); + if (!Number.isInteger(id) || id <= 0) { + const err = new Error('id inválido'); + err.http = { status: 400 }; + throw err; + } + return id; +} diff --git a/services/app/src/api/v1/routes/routes.js b/packages/api/v1/routes/routes.js similarity index 56% rename from services/app/src/api/v1/routes/routes.js rename to packages/api/v1/routes/routes.js index c17f2cc..703d6f4 100644 --- a/services/app/src/api/v1/routes/routes.js +++ b/packages/api/v1/routes/routes.js @@ -1,93 +1,202 @@ -// services/manso/src/api/v1/routes/routes.js +// packages/api/v1/routes/routes.js import { Router } from 'express'; -import pool from '#db'; // Pool Singleton +import { withTenantClient, tquery } from '../repositories/db.mjs' +import { listarComandas, detalleComanda, actualizarComanda, eliminarComanda } from './handlers/comandasHand.mjs'; +import { loadColumns, loadForeignKeys, loadPrimaryKey, pickLabelColumn } from './utils/schemaInspector.mjs'; + const router = Router(); +const ALLOWED_TABLES = [ + 'roles', 'usuarios', 'usua_roles', + 'categorias', 'productos', + 'clientes', 'mesas', + 'comandas', 'deta_comandas', + 'proveedores', 'compras', 'deta_comp_producto', + 'mate_primas', 'deta_comp_materias', + 'prov_producto', 'prov_mate_prima', + 'receta_producto', 'asistencia_resumen_diario', + 'asistencia_intervalo', 'asistencia_detalle', + 'vw_compras' +]; + +const VALID_IDENT = /^[a-z_][a-z0-9_]*$/i; +const q = (ident) => `"${String(ident).replace(/"/g, '""')}"`; + +function ensureTable(name) { + if (!VALID_IDENT.test(name)) throw new Error('Identificador inválido'); + if (!ALLOWED_TABLES.includes(name)) throw new Error('Tabla no permitida'); + return name; +} + // ========================================================== // Rutas de API v1 // ========================================================== - - -// ---------------------------------------------------------- -// API Comandas // ---------------------------------------------------------- +// API Tablas +/*router.route('/tables').get( async (_req, res) => { + res.json(ALLOWED_TABLES); +});*/ -router.route('/comandas').get( async (req, res, next) => { +// GET /api/schema/:table → columnas + foreign keys +/*router.get('/schema/:table', async (req, res) => { try { - var client = await pool.getClient() - const estado = (req.query.estado || '').trim() || null; - const limit = Math.min(parseInt(req.query.limit || '200', 10), 1000); - - const { rows } = await client.query( - `SELECT * FROM public.f_comandas_resumen($1, $2)`, - [estado, limit] - ); - res.json(rows); + const table = ensureTable(req.params.table); + const client = await poolTenants.getClient(); + try { + const columns = await loadColumns(client, table); + const fks = await loadForeignKeys(client, table); + const enriched = columns.map(c => ({ ...c, foreign: fks[c.column_name] || null })); + res.json({ table, columns: enriched }); + } finally { + client.release(); + } } catch (e) { - next(e); - } finally { - client.release(); + res.status(400).json({ error: e.message }); + } +});*/ + +// GET /api/options/:table/:column → opciones FK +/*router.get('/options/:table/:column', async (req, res) => { + try { + const table = ensureTable(req.params.table); + const column = req.params.column; + if (!VALID_IDENT.test(column)) throw new Error('Columna inválida'); + + const client = await poolTenants.getClient(); + try { + const fks = await loadForeignKeys(client, table); + const fk = fks[column]; + if (!fk) return res.json([]); + + const refTable = fk.foreign_table; + const refId = fk.foreign_column; + const labelCol = await pickLabelColumn(client, refTable); + + const sql = `SELECT ${q(refId)} AS id, ${q(labelCol)} AS label + FROM ${q(refTable)} ORDER BY ${q(labelCol)} LIMIT 1000`; + const result = await client.query(sql); + res.json(result.rows); + } finally { + client.release(); + } + } catch (e) { + res.status(400).json({ error: e.message }); + } +});*/ + +// GET /api/table/:table → preview de datos +/*router.get('/table/:table', async (req, res) => { + try { + const table = ensureTable(req.params.table); + const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000); + await withTenantClient(req, async (client) => { + const pks = await loadPrimaryKey(client, table); + const orderBy = pks.length ? `ORDER BY ${pks.map(q).join(', ')} DESC` : ''; + const sql = `SELECT * FROM ${q(table)} ${orderBy} LIMIT ${limit}`; + const { rows } = await client.query(sql); + res.json(rows); + }); + } catch (e) { + res.status(400).json({ error: e.message, code: e.code, detail: e.detail }); + } +});*/ + +// POST /api/table/:table → insertar fila +/*router.post('/table/:table', async (req, res) => { + const table = ensureTable(req.params.table); + const payload = req.body || {}; + try { + const client = await poolTenants.getClient(); + try { + const columns = await loadColumns(client, table); + const insertable = columns.filter(c => + !c.is_primary && + !c.is_identity && + !(c.column_default || '').startsWith('nextval(') + ); + const allowedCols = new Set(insertable.map(c => c.column_name)); + + const cols = []; + const vals = []; + const params = []; + let idx = 1; + for (const [k, v] of Object.entries(payload)) { + if (!allowedCols.has(k)) continue; + if (!VALID_IDENT.test(k)) continue; + cols.push(q(k)); + vals.push(`$${idx++}`); + params.push(v); + } + + let rows; + if (!cols.length) { + ({ rows } = await client.query( + `INSERT INTO ${q(table)} DEFAULT VALUES RETURNING *` + )); + } else { + ({ rows } = await client.query( + `INSERT INTO ${q(table)} (${cols.join(', ')}) + VALUES (${vals.join(', ')}) RETURNING *`, + params + )); + } + res.status(201).json({ inserted: rows[0] }); + } catch (e) { + if (e.code === '23503') return res.status(400).json({ error: 'Violación de clave foránea', detail: e.detail }); + if (e.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail }); + if (e.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail }); + if (e.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail }); + throw e; + } finally { + client.release(); + } + } catch (e) { + res.status(400).json({ error: e.message }); } }); +*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -router.route('/comandas/:id/detalle').get( async (req, res, next) => { - try { - const client = await pool.getClient() - client.query( - `SELECT id_det_comanda, id_producto, producto_nombre, - cantidad, pre_unitario, subtotal, observaciones - FROM public.v_comandas_detalle_items - WHERE id_comanda = $1::int - ORDER BY id_det_comanda`, - [req.params.id] - ) - .then(r => res.json(r.rows)) - .catch(next) - client.release(); - } catch (error) { - next(e); - } -}); -router.route('/comandas/:id/cerrar').post( async (req, res, next) => { - try { - const client = await pool.getClient() - const id = Number(req.params.id); - if (!Number.isInteger(id) || id <= 0) { - return res.status(400).json({ error: 'id inválido' }); - } - const { rows } = await client.query( - `SELECT public.f_cerrar_comanda($1) AS data`, - [id] - ); - if (!rows.length || rows[0].data === null) { - return res.status(404).json({ error: 'Comanda no encontrada' }); - } - res.json(rows[0].data); - client.release(); - } catch (err) { next(err); } -}); -router.route('/comandas/:id/abrir').post( async (req, res, next) => { - try { - const client = await pool.getClient() - const id = Number(req.params.id); - if (!Number.isInteger(id) || id <= 0) { - return res.status(400).json({ error: 'id inválido' }); - } - const { rows } = await client.query( - `SELECT public.f_abrir_comanda($1) AS data`, - [id] - ); - if (!rows.length || rows[0].data === null) { - return res.status(404).json({ error: 'Comanda no encontrada' }); - } - res.json(rows[0].data); - client.release(); - } catch (err) { next(err); } -}); @@ -96,19 +205,19 @@ router.route('/comandas/:id/abrir').post( async (req, res, next) => { // ---------------------------------------------------------- // GET producto + receta -router.route('/rpc/get_producto/:id').get( async (req, res) => { - const client = await pool.getClient() +/*router.route('/rpc/get_producto/:id').get( async (req, res) => { + const client = await poolTenants.getClient() const id = Number(req.params.id); const { rows } = await client.query('SELECT public.get_producto($1) AS data', [id]); res.json(rows[0]?.data || {}); client.release(); -}); +});*/ // POST guardar producto + receta -router.route('/rpc/save_producto').post(async (req, res) => { +/*router.route('/rpc/save_producto').post(async (req, res) => { try { // console.debug('receta payload:', req.body?.receta); // habilitalo si lo necesitás - const client = await pool.getClient() + const client = await poolTenants.getClient() const q = 'SELECT public.save_producto($1,$2,$3,$4,$5,$6,$7::jsonb) AS id_producto'; const { id_producto=null, nombre, img_producto=null, precio=0, activo=true, id_categoria=null, receta=[] } = req.body || {}; const params = [id_producto, nombre, img_producto, precio, activo, id_categoria, JSON.stringify(receta||[])]; @@ -119,7 +228,7 @@ router.route('/rpc/save_producto').post(async (req, res) => { console.error(e); res.status(500).json({ error: 'save_producto failed' }); } -}); +});*/ @@ -128,10 +237,10 @@ router.route('/rpc/save_producto').post(async (req, res) => { // ---------------------------------------------------------- // GET MP + proveedores -router.route('/rpc/get_materia/:id').get(async (req, res) => { +/*router.route('/rpc/get_materia/:id').get(async (req, res) => { const id = Number(req.params.id); try { - const client = await pool.getClient() + const client = await poolTenants.getClient() const { rows } = await client.query('SELECT public.get_materia_prima($1) AS data', [id]); res.json(rows[0]?.data || {}); client.release(); @@ -147,7 +256,7 @@ router.route('/rpc/save_materia').post( async (req, res) => { try { const q = 'SELECT public.save_materia_prima($1,$2,$3,$4,$5::jsonb) AS id_mat_prima'; const params = [id_mat_prima, nombre, unidad, activo, JSON.stringify(proveedores||[])]; - const { rows } = await pool.query(q, params); + const { rows } = await poolTenants.query(q, params); res.json(rows[0] || {}); } catch (e) { console.error(e); @@ -166,7 +275,7 @@ router.route('/rpc/find_usuarios_por_documentos').post( async (req, res) => { try { const docs = Array.isArray(req.body?.docs) ? req.body.docs : []; const sql = 'SELECT public.find_usuarios_por_documentos($1::jsonb) AS data'; - const { rows } = await pool.query(sql, [JSON.stringify(docs)]); + const { rows } = await poolTenants.query(sql, [JSON.stringify(docs)]); res.json(rows[0]?.data || {}); } catch (e) { console.error(e); @@ -180,7 +289,7 @@ router.route('/rpc/import_asistencia').post( async (req, res) => { const registros = Array.isArray(req.body?.registros) ? req.body.registros : []; const origen = req.body?.origen || null; const sql = 'SELECT public.import_asistencia($1::jsonb,$2) AS data'; - const { rows } = await pool.query(sql, [JSON.stringify(registros), origen]); + const { rows } = await poolTenants.query(sql, [JSON.stringify(registros), origen]); res.json(rows[0]?.data || {}); } catch (e) { console.error(e); @@ -193,7 +302,7 @@ router.route('/rpc/asistencia_get').post( async (req, res) => { try { const { doc, desde, hasta } = req.body || {}; const sql = 'SELECT public.asistencia_get($1::text,$2::date,$3::date) AS data'; - const { rows } = await pool.query(sql, [doc, desde, hasta]); + const { rows } = await poolTenants.query(sql, [doc, desde, hasta]); res.json(rows[0]?.data || {}); } catch (e) { console.error(e); res.status(500).json({ error: 'asistencia_get failed' }); @@ -205,7 +314,7 @@ router.route('/rpc/asistencia_update_raw').post( async (req, res) => { try { const { id_raw, fecha, hora, modo } = req.body || {}; const sql = 'SELECT public.asistencia_update_raw($1::bigint,$2::date,$3::text,$4::text) AS data'; - const { rows } = await pool.query(sql, [id_raw, fecha, hora, modo ?? null]); + const { rows } = await poolTenants.query(sql, [id_raw, fecha, hora, modo ?? null]); res.json(rows[0]?.data || {}); } catch (e) { console.error(e); res.status(500).json({ error: 'asistencia_update_raw failed' }); @@ -217,7 +326,7 @@ router.route('/rpc/asistencia_delete_raw').post( async (req, res) => { try { const { id_raw } = req.body || {}; const sql = 'SELECT public.asistencia_delete_raw($1::bigint) AS data'; - const { rows } = await pool.query(sql, [id_raw]); + const { rows } = await poolTenants.query(sql, [id_raw]); res.json(rows[0]?.data || {}); } catch (e) { console.error(e); res.status(500).json({ error: 'asistencia_delete_raw failed' }); @@ -237,7 +346,7 @@ router.route('/rpc/report_tickets').post( async (req, res) => { ? y : (new Date()).getFullYear(); - const { rows } = await pool.query( + const { rows } = await poolTenants.query( 'SELECT public.report_tickets_year($1::int) AS j', [year] ); res.json(rows[0].j); @@ -263,7 +372,7 @@ router.route('/rpc/report_asistencia').post( async (req, res) => { hasta = end.toISOString().slice(0,10); } - const { rows } = await pool.query( + const { rows } = await poolTenants.query( 'SELECT public.report_asistencia($1::date,$2::date) AS j', [desde, hasta] ); res.json(rows[0].j); @@ -286,7 +395,7 @@ router.route('/rpc/save_compra').post( async (req, res) => { const { id_compra, id_proveedor, fec_compra, detalles } = req.body || {}; const sql = 'SELECT * FROM public.save_compra($1::int,$2::int,$3::timestamptz,$4::jsonb)'; const args = [id_compra ?? null, id_proveedor, fec_compra ? new Date(fec_compra) : null, JSON.stringify(detalles)]; - const { rows } = await pool.query(sql, args); + const { rows } = await poolTenants.query(sql, args); res.json(rows[0]); // { id_compra, total } } catch (e) { console.error('save_compra error:', e); @@ -300,7 +409,7 @@ router.route('/rpc/get_compra').post( async (req, res) => { try { const { id_compra } = req.body || {}; const sql = `SELECT public.get_compra($1::int) AS data`; - const { rows } = await pool.query(sql, [id_compra]); + const { rows } = await poolTenants.query(sql, [id_compra]); res.json(rows[0]?.data || {}); } catch (e) { console.error(e); res.status(500).json({ error: 'get_compra failed' }); @@ -311,7 +420,7 @@ router.route('/rpc/get_compra').post( async (req, res) => { router.route('/rpc/delete_compra').post( async (req, res) => { try { const { id_compra } = req.body || {}; - await pool.query(`SELECT public.delete_compra($1::int)`, [id_compra]); + await poolTenants.query(`SELECT public.delete_compra($1::int)`, [id_compra]); res.json({ ok: true }); } catch (e) { console.error(e); res.status(500).json({ error: 'delete_compra failed' }); @@ -323,7 +432,7 @@ router.route('/rpc/delete_compra').post( async (req, res) => { router.route('/rpc/report_gastos').post( async (req, res) => { try { const year = parseInt(req.body?.year ?? new Date().getFullYear(), 10); - const { rows } = await pool.query( + const { rows } = await poolTenants.query( 'SELECT public.report_gastos($1::int) AS j', [year] ); res.json(rows[0].j); @@ -334,7 +443,7 @@ router.route('/rpc/report_gastos').post( async (req, res) => { message: e.message, detail: e.detail, code: e.code }); } -}); +});*/ export default router; \ No newline at end of file diff --git a/services/app/src/utilities/cargaEnVista.js b/packages/api/v1/routes/utils/schemaInspector.mjs similarity index 85% rename from services/app/src/utilities/cargaEnVista.js rename to packages/api/v1/routes/utils/schemaInspector.mjs index 9acc5e8..6f96ee2 100644 --- a/services/app/src/utilities/cargaEnVista.js +++ b/packages/api/v1/routes/utils/schemaInspector.mjs @@ -1,6 +1,6 @@ -// ---------------------------------------------------------- -// Introspección de esquema -// ---------------------------------------------------------- +// services/app/src/api/v1/routes/utils/schemaInspector.mjs +// Utilidades para inspeccionar columnas, claves y relaciones en PostgreSQL. + export async function loadColumns(client, table) { const sql = ` SELECT @@ -42,7 +42,8 @@ export async function loadForeignKeys(client, table) { `; const { rows } = await client.query(sql, [table]); const map = {}; - for (const r of rows) map[r.column_name] = { foreign_table: r.foreign_table, foreign_column: r.foreign_column }; + for (const r of rows) + map[r.column_name] = { foreign_table: r.foreign_table, foreign_column: r.foreign_column }; return map; } @@ -58,18 +59,17 @@ export async function loadPrimaryKey(client, table) { return rows.map(r => r.column_name); } -// label column for FK options export async function pickLabelColumn(client, refTable) { - const preferred = ['nombre', 'raz_social', 'apodo', 'documento', 'correo', 'telefono']; + const preferred = ['nombre','raz_social','apodo','documento','correo','telefono']; const { rows } = await client.query( `SELECT column_name, data_type FROM information_schema.columns WHERE table_schema='public' AND table_name=$1 ORDER BY ordinal_position`, [refTable] ); - for (const cand of preferred) { + for (const cand of preferred) if (rows.find(r => r.column_name === cand)) return cand; - } + const textish = rows.find(r => /text|character varying|varchar/i.test(r.data_type)); if (textish) return textish.column_name; return rows[0]?.column_name || 'id'; diff --git a/packages/core/db/package.json b/packages/core/db/package.json new file mode 100644 index 0000000..538c306 --- /dev/null +++ b/packages/core/db/package.json @@ -0,0 +1,22 @@ +{ + "name": "@suitecoffee/db", + "version": "1.0.0", + "type": "module", + "main": "./poolSingleton.mjs", + "types": "./poolSingleton.d.ts", + "exports": { + ".": { + "types": "./poolSingleton.d.ts", + "import": "./poolSingleton.mjs", + "default": "./poolSingleton.mjs" + }, + "./package.json": "./package.json" + }, + "peerDependencies": { + "pg": "^8.16.3" + }, + "files": [ + "poolSingleton.mjs", + "poolSingleton.d.ts" + ] +} diff --git a/packages/core/db/poolSingleton.d.ts b/packages/core/db/poolSingleton.d.ts new file mode 100644 index 0000000..654e9aa --- /dev/null +++ b/packages/core/db/poolSingleton.d.ts @@ -0,0 +1,68 @@ +// packages/core/db/poolSingleton.d.ts +// Declaraciones de tipos para @suitecoffee/db +// Refleja el módulo ESM que expone poolCore y poolTenants (ambos Singletons) + +import type { + Pool, + PoolClient, + PoolConfig, + QueryResult, + QueryResultRow, + QueryConfig +} from 'pg'; + +export type { Pool, PoolClient, PoolConfig, QueryResult, QueryResultRow, QueryConfig }; + +// Clases modeladas según la implementación JS (no se exportan como valores en runtime, +// pero se exponen como tipos para el consumidor que quiera tipar sus variables). +export declare class DatabaseCore { + /** Instancia singleton interna (solo informativa para tipado). */ + static instance?: DatabaseCore; + + /** Pool real de `pg`. */ + connection: Pool; + + constructor(); + + /** Ejecuta una consulta utilizando el pool. */ + query( + sql: string | QueryConfig, + params?: any[] + ): Promise>; + + /** Alias al `pool.connect()`; devuelve un `PoolClient`. */ + connect(): Promise; + + /** Alias al `pool.connect()`; devuelve un `PoolClient`. */ + getClient(): Promise; + + /** Cierra el pool subyacente. */ + release(): Promise; +} + +export declare class DatabaseTenants { + static instance?: DatabaseTenants; + connection: Pool; + + constructor(); + + query( + sql: string | QueryConfig, + params?: any[] + ): Promise>; + + connect(): Promise; + getClient(): Promise; + release(): Promise; +} + +/** Singletons creados por el módulo. */ +export declare const poolCore: DatabaseCore; +export declare const poolTenants: DatabaseTenants; + +/** Export por defecto del módulo: objeto con ambos pools. */ +declare const _default: { + poolCore: DatabaseCore; + poolTenants: DatabaseTenants; +}; +export default _default; diff --git a/packages/core/db/poolSingleton.mjs b/packages/core/db/poolSingleton.mjs new file mode 100644 index 0000000..35a4ac7 --- /dev/null +++ b/packages/core/db/poolSingleton.mjs @@ -0,0 +1,148 @@ +// poolSingleton.mjs +// Conexión Singleton a base de datos (pg/Pool) para CORE y TENANTS. +// Cambios mínimos respecto a tu versión original. + +import { Pool } from 'pg'; + +// Utilidad mínima para booleans +const isTrue = (v) => String(v).toLowerCase() === 'true'; + +// --------------------- CORE --------------------- +class DatabaseCore { + static instance = null; + + constructor() { + if (DatabaseCore.instance) { + return DatabaseCore.instance; // <-- corrección: antes devolvía Database.instance + } + + const host = process.env.CORE_DB_HOST; + const user = process.env.CORE_DB_USER; + const password = process.env.CORE_DB_PASS; + const database = process.env.CORE_DB_NAME; + const port = process.env.CORE_DB_PORT; + const ssl = + isTrue(process.env.CORE_PGSSL ?? process.env.PGSSL) + ? { rejectUnauthorized: false } + : undefined; + + const config = { + host, + user, + password, + database, + port: port ? Number(port) : undefined, + ssl, + }; + + this.host = host; + this.dbName = database; + this.connection = new Pool(config); + + DatabaseCore.instance = this; + } + + async query(sql, params) { + return this.connection.query(sql, params); + } + + async connect() { // idempotente a nivel de pool; retorna un client + return this.connection.connect(); + } + + async getClient() { // alias simple, conserva tu API + return this.connection.connect(); + } + + async release() { // cierra TODO el pool (uso excepcional) + await this.connection.end(); + } +} + +// --------------------- TENANTS --------------------- +class DatabaseTenants { + static instance = null; + + constructor() { + if (DatabaseTenants.instance) { + return DatabaseTenants.instance; // <-- corrección: antes devolvía Database.instance + } + + const host = process.env.TENANTS_DB_HOST; + const user = process.env.TENANTS_DB_USER; + const password = process.env.TENANTS_DB_PASS; + const database = process.env.TENANTS_DB_NAME; + const port = process.env.TENANTS_DB_PORT; + const ssl = + isTrue(process.env.TENANTS_PGSSL ?? process.env.PGSSL) + ? { rejectUnauthorized: false } + : undefined; + + const config = { + host, + user, + password, + database, + port: port ? Number(port) : undefined, + ssl, + }; + + this.host = host; + this.dbName = database; + this.connection = new Pool(config); + + DatabaseTenants.instance = this; + } + + async query(sql, params) { + return this.connection.query(sql, params); + } + + async connect() { // idempotente a nivel de pool; retorna un client + return this.connection.connect(); + } + + async getClient() { // alias simple, conserva tu API + return this.connection.connect(); + } + + async release() { // cierra TODO el pool (uso excepcional) + await this.connection.end(); + } +} + +// Instancias únicas por el cache de módulos de Node/ESM + guardas estáticas +const poolCore = new DatabaseCore(); +const poolTenants = new DatabaseTenants(); + +// --------------------- Healthchecks aquí dentro --------------------- +async function verificarConexionCore() { + try { + console.log(`[ PG ] Comprobando accesibilidad a la db ${poolCore.dbName} del host ${poolCore.host} ...`); + const client = await poolCore.getClient(); + const { rows } = await client.query('SELECT NOW() AS ahora'); + console.log(`[ PG ] Conexión con ${poolCore.dbName} OK. Hora DB:`, rows[0].ahora); + client.release(); + } catch (error) { + console.error('[ PG ] Error al conectar con la base de datos al iniciar:', error.message); + console.error('[ PG ] Revisar credenciales, accesos de red y firewall.'); + } +} + +async function verificarConexionTenants() { + try { + console.log(`[ PG ] Comprobando accesibilidad a la db ${poolTenants.dbName} del host ${poolTenants.host} ...`); + const client = await poolTenants.getClient(); + const { rows } = await client.query('SELECT NOW() AS ahora'); + console.log(`[ PG ] Conexión con ${poolTenants.dbName} OK. Hora DB:`, rows[0].ahora); + client.release(); + } catch (error) { + console.error('[ PG ] Error al conectar con la base de datos al iniciar:', error.message); + console.error('[ PG ] Revisar credenciales, accesos de red y firewall.'); + } +} + +// Exports (mantengo tu patrón) +export default { poolCore, poolTenants, verificarConexionCore, verificarConexionTenants }; +export { poolCore, poolTenants, verificarConexionCore, verificarConexionTenants }; +// export { DatabaseCore, DatabaseTenants }; // si lo necesitás para tests \ No newline at end of file diff --git a/packages/core/middlewares/datosGlobales.mjs b/packages/core/middlewares/datosGlobales.mjs new file mode 100644 index 0000000..8740473 --- /dev/null +++ b/packages/core/middlewares/datosGlobales.mjs @@ -0,0 +1,14 @@ +// @suitecoffee/middlewares/datosGlobales.mjs +// packages/core/middlewares/datosGlobales.mjs + +import { Router } from 'express'; +export const datosGlobales = Router(); + +datosGlobales.use((req, res, next) => { + res.locals.currentPath = req.path; + res.locals.pageTitle = 'SuiteCoffee'; + res.locals.pageId = ''; + next(); +}); + +export default datosGlobales; // opcional, pero útil si alguien quiere import default diff --git a/packages/core/middlewares/index.mjs b/packages/core/middlewares/index.mjs new file mode 100644 index 0000000..5923aa3 --- /dev/null +++ b/packages/core/middlewares/index.mjs @@ -0,0 +1,7 @@ +// @suitecoffee/middlewares/src/index.mjs +// Punto de entrada general del paquete de middlewares. + +export * from './requireAuth.mjs'; +export * from './datosGlobales.mjs'; +export * from './tenantContext.mjs'; +export * from './resolveTenantFromCore.mjs'; \ No newline at end of file diff --git a/packages/core/middlewares/package.json b/packages/core/middlewares/package.json new file mode 100644 index 0000000..263cd0c --- /dev/null +++ b/packages/core/middlewares/package.json @@ -0,0 +1,16 @@ +{ + "name": "@suitecoffee/middlewares", + "version": "1.0.0", + "type": "module", + "main": ".index.mjs", + "exports": { + ".": { + "import": "./index.mjs", + "default": "./index.mjs" + }, + "./package.json": "./package.json" + }, + "files": [ + ".index.mjs" + ] +} diff --git a/packages/core/middlewares/requireAuth.mjs b/packages/core/middlewares/requireAuth.mjs new file mode 100644 index 0000000..494e91a --- /dev/null +++ b/packages/core/middlewares/requireAuth.mjs @@ -0,0 +1,43 @@ +// packages/core/middlewares/src/requireAuth.mjs +// @suitecoffee/middlewares/src/requireAuth.mjs + +/** + * requireAuth + * Verifica que exista una sesión válida en req.session.user (con `sub`). + * - Si hay sesión, llama a next(). + * - Si no hay sesión: + * - Si se define `redirectTo`, redirige (302) cuando el cliente acepta HTML. + * - En caso contrario, responde 401 con { error: 'unauthenticated' }. + * + * @param {Object} [options] + * @param {string|null} [options.redirectTo=null] Ruta a la que redirigir si no hay sesión (p.ej. '/auth/login') + * @param {(req: import('express').Request) => any} [options.getSessionUser] Cómo leer el usuario de la sesión + * @returns {import('express').RequestHandler} + * + * Uso típico: + * import { requireAuth } from '@suitecoffee/middlewares'; + * app.get('/me', requireAuth(), (req,res)=> res.json({ user: req.session.user })); + * app.get('/dashboard', requireAuth({ redirectTo: '/auth/login' }), handler); + */ +export function requireAuth(options = {}) { + const { + redirectTo = null, + getSessionUser = (req) => req?.session?.user, + } = options; + + return function requireAuthMiddleware(req, res, next) { + const user = getSessionUser(req); + + if (user && user.sub) { + return next(); + } + + // Si el cliente acepta HTML y tenemos redirectTo, redirigimos (útil para front web) + if (redirectTo && req.accepts('html')) { + return res.redirect(302, redirectTo); + } + + // Fallback JSON + return res.status(401).json({ error: 'unauthenticated' }); + }; +} diff --git a/packages/core/middlewares/resolveTenantFromCore.mjs b/packages/core/middlewares/resolveTenantFromCore.mjs new file mode 100644 index 0000000..47ff59f --- /dev/null +++ b/packages/core/middlewares/resolveTenantFromCore.mjs @@ -0,0 +1,140 @@ +// packages/core/middlewares/resolveTenantFromCore.mjs +import { poolCore, poolTenants } from '@suitecoffee/db'; + +const UUID_RX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +/** + * Verifica si existe el esquema en la DB de tenants. + * No requiere setear search_path. + */ +async function schemaExists(schemaName) { + if (!schemaName) return false; + const q = ` + SELECT 1 + FROM information_schema.schemata + WHERE schema_name = $1 + LIMIT 1 + `; + const { rowCount } = await poolTenants.query(q, [schemaName]); + return rowCount === 1; +} + +/** + * Devuelve { id, schema } o null. + * Reglas: + * 1) Si el usuario tiene default_tenant => usarlo (y validar estado y existencia del schema). + * 2) Si no, buscar membresías: + * - si hay exactamente 1 => usarla (validando schema). + * - si hay 0 o >1 => devolver null (forzar selección explícita). + * + * @param {import('express').Request} req + * @param {any} sess (req.session) + * @param {Object} [opts] + * @param {boolean} [opts.debug=false] + * @param {Console} [opts.logger=console] + * @param {string[]} [opts.acceptStates=['ready']] // estados de sc_tenants aceptados + * @returns {Promise<{id:string, schema:string} | null>} + */ +export async function resolveTenantFromCore(req, sess, opts = {}) { + const { + debug = false, + logger = console, + acceptStates = ['ready'], + } = opts; + + const log = (msg, obj) => { + if (debug) logger.debug?.(`[resolveTenantFromCore] ${msg}`, obj ?? ''); + }; + + const sub = sess?.user?.sub; + if (!sub) { + log('no-sub-in-session'); + return null; + } + + try { + // 1) sc_users: obtener user_id y default_tenant + const uSql = ` + SELECT user_id, default_tenant + FROM sc_users + WHERE sub = $1 + LIMIT 1 + `; + const ures = await poolCore.query(uSql, [sub]); + if (ures.rowCount === 0) { + log('user-not-found', { sub }); + return null; + } + + const { user_id, default_tenant } = ures.rows[0] ?? {}; + + // Helper para validar fila de tenant y existencia de schema + const validateTenantRow = async (row) => { + if (!row) return null; + const { tenant_id, schema_name, state } = row; + if (!UUID_RX.test(String(tenant_id))) return null; + if (!schema_name) return null; + if (acceptStates.length && !acceptStates.includes(String(state))) return null; + + // Comprobar que el schema exista realmente en la DB de tenants + const exists = await schemaExists(schema_name); + if (!exists) { + log('schema-missing-in-tenants-db', { schema_name }); + return null; + } + return { id: String(tenant_id), schema: String(schema_name) }; + }; + + // 2) Si hay default_tenant, cargar su schema y validar + if (default_tenant) { + const tSql = ` + SELECT tenant_id, schema_name, state + FROM sc_tenants + WHERE tenant_id = $1 + LIMIT 1 + `; + const tres = await poolCore.query(tSql, [default_tenant]); + if (tres.rowCount === 1) { + const ok = await validateTenantRow(tres.rows[0]); + if (ok) { + sess.tenant = ok; + log('resolved-from-default_tenant', ok); + return ok; + } + // default_tenant roto → seguimos a membresías + log('default_tenant-invalid', { default_tenant }); + } + } + + // 3) Sin default_tenant válido: ver membresías (aceptando sólo tenants en estados permitidos) + const mSql = ` + SELECT m.tenant_id, t.schema_name, t.state, t.created_at, m.role + FROM sc_memberships m + JOIN sc_tenants t USING (tenant_id) + WHERE m.user_id = $1 + ${acceptStates.length ? `AND t.state = ANY($2)` : ''} + ORDER BY (m.role = 'owner') DESC, t.created_at ASC + LIMIT 2 + `; + const mParams = acceptStates.length ? [user_id, acceptStates] : [user_id]; + const mres = await poolCore.query(mSql, mParams); + + if (mres.rowCount === 1) { + const ok = await validateTenantRow(mres.rows[0]); + if (ok) { + sess.tenant = ok; + log('resolved-from-single-membership', ok); + return ok; + } + log('single-membership-invalid-row', mres.rows[0]); + return null; + } + + // 0 o >1 membresías → el usuario debe elegir explícitamente + log('ambiguous-or-no-memberships', { count: mres.rowCount }); + return null; + } catch (err) { + logger.error?.('[resolveTenantFromCore] error', { message: err?.message }); + return null; // preferimos no romper el request; el middleware decidirá + } +} diff --git a/packages/core/middlewares/tenantContext.mjs b/packages/core/middlewares/tenantContext.mjs new file mode 100644 index 0000000..c583c83 --- /dev/null +++ b/packages/core/middlewares/tenantContext.mjs @@ -0,0 +1,155 @@ +// packages/core/middlewares/src/tenantContext.mjs + +const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/; // schema seguro +const UUID_RX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +function redact(obj) { + // Evita loggear datos sensibles; muestra sólo lo útil para diagnóstico + if (!obj || typeof obj !== 'object') return obj; + const out = {}; + for (const k of Object.keys(obj)) { + if (['token', 'access_token', 'id_token', 'refresh_token'].includes(k)) { + out[k] = '[redacted]'; + } else if (k === 'sub' || k === 'email' || k === 'name') { + out[k] = obj[k]; + } else if (k === 'tenant') { + const t = obj[k] || {}; + out[k] = { id: t.id ?? null, schema: t.schema ?? null }; + } else if (k === 'user') { + const u = obj[k] || {}; + out[k] = { + sub: u.sub ?? null, + email: u.email ?? null, + default_tenant: u.default_tenant ?? u.defaultTenant ?? null, + memberships: Array.isArray(u.memberships) ? `[${u.memberships.length}]` : null, + }; + } else { + // no inundar el log; deja constancia de que existe + out[k] = '[present]'; + } + } + return out; +} + +export function tenantContext(opts = {}) { + const { + requireUser = true, + debug = false, + log = console, // podés inyectar tu logger + autoDeriveFromDefault = true, + // callback opcional para buscar tenant (p.ej., en CORE) si no está en sesión + // Debe devolver { id: uuid, schema: string } o null + resolveTenant = null, + schemaPrefixes = [ + process.env.TENANT_SCHEMA_PREFIX || 'empresa_', + ].filter(Boolean), + } = opts; + + const diag = (msg, data) => { + if (!debug) return; + try { log.debug?.(`[tenantContext] ${msg}`, data !== undefined ? redact(data) : ''); } + catch { /* noop */ } + }; + const setDiagHeader = (res, kv) => { + if (!debug) return; + const cur = res.getHeader('X-Tenant-Diag'); + const base = typeof cur === 'string' ? String(cur) + '; ' : ''; + res.setHeader('X-Tenant-Diag', base + kv); + }; + + return async (req, res, next) => { + try { + diag('incoming', { sid: req.sessionID, headers: { accept: req.headers.accept } }); + + const sess = req.session; + if (!sess) { + setDiagHeader(res, 'no-session'); + return res.status(401).json({ error: 'unauthenticated' }); + } + diag('session.present', { keys: Object.keys(sess) }); + + if (requireUser && !sess.user?.sub) { + diag('user.missing', { session: sess }); + setDiagHeader(res, 'no-user'); + return res.status(401).json({ error: 'unauthenticated' }); + } + if (requireUser) diag('user.ok', sess.user); + + // 1) Leer tenant desde sesión + let t = sess.tenant ?? null; + diag('session.tenant', t); + + // 2) Derivar automáticamente si falta + if ((!t?.id || !t?.schema) && autoDeriveFromDefault) { + const fallbackId = + sess.user?.tenant?.id || + sess.user?.default_tenant || + sess.user?.defaultTenant || + null; + + if (fallbackId && UUID_RX.test(String(fallbackId))) { + const prefix = String(schemaPrefixes[0] || 'empresa_'); + const schema = `${prefix}${String(fallbackId).replace(/-/g, '').toLowerCase()}`; + t = { id: String(fallbackId), schema }; + sess.tenant = t; // persistir para siguientes requests + diag('derived.fromDefault', t); + setDiagHeader(res, 'derived-default'); + } else { + diag('derived.fromDefault.skipped', { fallbackId }); + } + } + + // 3) Resolver con callback si aún falta + if ((!t?.id || !t?.schema) && typeof resolveTenant === 'function') { + try { + t = await resolveTenant(req, sess); + if (t) { + sess.tenant = t; + diag('derived.fromResolver', t); + setDiagHeader(res, 'derived-resolver'); + } else { + diag('resolver.returned-null'); + } + } catch (e) { + diag('resolver.error', { message: e?.message }); + } + } + + // 4) Validaciones + if (!t?.id || !t?.schema) { + diag('missing-tenant.final'); + setDiagHeader(res, 'missing-tenant'); + return res.status(401).json({ error: 'Sesión inválida o tenant no seleccionado' }); + } + if (!UUID_RX.test(String(t.id))) { + diag('invalid-tenant-id', t); + setDiagHeader(res, 'bad-tenant-id'); + return res.status(400).json({ error: 'TenantID inválido' }); + } + if (!VALID_IDENT.test(t.schema)) { + diag('invalid-schema', t); + setDiagHeader(res, 'bad-schema'); + return res.status(400).json({ error: 'Schema inválido' }); + } + const okPrefix = schemaPrefixes.some(p => + t.schema.toLowerCase().startsWith(String(p).toLowerCase()), + ); + if (!okPrefix) { + diag('schema-prefix.rejected', { schema: t.schema, schemaPrefixes }); + setDiagHeader(res, 'schema-prefix-rejected'); + return res.status(400).json({ error: 'Schema no permitido' }); + } + + // 5) OK + req.tenant = { id: String(t.id), schema: String(t.schema) }; + res.locals.tenant = req.tenant; + setDiagHeader(res, `ok schema=${req.tenant.schema}`); + diag('attach.req.tenant', req.tenant); + + return next(); + } catch (err) { + diag('exception', { message: err?.message }); + return next(err); + } + }; +} diff --git a/packages/core/redis/package.json b/packages/core/redis/package.json new file mode 100644 index 0000000..1126437 --- /dev/null +++ b/packages/core/redis/package.json @@ -0,0 +1,22 @@ +{ + "name": "@suitecoffee/redis", + "version": "1.0.0", + "type": "module", + "main": "./redisSingleton.mjs", + "types": "./redisSingleton.d.ts", + "exports": { + ".": { + "types": "./redisSingleton.d.ts", + "import": "./redisSingleton.mjs", + "default": "./redisSingleton.mjs" + }, + "./package.json": "./package.json" + }, + "peerDependencies": { + "pg": "^8.16.3" + }, + "files": [ + "redisSingleton.mjs", + "redisSingleton.d.ts" + ] +} diff --git a/services/plugins/.env.production b/packages/core/redis/redisSingleton.d.ts similarity index 100% rename from services/plugins/.env.production rename to packages/core/redis/redisSingleton.d.ts diff --git a/packages/core/redis/redisSingleton.mjs b/packages/core/redis/redisSingleton.mjs new file mode 100644 index 0000000..c603d6c --- /dev/null +++ b/packages/core/redis/redisSingleton.mjs @@ -0,0 +1,93 @@ +// redisSingleton.mjs +// Conexión Singleton a Redis para Authentik (AK) + +import { createClient } from 'redis'; + +class RedisAuthentik { + static instance = null; + + constructor() { + if (RedisAuthentik.instance) { + return RedisAuthentik.instance; + } + + const url = process.env.AK_REDIS_URL; + if (!url) { + throw new Error('Falta AK_REDIS_URL Ej: redis://:pass@host:6379/0'); + } + if (!/^redis(s)?:\/\//i.test(url)) { + throw new Error('AK_REDIS_URL inválida: debe comenzar con "redis://" o "rediss://".'); + } + + this.url = url; + this.client = createClient({ + url: this.url, + socket: { connectTimeout: 5000 }, + }); + + this.client.on('connect', () => console.log(`[REDIS AK] Conectando a ${this.url}`)); + this.client.on('ready', () => console.log('[REDIS AK] Conexión lista.')); + this.client.on('end', () => console.warn('[REDIS AK] Conexión cerrada.')); + this.client.on('reconnecting', () => console.warn('[REDIS AK] Reintentando conexión...')); + this.client.on('error', (err) => console.error('[REDIS AK] Error:', err?.message || err)); + + this._connectingPromise = null; + RedisAuthentik.instance = this; + } + + async connect() { + if (this.client.isOpen) return this.client; + if (this._connectingPromise) return this._connectingPromise; + + this._connectingPromise = this.client.connect() + .then(() => this.client) + .catch((err) => { + this._connectingPromise = null; + console.error('[REDIS AK] Falló la conexión inicial:', err?.message || err); + throw err; + }); + + return this._connectingPromise; + } + + getClient() { + return this.client; + } + + async release() { + try { + if (this.client?.isOpen) await this.client.quit(); + } catch (e) { + console.warn('[REDIS AK] Error al cerrar:', e?.message || e); + } finally { + this._connectingPromise = null; + } + } +} + +// Instancia única +const redisAuthentik = new RedisAuthentik(); + +// --------------------- Healthcheck --------------------- +async function verificarConexionRedisAuthentik() { + try { + console.log(`[REDIS AK] Comprobando accesibilidad a Redis en ${redisAuthentik.url} ...`); + await redisAuthentik.connect(); + const client = redisAuthentik.getClient(); + + const pong = await client.ping(); + const timeArr = await client.sendCommand(['TIME']); + const serverDate = new Date(Number(timeArr?.[0] || 0) * 1000); + + await client.set('hc:authentik', String(Date.now()), { EX: 10 }); + + console.log(`[REDIS AK] Conexión OK. PING=${pong}. Hora Redis:`, serverDate.toISOString()); + } catch (error) { + console.error('[REDIS AK] Error al conectar:', error?.message || error); + console.error('[REDIS AK] Revisar AK_REDIS_URL, credenciales, red y firewall.'); + } +} + +// Export al estilo de poolSingleton.mjs +export default { redisAuthentik, verificarConexionRedisAuthentik }; +export { redisAuthentik, verificarConexionRedisAuthentik }; diff --git a/packages/core/scripts/package.json b/packages/core/scripts/package.json new file mode 100644 index 0000000..8f05de7 --- /dev/null +++ b/packages/core/scripts/package.json @@ -0,0 +1,19 @@ +{ + "name": "@suitecoffee/scripts", + "version": "1.0.0", + "type": "module", + "main": ".src/index.mjs", + "types": ".src/index.d.ts", + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./src/index.mjs", + "default": "./src/index.mjs" + }, + "./package.json": "./package.json" + }, + "files": [ + "srcindex.mjs", + "srcindex.d.ts" + ] +} diff --git a/packages/core/scripts/src/index.mjs b/packages/core/scripts/src/index.mjs new file mode 100644 index 0000000..8b511bc --- /dev/null +++ b/packages/core/scripts/src/index.mjs @@ -0,0 +1,4 @@ +// @suitecoffee/scripts/src/index.mjs +// Punto de entrada general del paquete de utilidades. + +export * from './utils/env.mjs'; \ No newline at end of file diff --git a/packages/core/scripts/src/utils/env.mjs b/packages/core/scripts/src/utils/env.mjs new file mode 100644 index 0000000..85475e5 --- /dev/null +++ b/packages/core/scripts/src/utils/env.mjs @@ -0,0 +1,24 @@ +// @suitecoffee/scripts/src/utils/env.mjs + +/** + * checkRequiredEnvVars + * Verifica que todas las variables de entorno requeridas existan en process.env. + * Muestra advertencias si alguna falta. + * + * @param {...string} requiredKeys - Lista de nombres de variables esperadas + */ + + +export function checkRequiredEnvVars(...requiredKeys) { + const missingKeys = requiredKeys.filter((key) => !process.env[key]); + + if (missingKeys.length > 0) { + console.warn( + `[ ENV ] No se encontraron las siguientes variables de entorno:\n\n` + + missingKeys.map((k) => `-> ${k}`).join('\n') + + `\n` + ); + } else { + console.log(`[ ENV ] Todas las variables de entorno requeridas están definidas.`); + } +} diff --git a/packages/db/src/index.mjs b/packages/db/src/index.mjs deleted file mode 100644 index 0364e56..0000000 --- a/packages/db/src/index.mjs +++ /dev/null @@ -1,2 +0,0 @@ -export * from './pool-registry.mjs'; -export * from './poolSingleton.mjs'; diff --git a/packages/db/src/pool-registry.mjs b/packages/db/src/pool-registry.mjs deleted file mode 100644 index 8251002..0000000 --- a/packages/db/src/pool-registry.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import { Pool } from 'pg'; - -const REGISTRY = new Map(); - -export function getPool(name = 'core', cfg = {}) { - if (REGISTRY.has(name)) return REGISTRY.get(name); - - const pool = new Pool({ - connectionString: process.env.PG_URL, - max: Number(process.env.PG_POOL_MAX ?? 10), - idleTimeoutMillis: Number(process.env.PG_IDLE_MS ?? 30000), - connectionTimeoutMillis: Number(process.env.PG_CONN_MS ?? 5000), - statement_timeout: Number(process.env.PG_STMT_MS ?? 15000), - ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined, - ...cfg - }); - - pool.on('error', (err) => { - // ideal: reemplazar con pino/sentry - console.error(`[pg:${name}] pool error`, err); - }); - - REGISTRY.set(name, pool); - return pool; -} - -function assertTenantSchema(schema) { - if (!/^tenant_[a-f0-9-]{16,36}$/i.test(schema)) { - throw new Error('Invalid tenant schema'); - } - return `"${schema.replace(/"/g, '""')}"`; -} - -export async function withTenant(poolName, tenantSchema, fn) { - const pool = getPool(poolName); - const client = await pool.connect(); - try { - await client.query('BEGIN'); - await client.query(`SET LOCAL search_path TO ${assertTenantSchema(tenantSchema)}`); - const res = await fn(client); - await client.query('COMMIT'); - return res; - } catch (e) { - try { await client.query('ROLLBACK'); } catch {} - throw e; - } finally { - client.release(); - } -} - -export async function shutdownAll() { - await Promise.all([...REGISTRY.values()].map(p => p.end())); - REGISTRY.clear(); -} diff --git a/packages/db/src/poolSingleton.mjs b/packages/db/src/poolSingleton.mjs deleted file mode 100644 index 35d0fae..0000000 --- a/packages/db/src/poolSingleton.mjs +++ /dev/null @@ -1,46 +0,0 @@ -// Coneción Singleton a base de datos. - -import { Pool } from 'pg'; - -class Database { - constructor() { - - if (Database.instance) { - return Database.instance; - } - - const config = { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - port: process.env.DB_LOCAL_PORT ? Number(process.env.DB_LOCAL_PORT) : undefined, - ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined, - }; - - this.connection = new Pool(config); - - Database.instance = this; - } - async query(sql, params) { - return this.connection.query(sql,params); - } - - async connect() { /* Definida solo para evitar errores */ - return this.connection.connect(); - } - async getClient() { - return this.connection.connect(); - } - - async release() { - await this.connection.end(); - } -} - -// const db = new Database(); -// db.query('SELECT * FROM users'); - -const pool = new Database(); -export default pool; -export { Database }; \ No newline at end of file diff --git a/packages/devices/attendance/core/BaseFileDriver.mjs b/packages/devices/attendance/core/BaseFileDriver.mjs new file mode 100644 index 0000000..bba971b --- /dev/null +++ b/packages/devices/attendance/core/BaseFileDriver.mjs @@ -0,0 +1,99 @@ +// BaseFileDriver.mjs +import { DeviceInterface } from './DeviceInterface.mjs'; +import { fmtHMSUTC, fmtHM } from '../utils/dates.mjs'; +import * as intervalsCross from '../strategies/intervals/cross-day.mjs'; +import * as intervalsSame from '../strategies/intervals/same-day.mjs'; + +/** + * Template Method para drivers basados en archivos .txt + * Define el pipeline y delega el parseo de línea en this.parserStrategy.parseLine + */ +export class BaseFileDriver extends DeviceInterface { + constructor(opts = {}) { + super(opts); + if (!this.parserStrategy || typeof this.parserStrategy.parseLine !== 'function') { + throw new Error('BaseFileDriver requiere parserStrategy.parseLine(line)'); + } + } + + /** + * @param {string} text contenido completo del .txt en UTF-8 + */ + async processFile(text) { + if (!text || typeof text !== 'string') { + this.setStatus('Elegí un .txt válido'); + return { parsedRows: [], pairs: [], payloadDB: [], missing_docs: [], error: 'Archivo vacío o inválido' }; + } + + this.setStatus('Leyendo archivo…'); + + // 1) Parseo línea a línea (Strategy) + const lines = text.split(/\n/); + const parsedRows = []; + for (let i = 0; i < lines.length; i++) { + const r = this.parserStrategy.parseLine(lines[i]); + if (r) parsedRows.push(r); + if ((i & 511) === 0) this.emit('progress', { at: i, total: lines.length }); + } + + // 2) Resolver nombres por documento (inyectado) + const uniqueDocs = [...new Set(parsedRows.map(r => r.doc))]; + this.setStatus(`Leyendo archivo… | consultando ${uniqueDocs.length} documentos…`); + const map = await this._safeNamesResolver(uniqueDocs); + + // 3) Detectar documentos faltantes + const missing_docs = uniqueDocs.filter(d => { + const hit = map?.[d]; + if (!hit) return true; + if (typeof hit.found === 'boolean') return !hit.found; + return !(hit?.nombre || '').trim() && !(hit?.apellido || '').trim(); + }); + + if (missing_docs.length) { + this.setStatus('Hay documentos sin usuario. Corrigí y volvé a procesar.'); + return { parsedRows, pairs: [], payloadDB: [], missing_docs, + error: `No se encontraron ${missing_docs.length} documento(s) en la base` }; + } + + // 4) Enriquecer nombre desde DB + parsedRows.forEach(r => { + const hit = map?.[r.doc]; + if (hit && (hit.nombre || hit.apellido)) r.name = `${hit.nombre || ''} ${hit.apellido || ''}`.trim(); + }); + + // 5) Construcción de intervalos (Strategy) + const pairs = (this.intervalBuilder === 'sameDay') + ? intervalsSame.buildIntervals(parsedRows) + : intervalsCross.buildIntervalsCrossDay(parsedRows); + + // 6) Payload "raw" para DB + const payloadDB = parsedRows.map(r => ({ + doc: r.doc, isoDate: r.isoDate, time: r.time, mode: r.mode || null + })); + + this.setStatus(`${parsedRows.length} registros · ${pairs.length} intervalos`); + return { parsedRows, pairs, payloadDB, missing_docs: [] }; + } + + exportCSV(pairs) { + const list = Array.isArray(pairs) ? pairs : []; + if (!list.length) return ''; + const head = ['documento','nombre','fecha','desde','hasta','duracion_hhmm','duracion_min','obs']; + const rows = list.map(p => { + const iso = p.isoDate || p.fecha || ''; + const desdeStr = (p.desde_ms!=null) ? fmtHMSUTC(p.desde_ms) : ''; + const hastaStr = (p.hasta_ms!=null) ? fmtHMSUTC(p.hasta_ms) : ''; + const durStr = (p.durMins!=null) ? fmtHM(p.durMins) : ''; + const durMin = (p.durMins!=null) ? Math.round(p.durMins) : ''; + return [ + p.doc, p.name || '', iso, desdeStr, hastaStr, durStr, durMin, p.obs || '' + ].map(v => `"${String(v).replaceAll('"','""')}"`).join(','); + }); + return head.join(',') + '\n' + rows.join('\n'); + } + + async _safeNamesResolver(docs) { + try { return await this.namesResolver(docs); } + catch { return {}; } + } +} diff --git a/packages/devices/attendance/core/BaseTcpDriver.mjs b/packages/devices/attendance/core/BaseTcpDriver.mjs new file mode 100644 index 0000000..e69de29 diff --git a/packages/devices/attendance/core/DeviceInterface.mjs b/packages/devices/attendance/core/DeviceInterface.mjs new file mode 100644 index 0000000..cd45971 --- /dev/null +++ b/packages/devices/attendance/core/DeviceInterface.mjs @@ -0,0 +1,46 @@ +// DeviceInterface.mjs +import { EventEmitter } from 'node:events'; + +/** + * Contrato común que todos los drivers deben implementar. + * Drivers de archivo (.txt) pueden dejar connect/fetchLogs/parseLogData como no-op. + */ +export class DeviceInterface extends EventEmitter { + /** + * @param {object} [opts] + * @param {(docs:string[])=>Promise>} [opts.namesResolver] + * @param {'crossDay'|'sameDay'} [opts.intervalBuilder] + * @param {{ parseLine:(line:string)=>object|null }} [opts.parserStrategy] + */ + constructor(opts = {}) { + super(); + this.namesResolver = typeof opts.namesResolver === 'function' ? opts.namesResolver : async () => ({}); + this.intervalBuilder = opts.intervalBuilder || 'crossDay'; + this.parserStrategy = opts.parserStrategy || null; + } + + // ------- API esperada (drivers file) ------- + /** + * Procesa el contenido completo de un .txt y devuelve: + * { parsedRows, pairs, payloadDB, missing_docs, error? } + */ + async processFile(/* text:string */) { + throw new Error('processFile not implemented'); + } + + /** + * Retorna CSV como string (no descarga). + */ + exportCSV(/* pairs?:object[] */) { + throw new Error('exportCSV not implemented'); + } + + // ------- API opcional (drivers TCP/IP) ---- + async connect() { /* no-op */ } + async disconnect() { /* no-op */ } + async fetchLogs() { throw new Error('fetchLogs not implemented'); } + async parseLogData(/* raw */) { throw new Error('parseLogData not implemented'); } + + // ------- Utilidad: emitir estado ------- + setStatus(text) { this.emit('status', text || ''); } +} diff --git a/packages/devices/attendance/core/errors/DeviceErrors.mjs b/packages/devices/attendance/core/errors/DeviceErrors.mjs new file mode 100644 index 0000000..7062cad --- /dev/null +++ b/packages/devices/attendance/core/errors/DeviceErrors.mjs @@ -0,0 +1,4 @@ +// DeviceErrors.mjs +export class DeviceError extends Error { constructor(msg){ super(msg); this.name='DeviceError'; } } +export class DriverNotFoundError extends DeviceError { constructor(key){ super(`Driver no registrado: ${key}`); this.name='DriverNotFoundError'; } } +export class ParseError extends DeviceError { constructor(line){ super(`No se pudo parsear la línea: ${line}`); this.name='ParseError'; } } diff --git a/packages/devices/attendance/core/factories/DeviceFactory.mjs b/packages/devices/attendance/core/factories/DeviceFactory.mjs new file mode 100644 index 0000000..9ae1ccf --- /dev/null +++ b/packages/devices/attendance/core/factories/DeviceFactory.mjs @@ -0,0 +1,22 @@ +// DeviceFactory.mjs +import { DriverRegistry } from './DriverRegistry.mjs'; + +export class DeviceFactory { + static register(key, ctor, manifest) { + DriverRegistry.register(key, ctor, manifest); + } + + /** + * @param {string} key "vendor:model" + * @param {object} opts opciones para el constructor del driver + */ + static create(key, opts = {}) { + const reg = DriverRegistry.get(key); + if (!reg) throw new Error(`DeviceFactory: driver no registrado: ${key}`); + return new reg.ctor(opts); + } + + static listSupported() { + return DriverRegistry.list(); + } +} diff --git a/packages/devices/attendance/core/factories/DriverRegistry.mjs b/packages/devices/attendance/core/factories/DriverRegistry.mjs new file mode 100644 index 0000000..1fce66c --- /dev/null +++ b/packages/devices/attendance/core/factories/DriverRegistry.mjs @@ -0,0 +1,20 @@ +// DriverRegistry.mjs +const _registry = new Map(); +/** + * Clave: "vendor:model" en minúsculas + * Valor: { ctor: DriverClass, manifest?: object } + */ +export const DriverRegistry = { + register(key, ctor, manifest = null) { + const k = String(key || '').trim().toLowerCase(); + if (!k) throw new Error('DriverRegistry.register: key vacío'); + if (typeof ctor !== 'function') throw new Error('DriverRegistry.register: ctor inválido'); + _registry.set(k, { ctor, manifest: manifest || {} }); + }, + get(key) { + return _registry.get(String(key || '').trim().toLowerCase()) || null; + }, + list() { + return [..._registry.entries()].map(([k, v]) => ({ key: k, manifest: v.manifest || {} })); + } +}; diff --git a/packages/devices/attendance/core/index.mjs b/packages/devices/attendance/core/index.mjs new file mode 100644 index 0000000..26637c6 --- /dev/null +++ b/packages/devices/attendance/core/index.mjs @@ -0,0 +1,18 @@ +// index.mjs (Facade del dominio attendance) +export { DeviceInterface } from './DeviceInterface.mjs'; +export { BaseFileDriver } from './BaseFileDriver.mjs'; +export { DeviceFactory } from './factories/DeviceFactory.mjs'; +export { DriverRegistry } from './factories/DriverRegistry.mjs'; + +// Facade helpers +import { DeviceFactory } from './factories/DeviceFactory.mjs'; + +export function registerDriver(key, Ctor, manifest) { + DeviceFactory.register(key, Ctor, manifest); +} +export function createDevice(key, opts) { + return DeviceFactory.create(key, opts); +} +export function listSupported() { + return DeviceFactory.listSupported(); +} diff --git a/packages/devices/attendance/core/schema/manifest.schema.json b/packages/devices/attendance/core/schema/manifest.schema.json new file mode 100644 index 0000000..216116f --- /dev/null +++ b/packages/devices/attendance/core/schema/manifest.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Device Driver Manifest", + "type": "object", + "required": ["vendor", "model", "transport", "version"], + "properties": { + "vendor": { "type": "string", "minLength": 1 }, + "model": { "type": "string", "minLength": 1 }, + "transport": { "type": "string", "enum": ["file", "tcp", "http"] }, + "capabilities": { "type": "array", "items": { "type": "string" } }, + "version": { "type": "string" } + }, + "additionalProperties": true +} diff --git a/packages/devices/attendance/core/schema/record.schema.json b/packages/devices/attendance/core/schema/record.schema.json new file mode 100644 index 0000000..e69de29 diff --git a/packages/devices/attendance/core/strategies/intervals/cross-day.mjs b/packages/devices/attendance/core/strategies/intervals/cross-day.mjs new file mode 100644 index 0000000..f86f760 --- /dev/null +++ b/packages/devices/attendance/core/strategies/intervals/cross-day.mjs @@ -0,0 +1,29 @@ +// cross-day.mjs +// Pares ordenados para jornadas que pueden cruzar medianoche. +// rows: [{ doc, name, isoDate, dt_ms, ... }, ...] +export function buildIntervalsCrossDay(rows){ + const byDoc = new Map(); + rows.forEach(r => { + (byDoc.get(r.doc) || byDoc.set(r.doc, []).get(r.doc)) + .push({ ms: r.dt_ms, date: r.isoDate, name: r.name }); + }); + + const out = []; + for (const [doc, arr] of byDoc.entries()){ + arr.sort((a,b)=>a.ms-b.ms); + for (let i=0;i x.doc.localeCompare(y.doc) || + x.fecha.localeCompare(y.fecha) || + (x.desde_ms - y.desde_ms)); + return out; +} diff --git a/packages/devices/attendance/core/strategies/intervals/same-day.mjs b/packages/devices/attendance/core/strategies/intervals/same-day.mjs new file mode 100644 index 0000000..ab3ce57 --- /dev/null +++ b/packages/devices/attendance/core/strategies/intervals/same-day.mjs @@ -0,0 +1,34 @@ +// same-day.mjs +// Agrupa por (doc, fecha) y arma pares 1-2, 3-4, ... +export function buildIntervals(rows) { + const nameByDoc = new Map(); + const byKey = new Map(); // doc|isoDate -> [ms] + + for (const r of rows) { + nameByDoc.set(r.doc, r.name); + const key = `${r.doc}|${r.isoDate}`; + (byKey.get(key) || byKey.set(key, []).get(key)).push(r.dt_ms); + } + + const result = []; + for (const [key, arr] of byKey.entries()) { + arr.sort((a,b)=>a-b); + const [doc, isoDate] = key.split('|'); + const name = nameByDoc.get(doc) || ''; + for (let i=0; i{ + if (a.doc !== b.doc) return a.doc.localeCompare(b.doc); + if (a.isoDate !== b.isoDate) return a.isoDate.localeCompare(b.isoDate); + return (a.desde_ms||0) - (b.desde_ms||0); + }); + return result; +} diff --git a/packages/devices/attendance/core/strategies/parsers/LineParserInterface.mjs b/packages/devices/attendance/core/strategies/parsers/LineParserInterface.mjs new file mode 100644 index 0000000..b01e98e --- /dev/null +++ b/packages/devices/attendance/core/strategies/parsers/LineParserInterface.mjs @@ -0,0 +1,6 @@ +// LineParserInterface.mjs +export class LineParserInterface { + parseLine(/* line:string */) { + throw new Error('parseLine not implemented'); + } +} diff --git a/packages/devices/attendance/core/strategies/parsers/generic-i60.mjs b/packages/devices/attendance/core/strategies/parsers/generic-i60.mjs new file mode 100644 index 0000000..e69de29 diff --git a/packages/devices/attendance/core/strategies/parsers/zkteco-i60.mjs b/packages/devices/attendance/core/strategies/parsers/zkteco-i60.mjs new file mode 100644 index 0000000..e69de29 diff --git a/packages/devices/attendance/core/utils/dates.mjs b/packages/devices/attendance/core/utils/dates.mjs new file mode 100644 index 0000000..1ef55ed --- /dev/null +++ b/packages/devices/attendance/core/utils/dates.mjs @@ -0,0 +1,31 @@ +// dates.mjs +export const z2 = n => String(n).padStart(2,'0'); + +export function toUTCms(isoDate, time) { + const [Y,M,D] = isoDate.split('-').map(n=>parseInt(n,10)); + const [h,m,s] = time.split(':').map(n=>parseInt(n,10)); + return Date.UTC(Y, (M||1)-1, D||1, h||0, m||0, s||0); +} + +export function fmtHMSUTC(ms){ + const d = new Date(ms); + const z = n => String(n).padStart(2,'0'); + return `${z(d.getUTCHours())}:${z(d.getUTCMinutes())}:${z(d.getUTCSeconds())}`; +} + +export const fmtHM = mins => { + const h = Math.floor(mins/60); const m = Math.round(mins%60); + return `${z2(h)}:${z2(m)}`; +}; + +// "YY/MM/DD" o "YYYY/MM/DD" (o '-') -> "YYYY-MM-DD" +export function normDateStr(s) { + const m = String(s || '').trim().match(/^(\d{2,4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/); + if (!m) return null; + let [_, y, mo, d] = m; + let yy = parseInt(y, 10); + if (y.length === 2) yy = 2000 + yy; + const mm = parseInt(mo, 10), dd = parseInt(d, 10); + if (!(mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31)) return null; + return `${yy}-${String(mm).padStart(2,'0')}-${String(dd).padStart(2,'0')}`; +} diff --git a/packages/devices/attendance/core/utils/docs.mjs b/packages/devices/attendance/core/utils/docs.mjs new file mode 100644 index 0000000..9ed4a95 --- /dev/null +++ b/packages/devices/attendance/core/utils/docs.mjs @@ -0,0 +1,20 @@ +// docs.mjs +import { z2 } from './dates.mjs'; + +export const normDoc = s => { + const v = String(s||'').replace(/\D/g,'').replace(/^0+/,''); + return v || '0'; +}; + +export const cleanDoc = s => { + const v = String(s||'').trim().replace(/^0+/, ''); + return v === '' ? '0' : v; +}; + +// HH:MM o HH:MM:SS -> HH:MM:SS +export const normTime = s => { + if (!s) return ''; + const m = String(s).trim().match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/); + if (!m) return ''; + return `${z2(+m[1])}:${z2(+m[2])}:${z2(+m[3]||0)}`; +}; diff --git a/packages/devices/attendance/drivers/generic/i60/GenericI60Driver.mjs b/packages/devices/attendance/drivers/generic/i60/GenericI60Driver.mjs new file mode 100644 index 0000000..65addf3 --- /dev/null +++ b/packages/devices/attendance/drivers/generic/i60/GenericI60Driver.mjs @@ -0,0 +1,17 @@ +// GenericI60Driver.mjs +import { BaseFileDriver } from '../../core/BaseFileDriver.mjs'; +import * as Parser from './parser.mjs'; + +/** + * Driver genérico i60 (sin conectividad). Lee archivos .txt exportados del equipo. + * Implementa el "Template Method" heredado de BaseFileDriver. + */ +export default class GenericI60Driver extends BaseFileDriver { + constructor(opts = {}) { + super({ + ...opts, + parserStrategy: { parseLine: Parser.parseLine }, + intervalBuilder: opts.intervalBuilder || 'crossDay' + }); + } +} diff --git a/packages/devices/attendance/drivers/generic/i60/index.mjs b/packages/devices/attendance/drivers/generic/i60/index.mjs new file mode 100644 index 0000000..6649aa1 --- /dev/null +++ b/packages/devices/attendance/drivers/generic/i60/index.mjs @@ -0,0 +1,13 @@ +// index.mjs +import GenericI60Driver from './GenericI60Driver.mjs'; + +export const manifest = { + vendor: 'generic', + model: 'i60', + transport: 'file', + capabilities: ['import', 'intervals:cross-day'], + version: '1.0.0' +}; + +export default GenericI60Driver; +export { manifest }; diff --git a/packages/devices/attendance/drivers/generic/i60/manifest.json b/packages/devices/attendance/drivers/generic/i60/manifest.json new file mode 100644 index 0000000..e69de29 diff --git a/packages/devices/attendance/drivers/generic/i60/parser.mjs b/packages/devices/attendance/drivers/generic/i60/parser.mjs new file mode 100644 index 0000000..d553d7d --- /dev/null +++ b/packages/devices/attendance/drivers/generic/i60/parser.mjs @@ -0,0 +1,54 @@ +// parser.mjs +import { normDateStr, toUTCms } from '../../core/utils/dates.mjs'; +import { cleanDoc, normTime } from '../../core/utils/docs.mjs'; + +/** + * Parsea una línea con prioridad por TAB; si no hay, cae a espacios; + * separa fecha/hora si vienen juntas. + * Devuelve { doc, name, isoDate, time, dt_ms, mode } o null. + */ +export function parseLine(line) { + const raw = String(line || '').replace(/\r/g, '').trim(); + if (!raw) return null; + + // omitir encabezados comunes + if (/^no[\t ]|^mchn[\t ]|^enno[\t ]|^name[\t ]|^datetime[\t ]/i.test(raw)) return null; + + let parts = raw.split(/\t+/); + + // Fallback: dos o más espacios + DateTime al final + if (parts.length < 7) { + const dtMatch = raw.match(/(\d{2,4}[\/-]\d{1,2}[\/-]\d{1,2})\s+(\d{1,2}:\d{2}:\d{2})$/); + if (dtMatch) { + const head = raw.slice(0, dtMatch.index).trim(); + const headParts = head.split(/\t+|\s{2,}/).filter(Boolean); + parts = [...headParts, dtMatch[1], dtMatch[2]]; + } else { + parts = raw.split(/\s{2,}/).filter(Boolean); + } + } + if (parts.length < 7) return null; + + // Indices "normales": 0:No, 1:Mchn, 2:EnNo(doc), 3:Name, 4:Mode, 5:Fecha, 6:Hora + const doc = cleanDoc(parts[2]); + const name = String(parts[3] || '').trim(); + const mode = String(parts[4] || '').trim(); + + let dateStr = String(parts[5] || '').trim(); + let timeStr = String(parts[6] || '').trim(); + + // Caso: la última columna es "YYYY/MM/DD HH:MM:SS" + const last = parts[parts.length - 1]; + const dtBoth = /(\d{2,4}[\/-]\d{1,2}[\/-]\d{1,2})\s+(\d{1,2}:\d{2}:\d{2})/.exec(last); + if (dtBoth) { dateStr = dtBoth[1]; timeStr = dtBoth[2]; } + else if (!timeStr && /\d{1,2}:\d{2}:\d{2}/.test(dateStr)) { + const m = dateStr.match(/^(.+?)\s+(\d{1,2}:\d{2}:\d{2})$/); + if (m) { dateStr = m[1]; timeStr = m[2]; } + } + + const iso = normDateStr(dateStr); + const timeNorm = normTime(timeStr); + if (!iso || !timeNorm) return null; + + return { doc, name, isoDate: iso, time: timeNorm, dt_ms: toUTCms(iso, timeNorm), mode }; +} diff --git a/packages/devices/attendance/drivers/generic/src/DeviceFactory.mjs b/packages/devices/attendance/drivers/generic/src/DeviceFactory.mjs new file mode 100644 index 0000000..ac88951 --- /dev/null +++ b/packages/devices/attendance/drivers/generic/src/DeviceFactory.mjs @@ -0,0 +1,11 @@ +import { GenericI60Driver } from './drivers/Generic/i60/GenericI60Driver'; + +export class DeviceFactory { + static create(model, config) { + switch (model) { + case 'Generic-i60': return new GenericI60Driver(config); + default: + throw new Error(`El modelo indicado no esta soportado. ${model}\n Porfavor ponerse en contacto con el equipo para implementarlo.`); + } + } +} \ No newline at end of file diff --git a/packages/devices/attendance/drivers/generic/src/DeviceInterface.mjs b/packages/devices/attendance/drivers/generic/src/DeviceInterface.mjs new file mode 100644 index 0000000..76ea8fa --- /dev/null +++ b/packages/devices/attendance/drivers/generic/src/DeviceInterface.mjs @@ -0,0 +1,6 @@ +// DeviceInterface.mjs +export class DeviceInterface { + async connect() { throw new Error('Not implemented'); } + async fetchLogs() { throw new Error('Not implemented'); } + async parseLogData(raw) { throw new Error('Not implemented'); } +} \ No newline at end of file diff --git a/packages/devices/attendance/drivers/generic/src/index.mjs b/packages/devices/attendance/drivers/generic/src/index.mjs new file mode 100644 index 0000000..e69de29 diff --git a/packages/devices/attendance/drivers/generic/src/utils.mjs b/packages/devices/attendance/drivers/generic/src/utils.mjs new file mode 100644 index 0000000..e69de29 diff --git a/packages/devices/i60/DriverFactory.mjs b/packages/devices/i60/DriverFactory.mjs new file mode 100644 index 0000000..11a32a3 --- /dev/null +++ b/packages/devices/i60/DriverFactory.mjs @@ -0,0 +1,13 @@ +import { GenericDriver } from './GenericDriver.mjs'; + +export class DriverFactory { + static create(model = 'Generico'){ + switch (String(model).toLowerCase()) { + case 'generico': + case 'generic': + default: + // El constructor de GenericDriver es Singleton; devolverá siempre la misma instancia + return new GenericDriver(); + } + } +} diff --git a/packages/devices/i60/GenericDriver.mjs b/packages/devices/i60/GenericDriver.mjs new file mode 100644 index 0000000..6c4cd8b --- /dev/null +++ b/packages/devices/i60/GenericDriver.mjs @@ -0,0 +1,74 @@ +import { readFile } from 'node:fs/promises'; +import { parseLine } from './parsing.mjs'; +import { buildIntervalsCrossDay } from './intervals.mjs'; +import { exportCSV } from './csv.mjs'; +import { NamesServiceProxy } from './namesProxy.mjs'; + +class GenericDriver { + constructor(){ + if (GenericDriver._instance) return GenericDriver._instance; + /** @type {Array} */ this.parsedRows = []; + /** @type {Array} */ this.payloadDB = []; + /** @type {Array} */ this.pairs = []; + GenericDriver._instance = this; + } + + // Orquesta el proceso a partir de texto plano + async processText(text, { fetchNamesForDocs } = {}){ + const lines = String(text||'').split(/\n/); + const rows = []; + for (const line of lines) { + const r = parseLine(line); + if (r) rows.push(r); + } + this.parsedRows = rows; + + const uniqueDocs = [...new Set(this.parsedRows.map(r => r.doc))]; + + const namesProxy = new NamesServiceProxy(fetchNamesForDocs); + const map = await namesProxy.get(uniqueDocs); + + const missingDocs = uniqueDocs.filter(d => { + const hit = map?.[d]; + if (!hit) return true; + if (typeof hit.found === 'boolean') return !hit.found; + return !(hit?.nombre||'').trim() && !(hit?.apellido||'').trim(); + }); + + // sobreescribir nombre cuando DB provee + this.parsedRows.forEach(r => { + const hit = map?.[r.doc]; + if (hit && (hit.nombre || hit.apellido)) { + r.name = `${hit.nombre || ''} ${hit.apellido || ''}`.trim(); + } + }); + + // Pairs (permitiendo cruce de medianoche) + this.pairs = buildIntervalsCrossDay(this.parsedRows); + + // Payload crudo para insertar + this.payloadDB = this.parsedRows.map(r => ({ + doc: r.doc, + isoDate: r.isoDate, + time: r.time, + mode: r.mode || null + })); + + return { parsedRows: this.parsedRows, pairs: this.pairs, payloadDB: this.payloadDB, missingDocs }; + } + + // Conveniencia: leer desde ruta en disco + async processFileFromPath(filePath, opts = {}){ + const txt = await readFile(filePath, 'utf8'); + return await this.processText(txt, opts); + } + + // CSV server-side (devuelve string) + exportCSV(pairs = this.pairs){ + return exportCSV(pairs); + } +} + +const instance = new GenericDriver(); +export default instance; +export { GenericDriver }; diff --git a/packages/devices/i60/GenericDriverFacade.mjs b/packages/devices/i60/GenericDriverFacade.mjs new file mode 100644 index 0000000..98c177a --- /dev/null +++ b/packages/devices/i60/GenericDriverFacade.mjs @@ -0,0 +1,8 @@ +import { DriverFactory } from './DriverFactory.mjs'; + +export class GenericDriverFacade { + constructor(driver = DriverFactory.create('Generico')){ this.driver = driver; } + async processTxt(text, services = {}){ return await this.driver.processText(text, services); } + async processFile(filePath, services = {}){ return await this.driver.processFileFromPath(filePath, services); } + exportCSV(pairs){ return this.driver.exportCSV(pairs); } +} diff --git a/packages/devices/i60/csv.mjs b/packages/devices/i60/csv.mjs new file mode 100644 index 0000000..00a9a59 --- /dev/null +++ b/packages/devices/i60/csv.mjs @@ -0,0 +1,17 @@ +import { fmtHM, fmtHMSUTC } from './helpers.mjs'; + +// Genera CSV (server-side: retorna string) — nombre preservado +export function exportCSV(pairs) { + if (!pairs?.length) return ''; + const head = ['documento','nombre','fecha','desde','hasta','duracion_hhmm','duracion_min','obs']; + const rows = pairs.map(p => { + const fecha = p.fecha || p.isoDate || ''; + const desde = p.desde_ms!=null ? fmtHMSUTC(p.desde_ms) : ''; + const hasta = p.hasta_ms!=null ? fmtHMSUTC(p.hasta_ms) : ''; + const durHHMM = p.durMins!=null ? fmtHM(p.durMins) : ''; + const durMin = p.durMins!=null ? Math.round(p.durMins) : ''; + return [p.doc, p.name || '', fecha, desde, hasta, durHHMM, durMin, p.obs || ''] + .map(v => `"${String(v).replaceAll('"','""')}"`).join(','); + }); + return head.join(',') + '\n' + rows.join('\n'); +} diff --git a/packages/devices/i60/helpers.mjs b/packages/devices/i60/helpers.mjs new file mode 100644 index 0000000..f504abb --- /dev/null +++ b/packages/devices/i60/helpers.mjs @@ -0,0 +1,40 @@ +// Helpers comunes (nombres preservados) +export const z2 = n => String(n).padStart(2,'0'); +export const pad2 = z2; +export const fmtHM = mins => { const h = Math.floor(mins/60); const m = Math.round(mins%60); return `${z2(h)}:${z2(m)}`; }; +export const ymd = s => String(s||'').slice(0,10); // '2025-08-29T..' -> '2025-08-29' + +// Normaliza fecha "YY/MM/DD" o "YYYY/MM/DD" a "YYYY-MM-DD" +export function normDateStr(s) { + const m = String(s || '').trim().match(/^(\d{2,4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/); + if (!m) return null; + let [_, y, mo, d] = m; + let yy = parseInt(y, 10); + if (y.length === 2) yy = 2000 + yy; // 20YY + const mm = parseInt(mo, 10), dd = parseInt(d, 10); + if (!(mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31)) return null; + return `${yy}-${String(mm).padStart(2,'0')}-${String(dd).padStart(2,'0')}`; +} + +// Normaliza documento quitando ceros a la izquierda +export const cleanDoc = s => { + const v = String(s||'').trim().replace(/^0+/, ''); + return v === '' ? '0' : v; +}; + +// Compat alias (mantener nombre) +export const normDoc = s => { + const v = String(s||'').replace(/\D/g,'').replace(/^0+/,''); + return v || '0'; +}; + +export function toUTCms(isoDate, time) { + const [Y,M,D] = isoDate.split('-').map(n=>parseInt(n,10)); + const [h,m,s] = time.split(':').map(n=>parseInt(n,10)); + return Date.UTC(Y, (M||1)-1, D||1, h||0, m||0, s||0); // UTC fijo +} +export function fmtHMSUTC(ms){ + const d = new Date(ms); + const z = n => String(n).padStart(2,'0'); + return `${z(d.getUTCHours())}:${z(d.getUTCMinutes())}:${z(d.getUTCSeconds())}`; +} diff --git a/packages/devices/i60/index.mjs b/packages/devices/i60/index.mjs new file mode 100644 index 0000000..843a7eb --- /dev/null +++ b/packages/devices/i60/index.mjs @@ -0,0 +1,32 @@ +export { default as GenericDriverDefault, GenericDriver } from './GenericDriver.mjs'; +export { DriverFactory } from './DriverFactory.mjs'; +export { GenericDriverFacade } from './GenericDriverFacade.mjs'; +export { NamesServiceProxy } from './namesProxy.mjs'; + +export * from './helpers.mjs'; +export * from './parsing.mjs'; +export * from './intervals.mjs'; +export * from './csv.mjs'; + + + +/* +Uso mínimo (en tu servidor, al recibir un .txt subido): + +// ejemplo en tu ruta de subida +import { GenericDriverFacade } from './drivers/generic/i60/GenericDriverFacade.mjs'; + +const facade = new GenericDriverFacade(); + +const { parsedRows, pairs, payloadDB, missingDocs } = + await facade.processFile(tempFilePath, { + // opcional: integra tu búsqueda de usuarios por documento + fetchNamesForDocs: async (docs) => { + // devuelve: { "12345678": { nombre, apellido, found:true } , ... } + return await dbFindUsuariosPorDocumentos(docs); + } + }); + +// luego persistes payloadDB y/o pairs según tu lógica + +*/ \ No newline at end of file diff --git a/packages/devices/i60/intervals.mjs b/packages/devices/i60/intervals.mjs new file mode 100644 index 0000000..d5d95ba --- /dev/null +++ b/packages/devices/i60/intervals.mjs @@ -0,0 +1,53 @@ +// Agrupa por empleado, ordena cronológicamente y arma pares 1-2, 3-4, ... permitiendo cruzar medianoche. +export function buildIntervalsCrossDay(rows){ + const byDoc = new Map(); + for (const r of rows) { + if (!byDoc.has(r.doc)) byDoc.set(r.doc, []); + byDoc.get(r.doc).push({ ms: r.dt_ms, date: r.isoDate, name: r.name }); + } + const out = []; + for (const [doc, arr] of byDoc.entries()){ + arr.sort((a,b)=>a.ms-b.ms); + for (let i=0;i x.doc.localeCompare(y.doc) || x.fecha.localeCompare(y.fecha) || (x.desde_ms - y.desde_ms)); + return out; +} + +// Alternativa por (doc, fecha) exacta (conservar nombre y firma) +export function buildIntervals(rows) { + const nameByDoc = new Map(); + const byKey = new Map(); // doc|isoDate -> [ms] + for (const r of rows) { + nameByDoc.set(r.doc, r.name); + const key = `${r.doc}|${r.isoDate}`; + if (!byKey.has(key)) byKey.set(key, []); + byKey.get(key).push(r.dt_ms); + } + const result = []; + for (const [key, arr] of byKey.entries()) { + arr.sort((a,b)=>a-b); + const [doc, isoDate] = key.split('|'); + const name = nameByDoc.get(doc) || ''; + for (let i=0; i{ + if (a.doc !== b.doc) return a.doc.localeCompare(b.doc); + if (a.isoDate !== b.isoDate) return a.isoDate.localeCompare(b.isoDate); + return (a.desde_ms||0) - (b.desde_ms||0); + }); + return result; +} diff --git a/packages/devices/i60/namesProxy.mjs b/packages/devices/i60/namesProxy.mjs new file mode 100644 index 0000000..100ba4e --- /dev/null +++ b/packages/devices/i60/namesProxy.mjs @@ -0,0 +1,18 @@ +// Proxy de servicio de nombres (caché + normalización) +export class NamesServiceProxy { + constructor(fetchNamesForDocs){ + this._fetch = typeof fetchNamesForDocs === 'function' ? fetchNamesForDocs : async () => ({}); + this._cache = new Map(); + } + async get(docs){ + const ask = []; + for (const d of docs) if (!this._cache.has(d)) ask.push(d); + if (ask.length){ + const map = await this._fetch(ask); + for (const [k,v] of Object.entries(map || {})) this._cache.set(String(k), v || {}); + } + const out = {}; + for (const d of docs) out[d] = this._cache.get(d) || {}; + return out; + } +} diff --git a/packages/devices/i60/package.json b/packages/devices/i60/package.json new file mode 100644 index 0000000..cfc333b --- /dev/null +++ b/packages/devices/i60/package.json @@ -0,0 +1,15 @@ +{ + "name": "@suitecoffee/driver-i60", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Driver genérico para lector I60 (asistencia)", + "exports": { + ".": "./src/index.mjs" + }, + "files": ["src"], + "dependencies": { + "@suitecoffee/db": "workspace:*", + "@suitecoffee/utils": "workspace:*" + } +} diff --git a/packages/devices/i60/parsing.mjs b/packages/devices/i60/parsing.mjs new file mode 100644 index 0000000..86ded82 --- /dev/null +++ b/packages/devices/i60/parsing.mjs @@ -0,0 +1,64 @@ +import { cleanDoc, normDateStr, toUTCms } from './helpers.mjs'; + +// Parsea una línea (nombres preservados) +export function parseLine(line) { + const raw = String(line || '').replace(/\r/g, '').trim(); + if (!raw) return null; + + // omitir encabezado + if (/^no[\t ]|^mchn[\t ]|^enno[\t ]|^name[\t ]|^datetime[\t ]/i.test(raw)) return null; + + let parts = raw.split(/\t+/); + + // Si no alcanzan 7 campos, intentar fallback con dos o más espacios + if (parts.length < 7) { + const dtMatch = raw.match(/(\d{2,4}[\/-]\d{1,2}[\/-]\d{1,2})\s+(\d{1,2}:\d{2}:\d{2})$/); + if (dtMatch) { + const head = raw.slice(0, dtMatch.index).trim(); + const headParts = head.split(/\t+|\s{2,}/).filter(Boolean); + parts = [...headParts, dtMatch[1], dtMatch[2]]; + } else { + parts = raw.split(/\s{2,}/).filter(Boolean); + } + } + if (parts.length < 7) return null; + + // 0:No, 1:Mchn, 2:EnNo(doc), 3:Name, 4:Mode, 5:Fecha, 6:Hora + const DOC_IDX = 2; + const NAME_IDX = 3; + const MODE_IDX = 4; + + const doc = cleanDoc(parts[DOC_IDX]); + const name = String(parts[NAME_IDX] || '').trim(); + const mode = String(parts[MODE_IDX] || '').trim(); + + let dateStr = String(parts[5] || '').trim(); + let timeStr = String(parts[6] || '').trim(); + + // Caso: 7 columnas y última es "DateTime" + const last = parts[parts.length - 1]; + const dtBoth = /(\d{2,4}[\/-]\d{1,2}[\/-]\d{1,2})\s+(\d{1,2}:\d{2}:\d{2})/.exec(last); + if (dtBoth) { + dateStr = dtBoth[1]; + timeStr = dtBoth[2]; + } else if (!timeStr && /\d{1,2}:\d{2}:\d{2}/.test(dateStr)) { + const m = dateStr.match(/^(.+?)\s+(\d{1,2}:\d{2}:\d{2})$/); + if (m) { dateStr = m[1]; timeStr = m[2]; } + } + + const iso = normDateStr(dateStr); // YY/MM/DD o YYYY/MM/DD -> YYYY-MM-DD + if (!iso || !/^\d{1,2}:\d{2}:\d{2}$/.test(timeStr)) return null; + + const [H, M, S] = timeStr.split(':').map(n => parseInt(n, 10)); + // mantener construcción local solo por paridad con el snippet original + // eslint-disable-next-line no-unused-vars + const dt = new Date(`${iso}T${String(H).padStart(2,'0')}:${String(M).padStart(2,'0')}:${String(S).padStart(2,'0')}`); + + return { + doc, name, + isoDate: iso, + time: timeStr, + dt_ms: toUTCms(iso, timeStr), // ⬅️ clave + mode + }; +} diff --git a/packages/db/package.json b/packages/devices/package.json similarity index 91% rename from packages/db/package.json rename to packages/devices/package.json index 31c995d..e911589 100644 --- a/packages/db/package.json +++ b/packages/devices/package.json @@ -1,5 +1,5 @@ { - "name": "@suitecoffee/db", + "name": "@suitecoffee/devices", "version": "0.1.0", "private": true, "type": "module", diff --git a/packages/oidc/package.json b/packages/oidc/package.json new file mode 100644 index 0000000..75c7457 --- /dev/null +++ b/packages/oidc/package.json @@ -0,0 +1,12 @@ +{ + "name": "@suitecoffee/oidc", + "version": "1.0.0", + "type": "module", + "main": "src/index.mjs", + "exports": { + ".": "./src/index.mjs" + }, + "dependencies": { + "openid-client": "^6.0.0" + } +} diff --git a/packages/oidc/src/index.mjs b/packages/oidc/src/index.mjs new file mode 100644 index 0000000..39f087e --- /dev/null +++ b/packages/oidc/src/index.mjs @@ -0,0 +1,70 @@ +// @suitecoffee/oidc/src/index.mjs +// OIDC minimal (ESM) — siempre usa discovery vía OIDC_CONFIG_URL + +import { Issuer } from 'openid-client'; + +let _cached = null; + +/** + * ENV requeridas: + * - OIDC_CONFIG_URL -> https://.../.well-known/openid-configuration + * - OIDC_CLIENT_ID + * - OIDC_CLIENT_SECRET -> opcional (si tu client es confidencial) + * - OIDC_REDIRECT_URI + */ +export async function initOIDCFromEnv() { + if (_cached) return _cached; + + const configUrl = process.env.OIDC_CONFIG_URL; + const clientId = process.env.OIDC_CLIENT_ID; + const clientSecret = process.env.OIDC_CLIENT_SECRET || undefined; + const redirectUri = process.env.OIDC_REDIRECT_URI; + + // Discovery directo (assume OK) + const issuer = await Issuer.discover(configUrl); + + const client = new issuer.Client({ + client_id: clientId, + client_secret: clientSecret, + redirect_uris: [redirectUri], + response_types: ['code'], + token_endpoint_auth_method: clientSecret ? 'client_secret_post' : 'none', + }); + + _cached = { + issuer, + client, + + // Construye la URL de autorización (PKCE) + getAuthUrl({ state, nonce, code_challenge, scope = 'openid email profile' }) { + return client.authorizationUrl({ + scope, + redirect_uri: redirectUri, + code_challenge, + code_challenge_method: 'S256', + state, + nonce, + }); + }, + + // Intercambia el authorization code en el callback + async handleCallback(req, expected) { + const params = client.callbackParams(req); + return client.callback(redirectUri, params, expected); + }, + + // URL de fin de sesión (si el OP la expone) + endSessionUrl({ id_token_hint, post_logout_redirect_uri }) { + return client.endSessionUrl + ? client.endSessionUrl({ id_token_hint, post_logout_redirect_uri }) + : null; + }, + }; + + return _cached; +} + +export function getOIDC() { + if (!_cached) throw new Error('[OIDC] initOIDCFromEnv() no fue llamado aún'); + return _cached; +} diff --git a/services/app/.env.development b/services/app/.env.development index 4ad8890..cca50cc 100644 --- a/services/app/.env.development +++ b/services/app/.env.development @@ -1,62 +1,54 @@ -# ===== Runtime ===== +# ======================================================= +# Runtime NODE_ENV=development PORT=3030 +APP_BASE_URL=https://dev.suitecoffee.uy +# ======================================================= -# ===== Session (usa el Redis del stack) ===== -# Para DEV podemos reutilizar el Redis de Authentik. En prod conviene uno separado. -SESSION_SECRET=Neon*Mammal*Boaster*Ludicrous*Fender8*Crablike -SESSION_COOKIE_NAME=sc.sid - -# ===== DB principal (metadatos de SuiteCoffee) ===== -# Usa el alias de red del servicio 'db' (compose: aliases [dev-db]) -DB_HOST=dev-db -DB_NAME=dev_suitecoffee_core -DB_PORT=5432 -DB_USER=dev-user-suitecoffee -DB_PASS=dev-pass-suitecoffee +# ======================================================= +# Configuración de Dases de Datos CORE_DB_HOST=dev-db CORE_DB_NAME=dev_suitecoffee_core CORE_DB_PORT=5432 CORE_DB_USER=dev-user-suitecoffee CORE_DB_PASS=dev-pass-suitecoffee -# ===== DB tenants (Tenants de SuiteCoffee) ===== -TENANTS_HOST=dev-tenants -TENANTS_DB=dev_suitecoffee_tenants -TENANTS_PORT=5432 -TENANTS_USER=suitecoffee -TENANTS_PASS=suitecoffee - TENANTS_DB_HOST=dev-tenants TENANTS_DB_NAME=dev_suitecoffee_tenants TENANTS_DB_PORT=5432 TENANTS_DB_USER=suitecoffee TENANTS_DB_PASS=suitecoffee +# ======================================================= -# ===== Authentik — Admin API (server-to-server dentro de la red) ===== -# Usa el alias de red del servicio 'authentik' y su puerto interno 9000 + +# ======================================================= +# Sesiones +SESSION_SECRET=Neon*Mammal*Boaster*Ludicrous*Fender8*Crablike +SESSION_NAME=sc.sid +# COOKIE_DOMAIN=dev.suitecoffee.uy +# ======================================================= + + + +# ======================================================= +# Authentik y OIDC AK_TOKEN=h2apVHbd3ApMcnnSwfQPXbvximkvP8HnUE25ot3zXWuEEtJFaNCcOzDHB6Xw AK_REDIS_URL=redis://ak-redis:6379 -# ===== OIDC (DEBE coincidir con el Provider) ===== -# DEV (todo dentro de la red de Docker): -# - El auth service redirige al navegador a este issuer. Si NO tenés reverse proxy hacia Authentik, -# esta URL interna NO será accesible desde el navegador del host. En ese caso, ver nota más abajo. - -APP_BASE_URL=https://suitecoffee.uy - OIDC_LOGIN_URL=https://sso.suitecoffee.uy -OIDC_REDIRECT_URI = https://suitecoffee.uy/auth/callback +OIDC_REDIRECT_URI=https://suitecoffee.uy/auth/callback + +OIDC_CLIENT_ID=1orMM8vOvf3WkN2FejXYvUFpPtONG0Lx1eMlwIpW +OIDC_CLIENT_SECRET=t5wx13qBcM0EFQ3cGnUIAmLzvbdsQrUVPv1OGWjszWkEp35pJQ55t7vZeeShqG49kuRAaiXv6PSGJLhRfGaponGaJl8gH1uCL7KIxdmm7UihgYoAXB2dFhZV4zRxfze2 -OIDC_CLIEN_ID=1orMM8vOvf3WkN2FejXYvUFpPtONG0Lx1eMlwIpW OIDC_CONFIG_URL=https://sso.suitecoffee.uy/application/o/suitecoffee/.well-known/openid-configuration -OIDC_ISSUER=https://sso.suitecoffee.uy/application/o/suitecoffee/ -OIDC_ISSUER_DISCOVERY=https://sso.suitecoffee.uy/application/o/suitecoffee/.well-known/openid-configuration OIDC_AUTHORIZE_URL=https://sso.suitecoffee.uy/application/o/authorize/ OIDC_TOKEN_URL=https://sso.suitecoffee.uy/application/o/token/ OIDC_USERINFO_URL=https://sso.suitecoffee.uy/application/o/userinfo/ OIDC_LOGOUT_URL=https://sso.suitecoffee.uy/application/o/suitecoffee/end-session/ -OIDC_JWKS_URL=https://sso.suitecoffee.uy/application/o/suitecoffee/jwks/ \ No newline at end of file +OIDC_JWKS_URL=https://sso.suitecoffee.uy/application/o/suitecoffee/jwks/ + +# ======================================================= \ No newline at end of file diff --git a/services/app/.env.production b/services/app/.env.production deleted file mode 100644 index 0b341ba..0000000 --- a/services/app/.env.production +++ /dev/null @@ -1,22 +0,0 @@ -NODE_ENV=production # Entorno de desarrollo - -PORT=3000 # Variables del servicio -> suitecoffee-app - -# Variables del servicio -> suitecoffee-db de suitecoffee-app - -DB_HOST=prod-tenants -# Nombre de la base de datos -DB_NAME=postgres - -# Usuario y contraseña -DB_USER=postgres -DB_PASS=postgres - -# Puertos del servicio de db -DB_LOCAL_PORT=5432 -DB_DOCKER_PORT=5432 - -# Colores personalizados -COL_PRI=452D19 # Marrón oscuro -COL_SEC=D7A666 # Crema / Café -COL_BG=FFA500 # Naranja \ No newline at end of file diff --git a/services/app/package-lock.json b/services/app/package-lock.json index bedd6b7..6f6127f 100644 --- a/services/app/package-lock.json +++ b/services/app/package-lock.json @@ -9,6 +9,12 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@suitecoffee/api": "file:../../packages/api/", + "@suitecoffee/db": "file:../../packages/core/db", + "@suitecoffee/middlewares": "file:../../packages/core/middlewares", + "@suitecoffee/redis": "file:../../packages/core/redis", + "@suitecoffee/repositories": "file:../../packages/core/repositories", + "@suitecoffee/scripts": "file:../../packages/core/scripts", "bcrypt": "^6.0.0", "chalk": "^5.6.0", "connect-redis": "^9.0.0", @@ -23,6 +29,7 @@ "jose": "^6.1.0", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.2.0", + "mime": "^4.1.0", "morgan": "^1.10.1", "node-appwrite": "^20.2.1", "node-fetch": "^3.3.2", @@ -36,6 +43,37 @@ "nodemon": "^3.1.10" } }, + "../../packages/api": { + "name": "@suitecoffee/api", + "version": "1.0.0" + }, + "../../packages/core": {}, + "../../packages/core/db": { + "name": "@suitecoffee/db", + "version": "1.0.0", + "peerDependencies": { + "pg": "^8.16.3" + } + }, + "../../packages/core/middlewares": { + "name": "@suitecoffee/middlewares", + "version": "1.0.0" + }, + "../../packages/core/redis": { + "name": "@suitecoffee/redis", + "version": "1.0.0", + "peerDependencies": { + "pg": "^8.16.3" + } + }, + "../../packages/core/repositories": { + "name": "@suitecoffee/repositories", + "version": "0.0.1" + }, + "../../packages/core/scripts": { + "name": "@suitecoffee/scripts", + "version": "1.0.0" + }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "dev": true, @@ -95,6 +133,30 @@ "@redis/client": "^5.8.2" } }, + "node_modules/@suitecoffee/api": { + "resolved": "../../packages/api", + "link": true + }, + "node_modules/@suitecoffee/db": { + "resolved": "../../packages/core/db", + "link": true + }, + "node_modules/@suitecoffee/middlewares": { + "resolved": "../../packages/core/middlewares", + "link": true + }, + "node_modules/@suitecoffee/redis": { + "resolved": "../../packages/core/redis", + "link": true + }, + "node_modules/@suitecoffee/repositories": { + "resolved": "../../packages/core/repositories", + "link": true + }, + "node_modules/@suitecoffee/scripts": { + "resolved": "../../packages/core/scripts", + "link": true + }, "node_modules/@types/body-parser": { "version": "1.19.6", "license": "MIT", @@ -371,6 +433,8 @@ }, "node_modules/connect-redis": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-9.0.0.tgz", + "integrity": "sha512-QwzyvUePTMvEzG1hy45gZYw3X3YHrjmEdSkayURlcZft7hqadQ3X39wYkmCqblK2rGlw+XItELYt6GnyG6DEIQ==", "license": "MIT", "engines": { "node": ">=18" @@ -641,6 +705,8 @@ }, "node_modules/express-session": { "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", "license": "MIT", "dependencies": { "cookie": "0.7.2", @@ -1137,6 +1203,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/mime-db": { "version": "1.54.0", "license": "MIT", diff --git a/services/app/package.json b/services/app/package.json index 6b65566..ee9fb58 100644 --- a/services/app/package.json +++ b/services/app/package.json @@ -15,6 +15,15 @@ "nodemon": "^3.1.10" }, "dependencies": { + + "@suitecoffee/scripts": "file:../../packages/core/scripts", + "@suitecoffee/db": "file:../../packages/core/db", + "@suitecoffee/redis": "file:../../packages/core/redis", + "@suitecoffee/middlewares": "file:../../packages/core/middlewares", + + "@suitecoffee/api": "file:../../packages/api/", + "@suitecoffee/repositories": "file:../../packages/core/repositories", + "bcrypt": "^6.0.0", "chalk": "^5.6.0", "connect-redis": "^9.0.0", @@ -29,6 +38,7 @@ "jose": "^6.1.0", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.2.0", + "mime": "^4.1.0", "morgan": "^1.10.1", "node-appwrite": "^20.2.1", "node-fetch": "^3.3.2", @@ -37,11 +47,7 @@ "redis": "^5.8.2", "serve-favicon": "^2.5.1" }, - "imports": { - "#v1Router": "./src/api/v1/routes/routes.js", - "#pages": "./src/pages/pages.js", - "#db": "./src/db/poolSingleton.js" - }, - "keywords": [], + "imports": { }, + "keywords": [], "description": "" } diff --git a/services/app/src/api/api.js b/services/app/src/api/api.js deleted file mode 100644 index d65d8b8..0000000 --- a/services/app/src/api/api.js +++ /dev/null @@ -1,181 +0,0 @@ -// // ---------------------------------------------------------- -// // API -// // ---------------------------------------------------------- -// app.get('/api/tables', async (_req, res) => { -// res.json(ALLOWED_TABLES); -// }); - -// app.get('/api/schema/:table', async (req, res) => { -// try { -// const table = ensureTable(req.params.table); -// const client = await getClient(); -// try { -// const columns = await loadColumns(client, table); -// const fks = await loadForeignKeys(client, table); -// const enriched = columns.map(c => ({ ...c, foreign: fks[c.column_name] || null })); -// res.json({ table, columns: enriched }); -// } finally { client.release(); } -// } catch (e) { -// res.status(400).json({ error: e.message }); -// } -// }); - -// app.get('/api/options/:table/:column', async (req, res) => { -// try { -// const table = ensureTable(req.params.table); -// const column = req.params.column; -// if (!VALID_IDENT.test(column)) throw new Error('Columna inválida'); - -// const client = await getClient(); -// try { -// const fks = await loadForeignKeys(client, table); -// const fk = fks[column]; -// if (!fk) return res.json([]); - -// const refTable = fk.foreign_table; -// const refId = fk.foreign_column; -// const labelCol = await pickLabelColumn(client, refTable); - -// const sql = `SELECT ${q(refId)} AS id, ${q(labelCol)} AS label FROM ${q(refTable)} ORDER BY ${q(labelCol)} LIMIT 1000`; -// const result = await client.query(sql); -// res.json(result.rows); -// } finally { client.release(); } -// } catch (e) { -// res.status(400).json({ error: e.message }); -// } -// }); - -// app.get('/api/table/:table', async (req, res) => { -// try { -// const table = ensureTable(req.params.table); -// const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000); -// const client = await getClient(); -// try { -// const pks = await loadPrimaryKey(client, table); -// const orderBy = pks.length ? `ORDER BY ${pks.map(q).join(', ')} DESC` : ''; -// const sql = `SELECT * FROM ${q(table)} ${orderBy} LIMIT ${limit}`; -// const result = await client.query(sql); - -// // Normalizar: siempre devolver objetos {col: valor} -// const colNames = result.fields.map(f => f.name); -// let rows = result.rows; -// if (rows.length && Array.isArray(rows[0])) { -// rows = rows.map(r => Object.fromEntries(r.map((v, i) => [colNames[i], v]))); -// } -// res.json(rows); -// } finally { client.release(); } -// } catch (e) { -// res.status(400).json({ error: e.message, code: e.code, detail: e.detail }); -// } -// }); - -// app.post('/api/table/:table', async (req, res) => { -// const table = ensureTable(req.params.table); -// const payload = req.body || {}; -// try { -// const client = await getClient(); -// try { -// const columns = await loadColumns(client, table); -// const insertable = columns.filter(c => -// !c.is_primary && !c.is_identity && !(c.column_default || '').startsWith('nextval(') -// ); -// const allowedCols = new Set(insertable.map(c => c.column_name)); - -// const cols = []; -// const vals = []; -// const params = []; -// let idx = 1; -// for (const [k, v] of Object.entries(payload)) { -// if (!allowedCols.has(k)) continue; -// if (!VALID_IDENT.test(k)) continue; -// cols.push(q(k)); -// vals.push(`$${idx++}`); -// params.push(v); -// } - -// if (!cols.length) { -// const { rows } = await client.query(`INSERT INTO ${q(table)} DEFAULT VALUES RETURNING *`); -// res.status(201).json({ inserted: rows[0] }); -// } else { -// const { rows } = await client.query( -// `INSERT INTO ${q(table)} (${cols.join(', ')}) VALUES (${vals.join(', ')}) RETURNING *`, -// params -// ); -// res.status(201).json({ inserted: rows[0] }); -// } -// } catch (e) { -// if (e.code === '23503') return res.status(400).json({ error: 'Violación de clave foránea', detail: e.detail }); -// if (e.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail }); -// if (e.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail }); -// if (e.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail }); -// throw e; -// } -// } catch (e) { -// res.status(400).json({ error: e.message }); -// } -// }); - -// app.get('/api/comandas', async (req, res, next) => { -// try { -// const estado = (req.query.estado || '').trim() || null; -// const limit = Math.min(parseInt(req.query.limit || '200', 10), 1000); - -// const { rows } = await mainPool.query( -// `SELECT * FROM public.f_comandas_resumen($1, $2)`, -// [estado, limit] -// ); -// res.json(rows); -// } catch (e) { next(e); } -// }); - - -// // Detalle de una comanda (con nombres de productos) -// // GET /api/comandas/:id/detalle -// app.get('/api/comandas/:id/detalle', (req, res, next) => -// mainPool.query( -// `SELECT id_det_comanda, id_producto, producto_nombre, -// cantidad, pre_unitario, subtotal, observaciones -// FROM public.v_comandas_detalle_items -// WHERE id_comanda = $1::int -// ORDER BY id_det_comanda`, -// [req.params.id] -// ) -// .then(r => res.json(r.rows)) -// .catch(next) -// ); - -// // Cerrar comanda (setea estado y fec_cierre en DB) -// app.post('/api/comandas/:id/cerrar', async (req, res, next) => { -// try { -// const id = Number(req.params.id); -// if (!Number.isInteger(id) || id <= 0) { -// return res.status(400).json({ error: 'id inválido' }); -// } -// const { rows } = await mainPool.query( -// `SELECT public.f_cerrar_comanda($1) AS data`, -// [id] -// ); -// if (!rows.length || rows[0].data === null) { -// return res.status(404).json({ error: 'Comanda no encontrada' }); -// } -// res.json(rows[0].data); -// } catch (err) { next(err); } -// }); - -// Abrir (reabrir) comanda -app.post('/api/comandas/:id/abrir', async (req, res, next) => { - try { - const id = Number(req.params.id); - if (!Number.isInteger(id) || id <= 0) { - return res.status(400).json({ error: 'id inválido' }); - } - const { rows } = await mainPool.query( - `SELECT public.f_abrir_comanda($1) AS data`, - [id] - ); - if (!rows.length || rows[0].data === null) { - return res.status(404).json({ error: 'Comanda no encontrada' }); - } - res.json(rows[0].data); - } catch (err) { next(err); } -}); \ No newline at end of file diff --git a/services/app/src/api/rpc.js b/services/app/src/api/rpc.js deleted file mode 100644 index 56766d1..0000000 --- a/services/app/src/api/rpc.js +++ /dev/null @@ -1,230 +0,0 @@ -// // GET producto + receta -// app.get('/api/rpc/get_producto/:id', async (req, res) => { -// const id = Number(req.params.id); -// const { rows } = await mainPool.query('SELECT public.get_producto($1) AS data', [id]); -// res.json(rows[0]?.data || {}); -// }); - -// // POST guardar producto + receta - -// app.post('/api/rpc/save_producto', async (req, res) => { -// try { -// // console.debug('receta payload:', req.body?.receta); // habilitalo si lo necesitás -// const q = 'SELECT public.save_producto($1,$2,$3,$4,$5,$6,$7::jsonb) AS id_producto'; -// const { id_producto = null, nombre, img_producto = null, precio = 0, activo = true, id_categoria = null, receta = [] } = req.body || {}; -// const params = [id_producto, nombre, img_producto, precio, activo, id_categoria, JSON.stringify(receta || [])]; -// const { rows } = await mainPool.query(q, params); -// res.json(rows[0] || {}); -// } catch (e) { -// console.error(e); -// res.status(500).json({ error: 'save_producto failed' }); -// } -// }); - -// // GET MP + proveedores -// app.get('/api/rpc/get_materia/:id', async (req, res) => { -// const id = Number(req.params.id); -// try { -// const { rows } = await mainPool.query('SELECT public.get_materia_prima($1) AS data', [id]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); -// res.status(500).json({ error: 'get_materia failed' }); -// } -// }); - -// // SAVE MP + proveedores (array) -// app.post('/api/rpc/save_materia', async (req, res) => { -// const { id_mat_prima = null, nombre, unidad, activo = true, proveedores = [] } = req.body || {}; -// try { -// const q = 'SELECT public.save_materia_prima($1,$2,$3,$4,$5::jsonb) AS id_mat_prima'; -// const params = [id_mat_prima, nombre, unidad, activo, JSON.stringify(proveedores || [])]; -// const { rows } = await mainPool.query(q, params); -// res.json(rows[0] || {}); -// } catch (e) { -// console.error(e); -// res.status(500).json({ error: 'save_materia failed' }); -// } -// }); - -// // POST /api/rpc/find_usuarios_por_documentos { docs: ["12345678","09123456", ...] } -// app.post('/api/rpc/find_usuarios_por_documentos', async (req, res) => { -// try { -// const docs = Array.isArray(req.body?.docs) ? req.body.docs : []; -// const sql = 'SELECT public.find_usuarios_por_documentos($1::jsonb) AS data'; -// const { rows } = await mainPool.query(sql, [JSON.stringify(docs)]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); -// res.status(500).json({ error: 'find_usuarios_por_documentos failed' }); -// } -// }); - -// // POST /api/rpc/import_asistencia { registros: [...], origen?: "AGL_001.txt" } -// app.post('/api/rpc/import_asistencia', async (req, res) => { -// try { -// const registros = Array.isArray(req.body?.registros) ? req.body.registros : []; -// const origen = req.body?.origen || null; -// const sql = 'SELECT public.import_asistencia($1::jsonb,$2) AS data'; -// const { rows } = await mainPool.query(sql, [JSON.stringify(registros), origen]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); -// res.status(500).json({ error: 'import_asistencia failed' }); -// } -// }); - -// // Consultar datos de asistencia (raw + pares) para un usuario y rango -// app.post('/api/rpc/asistencia_get', async (req, res) => { -// try { -// const { doc, desde, hasta } = req.body || {}; -// const sql = 'SELECT public.asistencia_get($1::text,$2::date,$3::date) AS data'; -// const { rows } = await mainPool.query(sql, [doc, desde, hasta]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); res.status(500).json({ error: 'asistencia_get failed' }); -// } -// }); - -// // Editar un registro crudo y recalcular pares -// app.post('/api/rpc/asistencia_update_raw', async (req, res) => { -// try { -// const { id_raw, fecha, hora, modo } = req.body || {}; -// const sql = 'SELECT public.asistencia_update_raw($1::bigint,$2::date,$3::text,$4::text) AS data'; -// const { rows } = await mainPool.query(sql, [id_raw, fecha, hora, modo ?? null]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); res.status(500).json({ error: 'asistencia_update_raw failed' }); -// } -// }); - -// // Eliminar un registro crudo y recalcular pares -// app.post('/api/rpc/asistencia_delete_raw', async (req, res) => { -// try { -// const { id_raw } = req.body || {}; -// const sql = 'SELECT public.asistencia_delete_raw($1::bigint) AS data'; -// const { rows } = await mainPool.query(sql, [id_raw]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); res.status(500).json({ error: 'asistencia_delete_raw failed' }); -// } -// }); - -// // POST /api/rpc/report_tickets { year } -// app.post('/api/rpc/report_tickets', async (req, res) => { -// try { -// const y = parseInt(req.body?.year ?? req.query?.year, 10); -// const year = (Number.isFinite(y) && y >= 2000 && y <= 2100) -// ? y -// : (new Date()).getFullYear(); - -// const { rows } = await mainPool.query( -// 'SELECT public.report_tickets_year($1::int) AS j', [year] -// ); -// res.json(rows[0].j); -// } catch (e) { -// console.error('report_tickets error:', e); -// res.status(500).json({ -// error: 'report_tickets failed', -// message: e.message, detail: e.detail, where: e.where, code: e.code -// }); -// } -// }); - -// // POST /api/rpc/report_asistencia { desde: 'YYYY-MM-DD', hasta: 'YYYY-MM-DD' } -// app.post('/api/rpc/report_asistencia', async (req, res) => { -// try { -// let { desde, hasta } = req.body || {}; -// // defaults si vienen vacíos/invalidos -// const re = /^\d{4}-\d{2}-\d{2}$/; -// if (!re.test(desde) || !re.test(hasta)) { -// const end = new Date(); -// const start = new Date(end); start.setDate(end.getDate() - 30); -// desde = start.toISOString().slice(0, 10); -// hasta = end.toISOString().slice(0, 10); -// } - -// const { rows } = await mainPool.query( -// 'SELECT public.report_asistencia($1::date,$2::date) AS j', [desde, hasta] -// ); -// res.json(rows[0].j); -// } catch (e) { -// console.error('report_asistencia error:', e); -// res.status(500).json({ -// error: 'report_asistencia failed', -// message: e.message, detail: e.detail, where: e.where, code: e.code -// }); -// } -// }); - -// // Guardar (insert/update) -// app.post('/api/rpc/save_compra', async (req, res) => { -// try { -// const { id_compra, id_proveedor, fec_compra, detalles } = req.body || {}; -// const sql = 'SELECT * FROM public.save_compra($1::int,$2::int,$3::timestamptz,$4::jsonb)'; -// const args = [id_compra ?? null, id_proveedor, fec_compra ? new Date(fec_compra) : null, JSON.stringify(detalles)]; -// const { rows } = await mainPool.query(sql, args); -// res.json(rows[0]); // { id_compra, total } -// } catch (e) { -// console.error('save_compra error:', e); -// res.status(500).json({ error: 'save_compra failed', message: e.message, detail: e.detail, where: e.where, code: e.code }); -// } -// }); - -// // Obtener para editar -// app.post('/api/rpc/get_compra', async (req, res) => { -// try { -// const { id_compra } = req.body || {}; -// const sql = `SELECT public.get_compra($1::int) AS data`; -// const { rows } = await mainPool.query(sql, [id_compra]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); res.status(500).json({ error: 'get_compra failed' }); -// } -// }); - -// // Eliminar -// app.post('/api/rpc/delete_compra', async (req, res) => { -// try { -// const { id_compra } = req.body || {}; -// await mainPool.query(`SELECT public.delete_compra($1::int)`, [id_compra]); -// res.json({ ok: true }); -// } catch (e) { -// console.error(e); res.status(500).json({ error: 'delete_compra failed' }); -// } -// }); - -// // POST /api/rpc/report_gastos { year: 2025 } -// app.post('/api/rpc/report_gastos', async (req, res) => { -// try { -// const year = parseInt(req.body?.year ?? new Date().getFullYear(), 10); -// const { rows } = await mainPool.query( -// 'SELECT public.report_gastos($1::int) AS j', [year] -// ); -// res.json(rows[0].j); -// } catch (e) { -// console.error('report_gastos error:', e); -// res.status(500).json({ -// error: 'report_gastos failed', -// message: e.message, detail: e.detail, code: e.code -// }); -// } -// }); - -// // (Opcional) GET para probar rápido desde el navegador: -// // /api/rpc/report_gastos?year=2025 -// app.get('/api/rpc/report_gastos', async (req, res) => { -// try { -// const year = parseInt(req.query.year ?? new Date().getFullYear(), 10); -// const { rows } = await mainPool.query( -// 'SELECT public.report_gastos($1::int) AS j', [year] -// ); -// res.json(rows[0].j); -// } catch (e) { -// console.error('report_gastos error:', e); -// res.status(500).json({ -// error: 'report_gastos failed', -// message: e.message, detail: e.detail, code: e.code -// }); -// } -// }); \ No newline at end of file diff --git a/services/app/src/db/poolSingleton.js b/services/app/src/db/poolSingleton.js deleted file mode 100644 index a0935c2..0000000 --- a/services/app/src/db/poolSingleton.js +++ /dev/null @@ -1,83 +0,0 @@ -// Coneción Singleton a base de datos. - -import { Pool } from 'pg'; - -class DatabaseCore { - constructor() { - - if (DatabaseCore.instance) { - return Database.instance; - } - - const config = { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - port: process.env.DB_LOCAL_PORT ? Number(process.env.DB_LOCAL_PORT) : undefined, - ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined, - }; - - this.connection = new Pool(config); - - DatabaseCore.instance = this; - } - async query(sql, params) { - return this.connection.query(sql,params); - } - - async connect() { /* Definida solo para evitar errores */ - return this.connection.connect(); - } - async getClient() { - return this.connection.connect(); - } - - async release() { - await this.connection.end(); - } -} -class DatabaseTenants { - constructor() { - - if (DatabaseTenants.instance) { - return Database.instance; - } - - const config = { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - port: process.env.DB_LOCAL_PORT ? Number(process.env.DB_LOCAL_PORT) : undefined, - ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined, - }; - - this.connection = new Pool(config); - - DatabaseTenants.instance = this; - } - async query(sql, params) { - return this.connection.query(sql,params); - } - - async connect() { /* Definida solo para evitar errores */ - return this.connection.connect(); - } - async getClient() { - return this.connection.connect(); - } - - async release() { - await this.connection.end(); - } -} - -// const db = new Database(); -// db.query('SELECT * FROM users'); - -const poolCore = new DatabaseCore(); -const poolTenants = new DatabaseTenants(); -export default {poolCore, poolTenants}; -export { poolCore, poolTenants }; -//export { DatabaseCore, DatabaseTenants }; \ No newline at end of file diff --git a/services/app/src/index.mjs b/services/app/src/index.mjs index 26f334e..b122704 100644 --- a/services/app/src/index.mjs +++ b/services/app/src/index.mjs @@ -5,14 +5,18 @@ import 'dotenv/config'; import favicon from 'serve-favicon'; // Favicon +import session from 'express-session'; import express from 'express'; // Framework para enderizado de apps Web import expressLayouts from 'express-ejs-layouts'; -// import { poolCore, poolTenants } from '@suitecoffee/db'; // dbCore y dbTenants desde módulo -import { poolCore, poolTenants } from '#db'; // dbCore y dbTenants -import v1Router from '#v1Router'; // Rutas API v1 -import expressPages from '#pages'; // Rutas "/", "/dashboard", ... +import { RedisStore } from 'connect-redis'; +import { checkRequiredEnvVars } from '@suitecoffee/scripts'; +import { verificarConexionCore, verificarConexionTenants } from '@suitecoffee/db'; // dbCore y dbTenants desde paquete +import { redisAuthentik, verificarConexionRedisAuthentik} from '@suitecoffee/redis'; +import { requireAuth, datosGlobales, tenantContext, resolveTenantFromCore } from '@suitecoffee/middlewares'; +import { apiv1 } from '@suitecoffee/api'; // Rutas API v1 +import expressPages from './views/routes.js'; // Rutas "/", "/dashboard", ... import path from 'path'; import { fileURLToPath } from 'url'; // Converts a file:// URL string or URL object into a platform-specific file import cookieParser from 'cookie-parser'; @@ -21,417 +25,122 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); - - - - - - // ----------------------------------------------------------------------------- // Validación de entorno mínimo (ajusta nombres si difieren) // ----------------------------------------------------------------------------- - -// Función para verificar que ciertas variables de entorno estén definida -function checkRequiredEnvVars(...requiredKeys) { - const missingKeys = requiredKeys.filter((key) => !process.env[key]); // Filtramos las que NO existen en process.env - if (missingKeys.length > 0) { // Si falta alguna, mostramos una advertencia - console.warn( - `[APP] No se encontraron las siguientes variables de entorno: \n\n-> ${missingKeys.join('\n-> ')}`+ - `\n` - ); - } -} - checkRequiredEnvVars( 'PORT', 'APP_BASE_URL', - 'CORE_DB_HOST', 'CORE_DB_PORT', 'CORE_DB_NAME', - 'TENANTS_DB_HOST', 'TENANTS_DB_PORT', 'TENANTS_DB_NAME', - - 'OIDC_LOGIN_URL', 'OIDC_REDIRECT_URI', - 'OIDC_CLIEN_ID', 'OIDC_CONFIG_URL', 'OIDC_ISSUER', - 'OIDC_ISSUER_DISCOVERY', 'OIDC_AUTHORIZE_URL', 'OIDC_TOKEN_URL', - 'OIDC_USERINFO_URL', 'OIDC_LOGOUT_URL', 'OIDC_JWKS_URL', - - 'SESSION_SECRET', 'SESSION_COOKIE_NAME', - 'AK_REDIS_URL', 'AK_TOKEN' + 'SESSION_SECRET', 'SESSION_NAME', 'AK_REDIS_URL', + 'OIDC_CLIENT_ID', 'OIDC_REDIRECT_URI', + 'OIDC_CONFIG_URL' // o 'OIDC_ISSUER' ); - - // ---------------------------------------------------------- // Variables del sistema // ---------------------------------------------------------- // De entorno -const PORT = process.env.PORT; -const APP_BASE_URL = process.env.APP_BASE_URL; - -const CORE_DB_HOST = process.env.CORE_DB_HOST; -const CORE_DB_PORT = process.env.CORE_DB_PORT; -const CORE_DB_NAME = process.env.CORE_DB_NAME; - -const TENANTS_DB_HOST = process.env.TENANTS_DB_HOST; -const TENANTS_DB_PORT = process.env.TENANTS_DB_PORT; -const TENANTS_DB_NAME = process.env.TENANTS_DB_NAME; - -const OIDC_LOGIN_URL = process.env.OIDC_LOGIN_URL; -const OIDC_REDIRECT_URI = process.env.OIDC_REDIRECT_URI; - -const OIDC_CLIEN_ID = process.env.OIDC_CLIEN_ID; -const OIDC_CONFIG_URL = process.env.OIDC_CONFIG_URL; -const OIDC_ISSUER = process.env.OIDC_ISSUER; -const OIDC_ISSUER_DISCOVERY = process.env.OIDC_ISSUER_DISCOVERY; -const OIDC_AUTHORIZE_URL = process.env.OIDC_AUTHORIZE_URL; -const OIDC_TOKEN_URL = process.env.OIDC_TOKEN_URL; -const OIDC_USERINFO_URL = process.env.OIDC_USERINFO_URL; -const OIDC_LOGOUT_URL = process.env.OIDC_LOGOUT_URL; -const OIDC_JWKS_URL = process.env.OIDC_JWKS_URL; - -const AK_SESSION_SECRET = process.env.AK_SESSION_SECRET; -const AK_SESSION_COOKIE_NAME = process.env.AK_SESSION_COOKIE_NAME; -const AK_REDIS_URL = process.env.AK_REDIS_URL; - - - -const url = v => !v ? "" : (v.startsWith("http") ? v : `/img/productos/${v}`); -const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/; -const q = (s) => `"${String(s).replace(/"/g, '""')}"`; // Identificadores SQL -> comillas dobles y escape correcto +const PORT = process.env.PORT; +const SESSION_NAME = process.env.SESSION_NAME; +const SESSION_SECRET = process.env.SESSION_SECRET; +const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN; // ---------------------------------------------------------- // App + Motor de vistas EJS // ---------------------------------------------------------- - const app = express(); -app.set('trust proxy', true); -app.set("views", path.join(__dirname, "views")); -app.set("view engine", "ejs"); -app.set("layout", "layouts/main"); app.disable("x-powered-by"); +app.set('trust proxy', true); +app.set("views", path.join(__dirname, "views/pages")); +app.set("layout", path.join(__dirname, "views/layouts/main")); +// app.set("layout", "layouts/main"); +app.set("view engine", "ejs"); +app.use(favicon(path.join(__dirname, 'public', 'favicon', 'favicon.ico'), { maxAge: '1y' })); app.use(express.json()); app.use(express.json({ limit: '1mb' })); app.use(express.urlencoded({ extended: true })); -// Archivos estáticos que fuerzan la re-descarga de arhivos -app.use(favicon(path.join(__dirname, 'public', 'favicon', 'favicon.ico'), { maxAge: '1y' })); -app.use(express.static(path.join(__dirname, "public"), { - etag: false, maxAge: 0, - setHeaders: (res, path) => { - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); - } -})); + +app.use(express.static(path.join(__dirname,"public"), { etag: false, maxAge: 0, setHeaders: (res, path) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); }})); app.use(expressLayouts); -app.use(cookieParser(process.env.SESSION_SECRET)); -app.use(expressPages); // Renderizado trae las paginas desde ./services/manso/src/routes/routes.js -// ---------------------------------------------------------- -// Uso de API v1 -// ---------------------------------------------------------- -app.use("/api/v1", v1Router); - -// /api/rpc/get_producto/:id -// /api/v1/rpc/get_producto/:id -> /rpc/get_producto/:id - - -// ---------------------------------------------------------- -// Seguridad: Tablas permitidas -// ---------------------------------------------------------- -const ALLOWED_TABLES = [ - 'roles','usuarios','usua_roles', - 'categorias','productos', - 'clientes','mesas', - 'comandas','deta_comandas', - 'proveedores','compras','deta_comp_producto', - 'mate_primas','deta_comp_materias', - 'prov_producto','prov_mate_prima', - 'receta_producto', 'asistencia_resumen_diario', - 'asistencia_intervalo', 'asistencia_detalle', - 'vw_compras' -]; - -function ensureTable(name) { - const t = String(name || '').toLowerCase(); - if (!ALLOWED_TABLES.includes(t)) throw new Error('Tabla no permitida'); - return t; -} - - - - -// ---------------------------------------------------------- -// Introspección de esquema -// ---------------------------------------------------------- -async function loadColumns(client, table) { - const sql = ` - SELECT - c.column_name, - c.data_type, - c.is_nullable = 'YES' AS is_nullable, - c.column_default, - (SELECT EXISTS ( - SELECT 1 FROM pg_attribute a - JOIN pg_class t ON t.oid = a.attrelid - JOIN pg_index i ON i.indrelid = t.oid AND a.attnum = ANY(i.indkey) - WHERE t.relname = $1 AND i.indisprimary AND a.attname = c.column_name - )) AS is_primary, - (SELECT a.attgenerated = 's' OR a.attidentity IN ('a','d') - FROM pg_attribute a - JOIN pg_class t ON t.oid = a.attrelid - WHERE t.relname = $1 AND a.attname = c.column_name - ) AS is_identity - FROM information_schema.columns c - WHERE c.table_schema='public' AND c.table_name=$1 - ORDER BY c.ordinal_position - `; - const { rows } = await client.query(sql, [table]); - return rows; -} - -async function loadForeignKeys(client, table) { - const sql = ` - SELECT - kcu.column_name, - ccu.table_name AS foreign_table, - ccu.column_name AS foreign_column - FROM information_schema.table_constraints tc - JOIN information_schema.key_column_usage kcu - ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema - JOIN information_schema.constraint_column_usage ccu - ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema - WHERE tc.table_schema='public' AND tc.table_name=$1 AND tc.constraint_type='FOREIGN KEY' - `; - const { rows } = await client.query(sql, [table]); - const map = {}; - for (const r of rows) map[r.column_name] = { foreign_table: r.foreign_table, foreign_column: r.foreign_column }; - return map; -} - -async function loadPrimaryKey(client, table) { - const sql = ` - SELECT a.attname AS column_name - FROM pg_index i - JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) - JOIN pg_class t ON t.oid = i.indrelid - WHERE t.relname = $1 AND i.indisprimary - `; - const { rows } = await client.query(sql, [table]); - return rows.map(r => r.column_name); -} - -// label column for FK options -async function pickLabelColumn(client, refTable) { - const preferred = ['nombre','raz_social','apodo','documento','correo','telefono']; - const { rows } = await client.query( - `SELECT column_name, data_type - FROM information_schema.columns - WHERE table_schema='public' AND table_name=$1 - ORDER BY ordinal_position`, [refTable] - ); - for (const cand of preferred) { - if (rows.find(r => r.column_name === cand)) return cand; - } - const textish = rows.find(r => /text|character varying|varchar/i.test(r.data_type)); - if (textish) return textish.column_name; - return rows[0]?.column_name || 'id'; -} - -// ---------------------------------------------------------- -// Middleware para datos globales -// ---------------------------------------------------------- -app.use((req, res, next) => { - res.locals.currentPath = req.path; - res.locals.pageTitle = "SuiteCoffee"; - res.locals.pageId = ""; - next(); -}); - - - - -// ---------------------------------------------------------- -// API -// ---------------------------------------------------------- -app.get('/api/tables', async (_req, res) => { - res.json(ALLOWED_TABLES); -}); - -app.get('/api/schema/:table', async (req, res) => { - try { - const table = ensureTable(req.params.table); - const client = await pool.getClient(); - try { - const columns = await loadColumns(client, table); - const fks = await loadForeignKeys(client, table); - const enriched = columns.map(c => ({ ...c, foreign: fks[c.column_name] || null })); - res.json({ table, columns: enriched }); - } finally { client.release(); } - } catch (e) { - res.status(400).json({ error: e.message }); - } -}); - -app.get('/api/options/:table/:column', async (req, res) => { - try { - const table = ensureTable(req.params.table); - const column = req.params.column; - if (!VALID_IDENT.test(column)) throw new Error('Columna inválida'); - - const client = await pool.getClient(); - try { - const fks = await loadForeignKeys(client, table); - const fk = fks[column]; - if (!fk) return res.json([]); - - const refTable = fk.foreign_table; - const refId = fk.foreign_column; - const labelCol = await pickLabelColumn(client, refTable); - - const sql = `SELECT ${q(refId)} AS id, ${q(labelCol)} AS label FROM ${q(refTable)} ORDER BY ${q(labelCol)} LIMIT 1000`; - const result = await client.query(sql); - res.json(result.rows); - } finally { client.release(); } - } catch (e) { - res.status(400).json({ error: e.message }); - } -}); - -app.get('/api/table/:table', async (req, res) => { - try { - const table = ensureTable(req.params.table); - const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000); - const client = await pool.getClient(); - try { - const pks = await loadPrimaryKey(client, table); - const orderBy = pks.length ? `ORDER BY ${pks.map(q).join(', ')} DESC` : ''; - const sql = `SELECT * FROM ${q(table)} ${orderBy} LIMIT ${limit}`; - const result = await client.query(sql); - - // Normalizar: siempre devolver objetos {col: valor} - const colNames = result.fields.map(f => f.name); - let rows = result.rows; - if (rows.length && Array.isArray(rows[0])) { - rows = rows.map(r => Object.fromEntries(r.map((v, i) => [colNames[i], v]))); - } - res.json(rows); - } finally { client.release(); } - } catch (e) { - res.status(400).json({ error: e.message, code: e.code, detail: e.detail }); - } -}); - -app.post('/api/table/:table', async (req, res) => { - const table = ensureTable(req.params.table); - const payload = req.body || {}; - try { - const client = await pool.getClient(); - try { - const columns = await loadColumns(client, table); - const insertable = columns.filter(c => - !c.is_primary && !c.is_identity && !(c.column_default || '').startsWith('nextval(') - ); - const allowedCols = new Set(insertable.map(c => c.column_name)); - - const cols = []; - const vals = []; - const params = []; - let idx = 1; - for (const [k, v] of Object.entries(payload)) { - if (!allowedCols.has(k)) continue; - if (!VALID_IDENT.test(k)) continue; - cols.push(q(k)); - vals.push(`$${idx++}`); - params.push(v); - } - - if (!cols.length) { - const { rows } = await client.query(`INSERT INTO ${q(table)} DEFAULT VALUES RETURNING *`); - res.status(201).json({ inserted: rows[0] }); - } else { - const { rows } = await client.query( - `INSERT INTO ${q(table)} (${cols.join(', ')}) VALUES (${vals.join(', ')}) RETURNING *`, - params - ); - res.status(201).json({ inserted: rows[0] }); - } - } catch (e) { - if (e.code === '23503') return res.status(400).json({ error: 'Violación de clave foránea', detail: e.detail }); - if (e.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail }); - if (e.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail }); - if (e.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail }); - throw e; - } finally { client.release(); } - } catch (e) { - res.status(400).json({ error: e.message }); - } -}); - - - - - - - - - -// ---------------------------------------------------------- -// Verificación de conexión -// ---------------------------------------------------------- - -async function verificarConexionCore() { - try { - console.log(`[APP] Comprobando accesibilidad a la db ${CORE_DB_NAME} del host ${CORE_DB_HOST} ...`); - const client = await poolCore.connect(); - const { rows } = await client.query('SELECT NOW() AS ahora'); - console.log(`\n[APP] Conexión con ${CORE_DB_NAME} OK. Hora DB:`, rows[0].ahora); - client.release(); - } catch (error) { - console.error('[APP] Error al conectar con la base de datos al iniciar:', error.message); - console.error('[APP] Revisar credenciales, accesos de red y firewall.'); - } -} -async function verificarConexionTenants() { - try { - console.log(`[APP] Comprobando accesibilidad a la db ${TENANTS_DB_NAME} del host ${TENANTS_DB_HOST} ...`); - const client = await poolTenants.connect(); - const { rows } = await client.query('SELECT NOW() AS ahora'); - console.log(`\n[APP] Conexión con ${TENANTS_DB_NAME} OK. Hora DB:`, rows[0].ahora); - client.release(); - } catch (error) { - console.error('[APP] Error al conectar con la base de datos al iniciar:', error.message); - console.error('[APP] Revisar credenciales, accesos de red y firewall.'); - } -} // ----------------------------------------------------------------------------- -// 404 + Manejo de errores +// Redis // ----------------------------------------------------------------------------- +await redisAuthentik.connect(); +const redisClient = redisAuthentik.getClient(); -/*app.use((req, res) => res.status(404).json({ error: 'Error 404, No se encontró la página', path: req.originalUrl })); -app.use((err, _req, res, _next) => { - console.error('[APP] ', err); - if (res.headersSent) return; - res.status(500).json({ error: '¡Oh! A ocurrido un error en el servidor app.', detail: err.stack || String(err) }); -});*/ + +// ----------------------------------------------------------------------------- +// Cookies de sesión +// ----------------------------------------------------------------------------- +app.use(cookieParser(SESSION_SECRET)); + +app.use(session({ + name: SESSION_NAME, + store: new RedisStore({ client: redisClient, prefix: 'sess:' }), + secret: SESSION_SECRET, + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + sameSite: 'lax', // 'none' si necesitás third-party estricto + secure: process.env.NODE_ENV === 'production', + ...(COOKIE_DOMAIN ? { domain: COOKIE_DOMAIN } : {}), // ✅ compatibilidad subdominios + }, +})); + + + +// ---------------------------------------------------------- +// Renderizado de las páginas importadas desde '#pages' + configuración global +// ---------------------------------------------------------- +// app.use(expressPages); // Renderizado trae las paginas desde ./services/manso/src/routes/routes.js +// app.use(requireAuth({ redirectTo: '/auth/login' }), expressPages); // Renderizado trae las paginas desde ./services/manso/src/routes/routes.js +// app.use(requireAuth({ redirectTo: '/auth/login' }), tenantContext({ debug: true }), expressPages); // Renderizado trae las paginas desde ./services/manso/src/routes/routes.js + +app.use( + requireAuth({ redirectTo: '/auth/login' }), + tenantContext({ + debug: true, + resolveTenant: (req, sess) => resolveTenantFromCore(req, sess, { debug: true }), + // acceptStates: ['ready'] // (default) o ['ready','provisioning'] si querés permitir provisión + }), + expressPages +); + +app.use(datosGlobales); + + + + +// ---------------------------------------------------------- +// API v1 +// ---------------------------------------------------------- +app.use("/api/v1", requireAuth({ redirectTo: '/auth/login' }), tenantContext(), apiv1); // ---------------------------------------------------------- // Inicio del servidor // ---------------------------------------------------------- -app.listen(PORT, () => { +app.listen(PORT, async () => { console.log(`[APP] SuiteCoffee corriendo en http://localhost:${PORT}`); - verificarConexionCore(); - verificarConexionTenants(); + await verificarConexionCore(); + await verificarConexionTenants(); + await verificarConexionRedisAuthentik(); }); // ----------------------------------------------------------------------------- // Healthcheck // ----------------------------------------------------------------------------- app.get('/health', (_req, res) => { - res.status(200).json({ status: 'ok'}), - console.log(`[APP] Saludable`) + res.status(200).json({ status: 'ok'}) + // console.log(`[APP] Saludable`) }); \ No newline at end of file diff --git a/services/app/src/middlewares/tenant.js b/services/app/src/middlewares/tenant.js deleted file mode 100644 index 78937bb..0000000 --- a/services/app/src/middlewares/tenant.js +++ /dev/null @@ -1,37 +0,0 @@ -// ----------------------------------------------------------------------------- -// Middlewares de Auth/Tenant -// ----------------------------------------------------------------------------- - -export function requireAuth(req, res, next) { - const authHeader = req.headers["authorization"]; - if (!authHeader) return res.status(401).send("Falta token"); - - const token = authHeader.split(" ")[1]; - - jwt.verify(token, getKey, { algorithms: ["RS256"] }, (err, decoded) => { - if (err) return res.status(403).send("Token inválido o vencido"); - - // Guardamos los claims del token en req.user - req.user = { - id: decoded.sub, - email: decoded.email, - username: decoded.preferred_username, - name: decoded.name, - roles: decoded.groups || [] - }; - - next(); - }); -} - -// export function exposeViewState(req, res, next) { -// res.locals.pageTitle = res.locals.pageTitle || ''; -// res.locals.pageId = res.locals.pageId || ''; -// res.locals.tenant_uuid = req.session?.tenant?.uuid || null; -// res.locals.ak_user_uuid = req.session?.tenant?.ak_user_uuid || null; -// // también pásalos como props al render -// res.locals.viewUser = req.session?.user || null; -// res.locals.viewCookies = req.cookies || {}; -// res.locals.viewSession = req.session ? JSON.parse(JSON.stringify(req.session)) : {}; -// next(); -// } \ No newline at end of file diff --git a/services/app/src/pages/pages.js b/services/app/src/pages/pages.js deleted file mode 100644 index 68d657a..0000000 --- a/services/app/src/pages/pages.js +++ /dev/null @@ -1,67 +0,0 @@ -// services/manso/src/api/v1/routes/routes.js - -import { Router } from 'express'; - -const router = Router(); - -// ---------------------------------------------------------- -// Rutas de UI -// ---------------------------------------------------------- - -router.get('/', (req, res) => { - res.locals.pageTitle = "Inicio"; // Título de pestaña - res.locals.pageId = "home"; // Sidebar contextual - res.render("dashboard"); // Archivo .ejs a renderizar - // res.json({ ok: true, route: '/inicio' }); // Debug json -}); - -router.get('/dashboard', (req, res) => { - res.locals.pageTitle = "Dashboard"; - res.locals.pageId = "dashboard"; - res.render("dashboard"); - // res.json({ ok: true, route: '/dashboard' }); -}); - -router.get('/comandas', (req, res) => { - res.locals.pageTitle = "Comandas"; - res.locals.pageId = "comandas"; - res.render("comandas"); - // res.json({ ok: true, route: '/comandas' }); -}); - -router.get('/estadoComandas', (req, res) => { - res.locals.pageTitle = "Estado"; - res.locals.pageId = "estadoComandas"; - res.render("estadoComandas"); - // res.json({ ok: true, route: '/estadoComandas' }); -}); - -router.get('/productos', (req, res) => { - res.locals.pageTitle = "Propductos"; - res.locals.pageId = "productos"; - res.render("productos"); - // res.json({ ok: true, route: '/productos' }); -}); - -router.get('/usuarios', (req, res) => { - res.locals.pageTitle = "Usuarios"; - res.locals.pageId = "usuarios"; - res.render("usuarios"); - // res.json({ ok: true, route: '/usuarios' }); -}); - -router.get('/reportes', (req, res) => { - res.locals.pageTitle = "Reportes"; - res.locals.pageId = "reportes"; - res.render("reportes"); - // res.json({ ok: true, route: '/reportes' }); -}); - -router.get('/compras', (req, res) => { - res.locals.pageTitle = "Compras"; - res.locals.pageId = "compras"; - res.render("compras"); - // res.json({ ok: true, route: '/compras' }); -}); - -export default router; \ No newline at end of file diff --git a/services/app/src/public/scripts/comandas/comandas.mjs b/services/app/src/public/scripts/comandas/comandas.mjs new file mode 100644 index 0000000..cc75ff1 --- /dev/null +++ b/services/app/src/public/scripts/comandas/comandas.mjs @@ -0,0 +1,341 @@ +// services/app/src/public/scripts/comandas/index.mjs +// ------------------------------------------------------------ +// SuiteCoffee — Comandas (frontend script) +// - Resuelve el schema activo desde la sesión (/auth/debug/session) +// - Setea header X-Tenant-Schema en todos los fetch +// - Asegura envío de cookies (credentials:'same-origin') +// - Carga y cachea el catálogo de productos (descubrimiento de endpoint) +// - Expone helpers globales (jget/jpost/jput/jdel, getActiveSchema, productos) +// ------------------------------------------------------------ + +'use strict'; + +/* ======================= + * Utils + * ======================= */ +const noDash = (v) => (v == null ? '' : String(v).replace(/-/g, '')); +const isJson = (res) => (res.headers.get('content-type') || '').includes('application/json'); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const byId = (id) => document.getElementById(id); + +/* ======================= + * Tenant schema cache + * ======================= */ +let SC_ACTIVE_SCHEMA = null; + +/** + * Lee la sesión desde /auth/debug/session y determina el esquema activo. + * Prioriza: + * 1) user.active_schema / user.activeSchema + * 2) membership de user.current_tenant / user.currentTenant + * 3) primer membership disponible + * Lanza Error si no puede determinarlo. + */ +async function getActiveSchema() { + if (SC_ACTIVE_SCHEMA) return SC_ACTIVE_SCHEMA; + + let ses = null; + try { + const res = await fetch('/auth/debug/session', { credentials: 'same-origin' }); + ses = res.ok ? await res.json() : null; + } catch (_) { /* ignore */ } + + const u = ses?.user ?? null; + const memberships = Array.isArray(u?.memberships) ? u.memberships : []; + + // 1) Esquema activo directo + let schema = + u?.active_schema || + u?.activeSchema || + // 2) Membership del current tenant + memberships.find( + (m) => + String(m.tenant_id) === String(u?.current_tenant) || + String(m.tenantId) === String(u?.currentTenant) + )?.schema_name || + // 3) Primer membership + memberships[0]?.schema_name || + null; + + if (!schema) throw new Error('Sesión inválida o tenant no seleccionado'); + + SC_ACTIVE_SCHEMA = schema; + return SC_ACTIVE_SCHEMA; +} + +/* ======================= + * Fetch helpers (con schema) + * ======================= */ +async function buildHeaders(extra = {}) { + const schema = await getActiveSchema(); + return { Accept: 'application/json', 'X-Tenant-Schema': schema, ...extra }; +} + +async function jfetch(url, opts = {}) { + const headers = await buildHeaders(opts.headers || {}); + const res = await fetch(url, { + ...opts, + headers, + credentials: 'same-origin', // imprescindible para que viajen las cookies + }); + + let payload = null; + try { + payload = isJson(res) ? await res.json() : await res.text(); + } catch (_) { /* ignore parse errors */ } + + if (!res.ok) { + const msg = (payload && payload.error) || `${res.status} ${res.statusText}`; + throw new Error(msg); + } + return payload; +} + +async function jget(url) { return jfetch(url, { method: 'GET' }); } +async function jpost(url, body) { + return jfetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body ?? {}) }); +} +async function jput(url, body) { + return jfetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body ?? {}) }); +} +async function jdel(url, body) { + return jfetch(url, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined }); +} + +/* ======================= + * Productos (catálogo) + * - Descubre endpoint + * - Cachea y normaliza + * ======================= */ +const productos = (() => { + // Posibles endpoints (se prueban en orden) + const CANDIDATES = [ + '/api/v1/productos?limit=1000', + '/api/v1/productos', + '/api/v1/catalogo?limit=1000', + '/api/v1/catalogo', + '/api/v1/items?limit=1000', + '/api/v1/items', + ]; + + // Estado en memoria + let discoveredEndpoint = null; + let cache = /** @type {Array| null} */(null); + let lastLoadedAt = null; + + // Normalización a { id, nombre, precio, categoria?, activo? } + function normalizeOne(p) { + const id = + p.id ?? p.id_producto ?? p.producto_id ?? p.productId ?? p.pk ?? null; + + const nombre = + p.nombre ?? p.producto_nombre ?? p.name ?? p.titulo ?? p.title ?? ''; + + const precio = + p.precio ?? p.price ?? p.pre_unitario ?? p.pu ?? p.monto ?? 0; + + const categoria = + p.categoria ?? p.category ?? p.nombre_categoria ?? null; + + const activo = + p.activo ?? p.enabled ?? p.habilitado ?? true; + + return { id, nombre, precio, categoria, activo, raw: p }; + } + + function normalizePayload(data) { + if (Array.isArray(data)) return data.map(normalizeOne); + // objetos comunes: { items: [...] } | { rows: [...] } | { data: [...] } + const arr = data?.items || data?.rows || data?.data || data?.productos || null; + return Array.isArray(arr) ? arr.map(normalizeOne) : []; + } + + async function discoverEndpoint() { + // si ya lo descubrimos, reusar + if (discoveredEndpoint) return discoveredEndpoint; + + for (const url of CANDIDATES) { + try { + const data = await jget(url); + const list = normalizePayload(data); + if (list.length >= 0) { // aceptar 0+ items (catálogo vacío) + discoveredEndpoint = url; + cache = list; + lastLoadedAt = new Date(); + return discoveredEndpoint; + } + } catch (_) { + // probar el siguiente + } + } + throw new Error('No se pudo descubrir el endpoint de productos'); + } + + async function ensureLoaded({ force = false } = {}) { + await discoverEndpoint(); + if (!force && cache && Array.isArray(cache)) return cache; + + // recargar desde endpoint descubierto + const base = discoveredEndpoint.split('?')[0]; + const url = base.includes('?') ? base : `${base}?limit=1000`; + const data = await jget(url); + cache = normalizePayload(data); + lastLoadedAt = new Date(); + return cache; + } + + function all() { + return Array.isArray(cache) ? cache.slice() : []; + } + + function search(q) { + const term = String(q || '').trim().toLowerCase(); + if (!term) return all(); + return all().filter(p => + String(p.nombre || '').toLowerCase().includes(term) || + String(p.categoria || '').toLowerCase().includes(term) + ); + } + + function getById(id) { + return all().find(p => String(p.id) === String(id)) || null; + } + + // Renderización opcional si existe algún selector en la página + function renderSelect({ selectorList = ['#productos', '#selProducto', 'select[name="producto"]'] } = {}) { + const el = selectorList.map((s) => document.querySelector(s)).find(Boolean); + if (!el) return; // nada que renderizar + + const list = all(); + if (el.tagName === 'SELECT') { + el.innerHTML = ''; // limpiar + const opt0 = document.createElement('option'); + opt0.value = ''; opt0.textContent = '— Seleccionar producto —'; + el.appendChild(opt0); + + for (const p of list) { + const opt = document.createElement('option'); + opt.value = p.id; + opt.textContent = `${p.nombre} — ${formatPrecio(p.precio)}`; + el.appendChild(opt); + } + } else { + // contenedor genérico (lista) + el.innerHTML = ''; + const ul = document.createElement('ul'); + ul.className = 'lista-productos'; + for (const p of list) { + const li = document.createElement('li'); + li.textContent = `${p.nombre} — ${formatPrecio(p.precio)}`; + li.dataset.productId = p.id; + ul.appendChild(li); + } + el.appendChild(ul); + } + } + + function bindSearch({ inputSelectors = ['#buscarProducto', 'input[name="buscar_producto"]'], selectorList } = {}) { + const input = inputSelectors.map((s) => document.querySelector(s)).find(Boolean); + if (!input) return; + + input.addEventListener('input', () => { + const q = input.value; + const list = search(q); + // re-render mínimo para SELECTs + const el = selectorList?.map((s) => document.querySelector(s)).find(Boolean) + || document.querySelector('#selProducto') + || document.querySelector('#productos'); + if (!el) return; + + if (el.tagName === 'SELECT') { + el.innerHTML = ''; + const opt0 = document.createElement('option'); + opt0.value = ''; opt0.textContent = '— Seleccionar producto —'; + el.appendChild(opt0); + + for (const p of list) { + const opt = document.createElement('option'); + opt.value = p.id; + opt.textContent = `${p.nombre} — ${formatPrecio(p.precio)}`; + el.appendChild(opt); + } + } else { + el.innerHTML = ''; + const ul = document.createElement('ul'); + ul.className = 'lista-productos'; + for (const p of list) { + const li = document.createElement('li'); + li.textContent = `${p.nombre} — ${formatPrecio(p.precio)}`; + li.dataset.productId = p.id; + ul.appendChild(li); + } + el.appendChild(ul); + } + }); + } + + function formatPrecio(v) { + const n = Number(v || 0); + try { return n.toLocaleString(undefined, { style: 'currency', currency: 'UYU' }); } + catch { return `${n.toFixed(2)} UYU`; } + } + + return { + ensureLoaded, + all, + search, + getById, + renderSelect, + bindSearch, + get endpoint() { return discoveredEndpoint; }, + get lastLoadedAt() { return lastLoadedAt; }, + }; +})(); + +/* ======================= + * Init de página + * ======================= */ +async function initPage() { + // 1) Fijar esquema activo (lanza si no hay sesión/tenant) + await getActiveSchema(); + + // 2) Cargar catálogo de productos (descubrimiento + caché) + await productos.ensureLoaded().catch((e) => { + console.error('[productos.ensureLoaded] fallo:', e); + // No cortamos la init de la página; pero mostramos feedback + alert(e?.message || 'No fue posible cargar el catálogo de productos'); + }); + + // 3) Render opcional si existen selectores conocidos + productos.renderSelect({ selectorList: ['#selProducto', '#productos', 'select[name="producto"]'] }); + productos.bindSearch({ inputSelectors: ['#buscarProducto', 'input[name="buscar_producto"]'], selectorList: ['#selProducto', '#productos'] }); + + // 4) Hooks opcionales del código original (si existen) + if (window.nueva?.init) await window.nueva.init(); + if (window.estado?.bind) window.estado.bind(); + if (window.estado?.loadLista) await window.estado.loadLista(); +} + +/* ======================= + * Arranque + * ======================= */ +document.addEventListener('DOMContentLoaded', async () => { + try { + await initPage(); + } catch (err) { + console.error('Error:', err); + alert(err?.message || 'Error inicializando Comandas'); + } +}); + +/* ======================= + * Export helpers globales + * ======================= */ +Object.assign(window, { + jget, + jpost, + jput, + jdel, + getActiveSchema, + productos, +}); diff --git a/services/app/src/public/scripts/productos/productos.mjs b/services/app/src/public/scripts/productos/productos.mjs new file mode 100644 index 0000000..e69de29 diff --git a/services/app/src/views/comandas.ejs b/services/app/src/views/comandas.ejs deleted file mode 100644 index ae564e9..0000000 --- a/services/app/src/views/comandas.ejs +++ /dev/null @@ -1,558 +0,0 @@ - -
-

📋 Nueva Comanda

- /api/* -
- -
- -
-
-
- Productos -
0 ítems
-
-
-
-
- -
-
- -
-
- -
- -
Cargando…
-
-
-
-
- - -
-
-
Detalles
-
-
-
- - -
-
- - -
-
- -
- - -
- -
- La fecha se completa automáticamente y los estados/activos usan sus valores por defecto. -
-
-
- -
-
Carrito
-
-
Aún no agregaste productos.
-
-
-
Ítems: 0
-
Total: $ 0.00
-
- - -
-
- -
-
-
- - - diff --git a/services/app/src/views/estadoComandas.ejs b/services/app/src/views/estadoComandas.ejs deleted file mode 100644 index 2d3615b..0000000 --- a/services/app/src/views/estadoComandas.ejs +++ /dev/null @@ -1,532 +0,0 @@ - -
-

🧾 Estado de Comandas

- ➕ Nueva comanda -
- -
- -
-
-
- Listado -
- - -
-
-
-
-
- -
-
- -
-
- -
-
Cargando…
-
-
-
-
- - -
-
-
- Detalle - -
- -
-
Selecciona una comanda para ver el detalle.
-
- -
-
ID:
-
Mesa:
-
Total: $ 0.00
-
- - -
- -
-
-
-
-
-
- - - - - diff --git a/services/app/src/views/inicio.ejs b/services/app/src/views/inicio.ejs deleted file mode 100644 index 6901b5f..0000000 --- a/services/app/src/views/inicio.ejs +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - - - Inicio • SuiteCoffee - - - -
-
- <% - // Espera que el backend pase: { user, cookies, session } - const hasUser = typeof user !== 'undefined' && user; - const hasCookies = typeof cookies !== 'undefined' && cookies && Object.keys(cookies).length; - const hasSession = typeof session !== 'undefined' && session && Object.keys(session).length; - - const displayName = - (hasUser && (user.name || user.displayName || user.email)) || - (hasCookies && (cookies.user_name || cookies.displayName || cookies.email)) || - (hasSession && (session.user?.email || session.user?.name)) || - 'usuario'; - %> - -

Hola, <%= displayName %> 👋

-

Bienvenido a SuiteCoffee. Este es tu inicio y panel de diagnóstico de cookies/sesión.

- - <% if (hasUser) { %> -

Sesión de Aplicación (user)

- - - <% for (const [k,v] of Object.entries(user)) { %> - - - - - <% } %> - -
<%= k %><%= typeof v === 'object' ? JSON.stringify(v) : String(v) %>
- <% } %> - - <% if (hasSession) { %> -

Sesión Express (req.session)

- - - <% for (const [k,v] of Object.entries(session)) { %> - - - - - <% } %> - -
<%= k %><%= typeof v === 'object' ? JSON.stringify(v) : String(v) %>
- <% } %> - -
-
-

Cookies (servidor: req.cookies)

- <% if (hasCookies) { %> - - - - - - <% for (const [name, value] of Object.entries(cookies)) { %> - - - - - <% } %> - -
NombreValor
<%= name %><%= typeof value === 'object' ? JSON.stringify(value) : String(value) %>
- <% } else { %> -

- No se recibieron cookies del lado servidor (req.cookies). - Asegurate de usar cookie-parser y de pasar cookies al render: -
res.render('inicio_v2', { user: req.session.user, cookies: req.cookies, session: req.session }) -

- <% } %> -
- -
-

Cookies (navegador: document.cookie)

- - - - - - - -
NombreValor
Cargando…
-

- Total cookies en navegador: 0 -

-

Raw document.cookie:

- -
-
-
-
- - - - diff --git a/services/app/src/views/inicio.ejs.bak b/services/app/src/views/inicio.ejs.bak deleted file mode 100644 index 392e712..0000000 --- a/services/app/src/views/inicio.ejs.bak +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - Inicio • SuiteCoffee - - - -
-
- <% - const hasUser = typeof user !== 'undefined' && user; - const hasCookies = typeof cookies !== 'undefined' && cookies && Object.keys(cookies).length; - const displayName = - (hasUser && (user.name || user.displayName || user.email)) || - (hasCookies && (cookies.user_name || cookies.displayName || cookies.email)) || - 'usuario'; - %> -

Hola, <%= displayName %> 👋

-

Bienvenido a SuiteCoffee. Este es tu inicio.

- - <% if (hasUser) { %> -

Sesión

- - - <% for (const [k,v] of Object.entries(user)) { %> - - - - - <% } %> - -
<%= k %><%= typeof v === 'object' ? JSON.stringify(v) : String(v) %>
- <% } %> - -
-
-

Cookies (servidor)

- <% if (hasCookies) { %> - - - - - - <% for (const [name, value] of Object.entries(cookies)) { %> - - - - - <% } %> - -
NombreValor
<%= name %><%= typeof value === 'object' ? JSON.stringify(value) : String(value) %>
- <% } else { %> -

No se recibieron cookies desde el servidor (req.cookies). ¿Estás usando cookie-parser o pasando cookies al render?

- <% } %> -
- -
-

Cookies (navegador)

- - - - - - - -
NombreValor
Cargando…
-

Raw document.cookie:

- -
-
-
-
- - - - diff --git a/services/app/src/views/pages/comandas.ejs b/services/app/src/views/pages/comandas.ejs new file mode 100644 index 0000000..e69de29 diff --git a/services/app/src/views/compras.ejs b/services/app/src/views/pages/compras.ejs similarity index 100% rename from services/app/src/views/compras.ejs rename to services/app/src/views/pages/compras.ejs diff --git a/services/app/src/views/dashboard.ejs b/services/app/src/views/pages/dashboard.ejs similarity index 100% rename from services/app/src/views/dashboard.ejs rename to services/app/src/views/pages/dashboard.ejs diff --git a/services/app/src/views/pages/inicio.ejs b/services/app/src/views/pages/inicio.ejs new file mode 100644 index 0000000..4d44ba4 --- /dev/null +++ b/services/app/src/views/pages/inicio.ejs @@ -0,0 +1,366 @@ + + + + + + + + Inicio • SuiteCoffee + + +<% + // ============ SERVIDOR ============ // + // Espera: { user, cookies, session } (pásalos en res.render) + + const hasUser = typeof user !== 'undefined' && user; + const hasCookies = typeof cookies !== 'undefined' && cookies && Object.keys(cookies).length; + const hasSession = typeof session !== 'undefined' && session && Object.keys(session).length; + + const cookieKeys = hasCookies ? Object.keys(cookies) : []; + const sidKey = cookieKeys.find(k => /^(sc\.sid|connect\.sid|.*sid|.*sessionid)$/i.test(k)) || null; + const sidVal = sidKey ? String(cookies[sidKey] ?? '') : null; + const sidValMasked = sidVal ? (sidVal.length > 20 ? (sidVal.slice(0, 12) + '…' + sidVal.slice(-6)) : sidVal) : '(sin valor)'; + const sessionId = (hasSession && session.id) || (typeof sidVal === 'string' ? '(derivado de cookie)' : '(no disponible)'); + + const displayName = + (hasUser && (user.name || user.displayName || user.email)) || + (hasCookies && (cookies.user_name || cookies.displayName || cookies.email)) || + (hasSession && (session.user?.email || session.user?.name)) || + 'usuario'; + + // ---- Detección de cookie de configuración (servidor) + const tenantId32 = hasUser && user.default_tenant ? String(user.default_tenant).toLowerCase() : null; + const cfgRe = /^(?:sc\.)?(config|prefs|ui)(?:\.([0-9a-f]{32}))?$/i; + + function pickConfigCookieName(keys, tenant) { + const matches = keys + .map(k => [k, k.match(cfgRe)]) + .filter(([, m]) => !!m); + if (!matches.length) return null; + if (tenant) { + const exact = matches.find(([, m]) => (m[2] || '').toLowerCase() === tenant); + if (exact) return exact[0]; + } + return matches[0][0]; + } + + const configCookieKey = hasCookies ? pickConfigCookieName(cookieKeys, tenantId32) : null; + const rawConfigCookie = configCookieKey ? cookies[configCookieKey] : null; + + function tryParseConfig(val) { + if (!val) return null; + const candidates = []; + try { candidates.push(String(val)); } catch {} + try { candidates.push(decodeURIComponent(String(val))); } catch {} + try { candidates.push(Buffer.from(String(val), 'base64').toString('utf8')); } catch {} + + for (const c of candidates) { + try { + const obj = JSON.parse(c); + if (obj && typeof obj === 'object') return obj; + } catch (_) {} + } + return null; + } + + const configFromCookie = tryParseConfig(rawConfigCookie); + + // Tema inicial (si la cookie define theme: 'light' | 'dark') + const initialTheme = (configFromCookie && typeof configFromCookie.theme === 'string') + ? (configFromCookie.theme.toLowerCase() === 'light' ? 'light' : 'dark') + : 'dark'; + + // ====== Normalización de "user" para evitar duplicados ====== + const preferredOrder = ['sub','email','user_id','name','default_tenant','memberships']; + const normalizedUser = {}; + if (hasUser) { + // Tomamos valores canónicos + normalizedUser.sub = user.sub ?? null; + normalizedUser.email = user.email ?? null; + normalizedUser.user_id = user.user_id ?? user.userId ?? null; + normalizedUser.name = user.name ?? user.displayName ?? null; + + // Unificar current_tenant/currentTenant -> default_tenant si éste no viene + const fallbackTenant = user.current_tenant ?? user.currentTenant ?? null; + normalizedUser.default_tenant = user.default_tenant ?? fallbackTenant ?? null; + + if (Array.isArray(user.memberships)) normalizedUser.memberships = user.memberships; + + // Extras: todo lo demás excepto duplicados y legacy + const skip = new Set([ + ...preferredOrder, + 'current_tenant','currentTenant', + 'user_uid_nodash','userUidNoDash' + ]); + const extras = Object.entries(user) + .filter(([k]) => !skip.has(k) && !/nodash/i.test(k)); + // Los adjuntamos en un objeto aparte para mostrarlos (si existieran) + normalizedUser.__extras = Object.fromEntries(extras); + } +%> + +
+
+

Hola, <%= displayName %> 👋

+

Bienvenido a SuiteCoffee. Este inicio lee la cookie de configuraciones actuales (servidor y navegador) y aplica el tema.

+ + +

Diagnóstico de Sesión

+ + + + + + + + + + + +
req.sessionID<%= typeof req !== 'undefined' && req.sessionID ? req.sessionID : sessionId %>
Cookie de sesión + <% if (sidKey) { %> + <%= sidKey %> +
valor: <%= sidValMasked %>
+ <% } else { %> + No detectada en req.cookies (ej. sc.sid). + <% } %> +
+ + <% if (hasUser) { %> +

Sesión de Aplicación (user)

+ + + <% for (const key of preferredOrder) { + const v = normalizedUser[key]; + if (typeof v === 'undefined' || v === null) continue; + %> + + + + + <% } %> + +
<%= key %><%= typeof v === 'object' ? JSON.stringify(v) : String(v) %>
+ + <% if (normalizedUser.__extras && Object.keys(normalizedUser.__extras).length) { %> +

Otros campos

+ + + + <% for (const [k,v] of Object.entries(normalizedUser.__extras)) { %> + + + + + <% } %> + +
CampoValor
<%= k %><%= typeof v === 'object' ? JSON.stringify(v) : String(v) %>
+ <% } %> + <% } %> + + +

Configuraciones actuales (cookie servidor)

+ + + + + + + + + + + + + + + +
Cookie detectada + <% if (configCookieKey) { %> + <%= configCookieKey %> + <% if (tenantId32) { %> + tenant=<%= tenantId32 %> + <% } %> + <% } else { %> + No se encontró cookie de configuración (busco: sc.config, sc.prefs, sc.ui o con sufijo .{tenantId32}). + <% } %> +
Tema aplicado<%= initialTheme %>
Contenido parseado + <% if (configFromCookie) { %> +
<%= JSON.stringify(configFromCookie, null, 2) %>
+ <% } else if (configCookieKey) { %> + No fue posible parsear JSON. Valor crudo: +
<%= String(rawConfigCookie).slice(0, 2000) %>
+ <% } else { %> + + <% } %> +
+ +
+
+

Cookies (servidor: req.cookies)

+ <% if (hasCookies) { %> + + + + + + <% for (const [name, value] of Object.entries(cookies)) { %> + + + + + <% } %> + +
NombreValor
<%= name %><%= typeof value === 'object' ? JSON.stringify(value) : String(value) %>
+ <% } else { %> +

+ No se recibieron cookies del lado servidor (req.cookies). + Asegurate de usar cookie-parser y de pasar cookies al render: +
res.render('inicio', { user: req.session.user, cookies: req.cookies, session: req.session }) +

+ <% } %> +
+ +
+

Cookies (navegador) + Config

+ + + + + + + +
NombreValor
Cargando…
+ +

+ Total cookies en navegador: 0 +

+ +

Config detectada (navegador)

+

Nombre: (buscando…)

+
(sin config)
+
+
+
+
+ + + + + + + diff --git a/services/app/src/views/login.ejs b/services/app/src/views/pages/login.ejs similarity index 100% rename from services/app/src/views/login.ejs rename to services/app/src/views/pages/login.ejs diff --git a/services/app/src/views/productos.ejs b/services/app/src/views/pages/productos.ejs similarity index 100% rename from services/app/src/views/productos.ejs rename to services/app/src/views/pages/productos.ejs diff --git a/services/app/src/views/reportes.ejs b/services/app/src/views/pages/reportes.ejs similarity index 100% rename from services/app/src/views/reportes.ejs rename to services/app/src/views/pages/reportes.ejs diff --git a/services/app/src/views/reportes.ejs.bak b/services/app/src/views/pages/reportes.ejs.bak similarity index 100% rename from services/app/src/views/reportes.ejs.bak rename to services/app/src/views/pages/reportes.ejs.bak diff --git a/services/app/src/views/usuarios.ejs b/services/app/src/views/pages/usuarios.ejs similarity index 100% rename from services/app/src/views/usuarios.ejs rename to services/app/src/views/pages/usuarios.ejs diff --git a/services/app/src/views/routes.js b/services/app/src/views/routes.js new file mode 100644 index 0000000..a895fb9 --- /dev/null +++ b/services/app/src/views/routes.js @@ -0,0 +1,84 @@ +// services/app/src/views/routes.js + +import { Router } from 'express'; +import { requireAuth } from '@suitecoffee/middlewares'; + +const SESSION_NAME = process.env.SESSION_NAME; + +const router = Router(); + + +// ---------------------------------------------------------- +// Rutas de UI +// ---------------------------------------------------------- + +router.get('/', async (req, res) => { + // combinamos cookies comunes + firmadas (signed se “desfirma”: queda el SID limpio) + res.locals.pageTitle = "Inicio2"; // Título de pestaña + res.locals.pageId = "inicio"; // Sidebar contextual + const mergedCookies = { ...(req.cookies || {}), ...(req.signedCookies || {}) }; + + res.render('inicio', { + user: req.session?.user ?? null, + session: req.session ?? {}, + cookies: mergedCookies, // <-- lo que la vista va a leer + cookieName: SESSION_NAME, // <-- para no hardcodear 'sc.sid' + }); +}); +/* +router.get('/comandas', (req, res) => { + res.locals.pageTitle = "Comandas"; + res.locals.pageId = "comandas"; + res.render("./pages/comandas"); + // res.json({ ok: true, route: '/comandas' }); +}); +*/ + +// ----------------------------------------------------------------------------- +// Rutas de testeo de Cookies +// ----------------------------------------------------------------------------- +/* +router.get('/inicio', requireAuth(), async (req, res) => { + res.locals.pageTitle = "Inicio2"; // Título de pestaña + res.locals.pageId = "inicio"; // Sidebar contextual + res.render("inicio", { + user: req.session?.user ?? null, + cookies: req.cookies ?? {}, + session: req.session ?? {}, + req, // para que el EJS pueda usar req.sessionID si quiere + }); +}); +*/ + +router.get('/inicio', requireAuth(), async (req, res) => { + // combinamos cookies comunes + firmadas (signed se “desfirma”: queda el SID limpio) + res.locals.pageTitle = "Inicio2"; // Título de pestaña + res.locals.pageId = "inicio"; // Sidebar contextual + const mergedCookies = { ...(req.cookies || {}), ...(req.signedCookies || {}) }; + + res.render('inicio', { + user: req.session?.user ?? null, + session: req.session ?? {}, + cookies: mergedCookies, // <-- lo que la vista va a leer + cookieName: SESSION_NAME, // <-- para no hardcodear 'sc.sid' + }); +}); + +router.get('/debug/tenant', (req, res) => { + res.json({ + sid: req.sessionID ?? null, + hasSession: !!req.session, + user: req.session?.user + ? { + sub: req.session.user.sub ?? null, + email: req.session.user.email ?? null, + default_tenant: req.session.user.default_tenant ?? req.session.user.defaultTenant ?? null, + } + : null, + tenant: req.session?.tenant ?? null, + reqTenant: req.tenant ?? null, + accept: req.headers.accept, + }); +}); + +export default router; \ No newline at end of file diff --git a/services/auth/.env.development b/services/auth/.env.development index 79f86e5..23c0059 100644 --- a/services/auth/.env.development +++ b/services/auth/.env.development @@ -1,62 +1,54 @@ -# ===== Runtime ===== +# ======================================================= +# Runtime NODE_ENV=development PORT=4040 +APP_BASE_URL=https://dev.suitecoffee.uy +# ======================================================= -# ===== Session (usa el Redis del stack) ===== -# Para DEV podemos reutilizar el Redis de Authentik. En prod conviene uno separado. -SESSION_SECRET=Neon*Mammal*Boaster*Ludicrous*Fender8*Crablike -SESSION_COOKIE_NAME=sc.sid - -# ===== DB principal (metadatos de SuiteCoffee) ===== -# Usa el alias de red del servicio 'db' (compose: aliases [dev-db]) -DB_HOST=dev-db -DB_NAME=dev_suitecoffee_core -DB_PORT=5432 -DB_USER=dev-user-suitecoffee -DB_PASS=dev-pass-suitecoffee +# ======================================================= +# Configuración de Dases de Datos CORE_DB_HOST=dev-db CORE_DB_NAME=dev_suitecoffee_core CORE_DB_PORT=5432 CORE_DB_USER=dev-user-suitecoffee CORE_DB_PASS=dev-pass-suitecoffee -# ===== DB tenants (Tenants de SuiteCoffee) ===== -TENANTS_HOST=dev-tenants -TENANTS_DB=dev_suitecoffee_tenants -TENANTS_PORT=5432 -TENANTS_USER=suitecoffee -TENANTS_PASS=suitecoffee - TENANTS_DB_HOST=dev-tenants TENANTS_DB_NAME=dev_suitecoffee_tenants TENANTS_DB_PORT=5432 TENANTS_DB_USER=suitecoffee TENANTS_DB_PASS=suitecoffee +# ======================================================= -# ===== Authentik — Admin API (server-to-server dentro de la red) ===== -# Usa el alias de red del servicio 'authentik' y su puerto interno 9000 + +# ======================================================= +# Sesiones +SESSION_SECRET=Neon*Mammal*Boaster*Ludicrous*Fender8*Crablike +SESSION_NAME=sc.sid +COOKIE_DOMAIN=dev.suitecoffee.uy +# ======================================================= + + + +# ======================================================= +# Authentik y OIDC AK_TOKEN=h2apVHbd3ApMcnnSwfQPXbvximkvP8HnUE25ot3zXWuEEtJFaNCcOzDHB6Xw AK_REDIS_URL=redis://ak-redis:6379 -# ===== OIDC (DEBE coincidir con el Provider) ===== -# DEV (todo dentro de la red de Docker): -# - El auth service redirige al navegador a este issuer. Si NO tenés reverse proxy hacia Authentik, -# esta URL interna NO será accesible desde el navegador del host. En ese caso, ver nota más abajo. +OIDC_LOGIN_URL=https://dev.sso.suitecoffee.uy +OIDC_REDIRECT_URI=https://dev.suitecoffee.uy/auth/callback -APP_BASE_URL=https://suitecoffee.uy +OIDC_CLIENT_ID=1orMM8vOvf3WkN2FejXYvUFpPtONG0Lx1eMlwIpW +OIDC_CLIENT_SECRET=t5wx13qBcM0EFQ3cGnUIAmLzvbdsQrUVPv1OGWjszWkEp35pJQ55t7vZeeShqG49kuRAaiXv6PSGJLhRfGaponGaJl8gH1uCL7KIxdmm7UihgYoAXB2dFhZV4zRxfze2 -OIDC_LOGIN_URL=https://sso.suitecoffee.uy -OIDC_REDIRECT_URI = https://suitecoffee.uy/auth/callback +OIDC_CONFIG_URL=https://dev.sso.suitecoffee.uy/application/o/suitecoffee/.well-known/openid-configuration +OIDC_AUTHORIZE_URL=https://dev.sso.suitecoffee.uy/application/o/authorize/ +OIDC_TOKEN_URL=https://dev.sso.suitecoffee.uy/application/o/token/ +OIDC_USERINFO_URL=https://dev.sso.suitecoffee.uy/application/o/userinfo/ +OIDC_LOGOUT_URL=https://dev.sso.suitecoffee.uy/application/o/suitecoffee/end-session/ +OIDC_JWKS_URL=https://dev.sso.suitecoffee.uy/application/o/suitecoffee/jwks/ -OIDC_CLIEN_ID=1orMM8vOvf3WkN2FejXYvUFpPtONG0Lx1eMlwIpW -OIDC_CONFIG_URL=https://sso.suitecoffee.uy/application/o/suitecoffee/.well-known/openid-configuration -OIDC_ISSUER=https://sso.suitecoffee.uy/application/o/suitecoffee/ -OIDC_ISSUER_DISCOVERY=https://sso.suitecoffee.uy/application/o/suitecoffee/.well-known/openid-configuration -OIDC_AUTHORIZE_URL=https://sso.suitecoffee.uy/application/o/authorize/ -OIDC_TOKEN_URL=https://sso.suitecoffee.uy/application/o/token/ -OIDC_USERINFO_URL=https://sso.suitecoffee.uy/application/o/userinfo/ -OIDC_LOGOUT_URL=https://sso.suitecoffee.uy/application/o/suitecoffee/end-session/ -OIDC_JWKS_URL=https://sso.suitecoffee.uy/application/o/suitecoffee/jwks/ \ No newline at end of file +# ======================================================= \ No newline at end of file diff --git a/services/auth/.env.production b/services/auth/.env.production deleted file mode 100644 index 709cbf7..0000000 --- a/services/auth/.env.production +++ /dev/null @@ -1,22 +0,0 @@ -NODE_ENV=production # Entorno de desarrollo - -PORT=4000 # Variables del servicio -> suitecoffee-app - -# AUTH_HOST=prod-auth - -DB_HOST=prod-db -# Nombre de la base de datos -DB_NAME=suitecoffee - -# Usuario y contraseña -DB_USER=suitecoffee -DB_PASS=suitecoffee - -# Puertos del servicio de db -DB_LOCAL_PORT=5432 -DB_DOCKER_PORT=5432 - -# Colores personalizados -COL_PRI=452D19 # Marrón oscuro -COL_SEC=D7A666 # Crema / Café -COL_BG=FFA500 # Naranja \ No newline at end of file diff --git a/services/auth/package-lock.json b/services/auth/package-lock.json index cb73938..f6bc50c 100644 --- a/services/auth/package-lock.json +++ b/services/auth/package-lock.json @@ -9,6 +9,11 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@suitecoffee/db": "file:../../packages/core/db", + "@suitecoffee/middlewares": "file:../../packages/core/middlewares", + "@suitecoffee/oidc": "file:../../packages/oidc", + "@suitecoffee/redis": "file:../../packages/core/redis", + "@suitecoffee/scripts": "file:../../packages/core/scripts", "axios": "^1.11.0", "bcrypt": "^5.1.1", "chalk": "^5.6.0", @@ -37,6 +42,35 @@ "nodemon": "^3.1.10" } }, + "../../packages/core/db": { + "name": "@suitecoffee/db", + "version": "1.0.0", + "peerDependencies": { + "pg": "^8.16.3" + } + }, + "../../packages/core/middlewares": { + "name": "@suitecoffee/middlewares", + "version": "1.0.0" + }, + "../../packages/core/redis": { + "name": "@suitecoffee/redis", + "version": "1.0.0", + "peerDependencies": { + "pg": "^8.16.3" + } + }, + "../../packages/core/scripts": { + "name": "@suitecoffee/scripts", + "version": "1.0.0" + }, + "../../packages/oidc": { + "name": "@suitecoffee/oidc", + "version": "1.0.0", + "dependencies": { + "openid-client": "^6.0.0" + } + }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "dev": true, @@ -132,6 +166,26 @@ "@redis/client": "^5.8.2" } }, + "node_modules/@suitecoffee/db": { + "resolved": "../../packages/core/db", + "link": true + }, + "node_modules/@suitecoffee/middlewares": { + "resolved": "../../packages/core/middlewares", + "link": true + }, + "node_modules/@suitecoffee/oidc": { + "resolved": "../../packages/oidc", + "link": true + }, + "node_modules/@suitecoffee/redis": { + "resolved": "../../packages/core/redis", + "link": true + }, + "node_modules/@suitecoffee/scripts": { + "resolved": "../../packages/core/scripts", + "link": true + }, "node_modules/@types/body-parser": { "version": "1.19.6", "license": "MIT", @@ -473,6 +527,8 @@ }, "node_modules/connect-redis": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-9.0.0.tgz", + "integrity": "sha512-QwzyvUePTMvEzG1hy45gZYw3X3YHrjmEdSkayURlcZft7hqadQ3X39wYkmCqblK2rGlw+XItELYt6GnyG6DEIQ==", "license": "MIT", "engines": { "node": ">=18" @@ -825,6 +881,8 @@ }, "node_modules/express-session": { "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", "license": "MIT", "dependencies": { "cookie": "0.7.2", @@ -1770,6 +1828,8 @@ }, "node_modules/openid-client": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", "license": "MIT", "dependencies": { "jose": "^4.15.9", diff --git a/services/auth/package.json b/services/auth/package.json index add584b..69b1dba 100644 --- a/services/auth/package.json +++ b/services/auth/package.json @@ -15,6 +15,13 @@ "nodemon": "^3.1.10" }, "dependencies": { + + "@suitecoffee/db": "file:../../packages/core/db", + "@suitecoffee/middlewares": "file:../../packages/core/middlewares", + "@suitecoffee/oidc": "file:../../packages/oidc", + "@suitecoffee/redis": "file:../../packages/core/redis", + "@suitecoffee/scripts": "file:../../packages/core/scripts", + "axios": "^1.11.0", "bcrypt": "^5.1.1", "chalk": "^5.6.0", @@ -38,11 +45,6 @@ "pg-format": "^1.0.4", "redis": "^5.8.2" }, - "imports": { - "#v1Router": "./src/api/v1/routes/routes.js", - "#pages": "./src/pages/pages.js", - "#db": "./src/db/poolSingleton.js" - }, "keywords": [], "description": "" } diff --git a/services/auth/src/ak.js b/services/auth/src/ak.js deleted file mode 100644 index e3b113e..0000000 --- a/services/auth/src/ak.js +++ /dev/null @@ -1,436 +0,0 @@ -// services/auth/src/ak.js -// ------------------------------------------------------------ -// Cliente para la API Admin de Authentik (v3) -// - Sin dependencias externas (usa fetch nativo de Node >=18) -// - ESM compatible -// - Timeouts, reintentos opcionales y mensajes de error claros -// - Compatible con services/auth/src/index.js actual -// ------------------------------------------------------------ - -// Utiliza AUTHENTIK_BASE_URL y AUTHENTIK_TOKEN para validar y devuelve la configuración (base URL y token) desde variables de entorno. -// Devuelve la URL base y el Token que se leyó desde .env - -/** - * @typedef {Object} AkCfg - * @property {string} BASE // p.ej. "https://idp.example.com" - * @property {string} TOKEN // bearer - */ - -/** - * @typedef {Object} AkOpts - * @property {Record>} [qs] - * @property {any} [body] - * @property {number} [timeoutMs=10000] - * @property {number} [retries=0] - * @property {Record} [headers] - */ - -function getConfig() { - const BASE = (process.env.AUTHENTIK_BASE_URL || "").trim().replace(/\/+$/, ""); - const TOKEN = process.env.AUTHENTIK_TOKEN || ''; - if (!BASE) throw new Error('[AK_CONFIG] Falta variable AUTHENTIK_BASE_URL'); - if (!TOKEN) throw new Error('[AK_CONFIG] Falta variable AUTHENTIK_TOKEN'); - return { BASE, TOKEN }; -} - -// ------------------------------------------------------------ -// Utilidades -// ------------------------------------------------------------ - -// Espera -const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); - - -// ------------------------------------------------------------ -// Helpers de sincronización -// ------------------------------------------------------------ - -export function createAkClient(cfg = getConfig()) { - return { - request: (method, path, opts = {}) => request(method, path, opts, cfg), - akGET: (path, opts) => request("GET", path, opts, cfg), - akPOST: (path, opts) => request("POST", path, opts, cfg), - akPUT: (path, opts) => request("PUT", path, opts, cfg), - akPATCH: (path, opts) => request("PATCH", path, opts, cfg), - akDELETE:(path, opts) => request("DELETE", path, opts, cfg), - }; -} - -// Listar grupos con búsqueda por nombre/slug -export async function akListGroups(search = "") { - const { akGET } = createAkClient(); - const term = String(search ?? "").trim(); - - const data = await akGET("/core/groups/", { - qs: term ? { search: term } : undefined, - }); - - if (Array.isArray(data)) return data; - if (data && Array.isArray(data.results)) return data.results; - return []; -} - -export async function akPatchUserAttributes(userPk, partialAttrs = {}) { - - const id = String(userPk ?? "").trim(); - if (!id) throw new TypeError("[AK_PATH_USER_ATRIBUTES] atribute `userPk` is required"); - - if (partialAttrs == null || typeof partialAttrs !== "object" || Array.isArray(partialAttrs)) { - throw new TypeError("[AK_PATH_USER_ATRIBUTES] atribute `partialAttrs` must be a plain object"); - } - - // Remove undefineds to avoid unintentionally nulling keys server-side - const cleaned = Object.fromEntries( - Object.entries(partialAttrs).filter(([, v]) => v !== undefined) - ); - - if (Object.keys(cleaned).length === 0) { - throw new TypeError("[AK_PATH_USER_ATRIBUTES] atribute `partialAttrs` is required"); - } - - // NOTE: pass path WITHOUT /api/v3; the client prefixes it - return akPATCH(`/core/users/${encodeURIComponent(id)}/`, { - body: { attributes: cleaned }, - }); -} - -export async function akEnsureGroupForTenant(tenantHex) { - const { akGET, akPOST } = createAkClient(); - - const hex = String(tenantHex ?? "").trim(); - if (!hex) throw new TypeError("akEnsureGroupForTenant: `tenantHex` is required"); - - const groupName = `tenant_${hex}`; - - // 1) Buscar existente (normaliza {results:[]}/[]) - const data = await akGET("/core/groups/", { qs: { search: groupName } }); - const list = Array.isArray(data) ? data : (Array.isArray(data?.results) ? data.results : []); - const existing = list.find(g => g?.name === groupName); - if (existing?.pk ?? existing?.id) return existing.pk ?? existing.id; - - // 2) Crear si no existe - try { - const created = await akPOST("/core/groups/", { - body: { name: groupName, attributes: { tenant_uuid: hex } }, - }); - return created?.pk ?? created?.id; - } catch (e) { - // 3) Condición de carrera (otro proceso lo creó): reconsulta y devuelve - const msg = String(e?.message || ""); - if (/already exists|unique|duplicate|409/i.test(msg)) { - const data2 = await akGET("/core/groups/", { qs: { search: groupName } }); - const list2 = Array.isArray(data2) ? data2 : (Array.isArray(data2?.results) ? data2.results : []); - const found = list2.find(g => g?.name === groupName); - if (found?.pk ?? found?.id) return found.pk ?? found.id; - } - throw e; - } -} - -export async function akAddUserToGroup(userPk, groupPk) { - const { akPOST } = createAkClient(); - - const user = String(userPk ?? "").trim(); - const group = String(groupPk ?? "").trim(); - if (!user) throw new TypeError("akAddUserToGroup: `userPk` is required"); - if (!group) throw new TypeError("akAddUserToGroup: `groupPk` is required"); - - // API reciente: POST /core/users//groups/ { group: } - const path = `/core/users/${encodeURIComponent(user)}/groups/`; - - try { - return await akPOST(path, { body: { group } }); - } catch (e) { - const msg = String(e?.message || ""); - // Si ya es miembro, tratamos como éxito idempotente - if (/already.*member|exists|duplicate|409/i.test(msg)) { - return { ok: true, alreadyMember: true, userPk: user, groupPk: group }; - } - // Fallback para instancias viejas: /core/group_memberships/ { user, group } - if (/404|not\s*found/i.test(msg)) { - return await akPOST("/core/group_memberships/", { body: { user, group } }); - } - throw e; - } -} - - -/** - * Llamada HTTP genérica con fetch + timeout + manejo de errores. - * @param {'GET'|'POST'|'PUT'|'PATCH'|'DELETE'} method - * @param {string} path Ruta relativa (ej. "/core/users/") o absoluta; si es relativa se antepone "/api/v3". - * @param {AkOpts} [opts] - * @param {AkCfg} [cfg] Config inyectada; si se omite se usa getConfig() - * @returns {Promise} - */ - -export async function request(method, path, opts = {}, cfg) { - const { BASE, TOKEN } = cfg ?? getConfig(); - const { - qs, - body, - timeoutMs = 10_000, - retries = 0, - headers = {}, - } = opts; - - // Construcción segura de URL + QS - const base = BASE.endsWith("/") ? BASE : `${BASE}/`; - let p = /^https?:\/\//i.test(path) ? path : (path.startsWith("/") ? path : `/${path}`); - if (!/^https?:\/\//i.test(p) && !p.startsWith("/api/")) p = `/api/v3${p}`; - const url = new URL(p, base); - if (qs && typeof qs === "object") { - for (const [k, v] of Object.entries(qs)) { - if (v == null) continue; - if (Array.isArray(v)) v.forEach((x) => url.searchParams.append(k, String(x))); - else url.searchParams.set(k, String(v)); - } - } - - // Reintentos + timeout - const maxAttempts = Math.max(1, retries + 1); - let lastErr; - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - const ctrl = new AbortController(); - const t = setTimeout(() => ctrl.abort(new Error("AK_TIMEOUT")), timeoutMs); - try { - const init = { - method, - signal: ctrl.signal, - headers: { - Authorization: `Bearer ${TOKEN}`, - Accept: "application/json", - ...headers, - }, - }; - if (body !== undefined) { - // Sólo forzar JSON si es objeto plano - const isPlainObj = body && typeof body === "object" && - !(body instanceof FormData) && - !(body instanceof URLSearchParams) && - !(typeof Blob !== "undefined" && body instanceof Blob); - if (isPlainObj) { - init.headers["Content-Type"] = init.headers["Content-Type"] || "application/json"; - init.body = JSON.stringify(body); - } else { - init.body = body; // deja que fetch maneje el Content-Type - } - } - - const res = await fetch(url, init); - clearTimeout(t); - - if (res.status === 204 || res.status === 205) return null; - const ctype = res.headers.get("content-type") || ""; - const payload = /\bapplication\/json\b/i.test(ctype) ? await res.json().catch(() => ({})) : await res.text(); - - if (!res.ok) { - const detail = typeof payload === "string" ? payload : payload?.detail || payload?.error || JSON.stringify(payload); - const err = new Error(`AK ${method} ${url.pathname}${url.search} → ${res.status}: ${detail}`); - err.status = res.status; // @ts-ignore - // Reintenta 5xx y 429 - if ((res.status >= 500 && res.status <= 599) || res.status === 429) { - lastErr = err; - if (attempt < maxAttempts) { - let delay = 500 * 2 ** (attempt - 1); - const ra = parseInt(res.headers.get("retry-after") || "", 10); - if (!Number.isNaN(ra)) delay = Math.max(delay, ra * 1000); - await new Promise(r => setTimeout(r, delay)); - continue; - } - } - throw err; - } - - return payload; - } catch (e) { - clearTimeout(t); - lastErr = e; - const msg = String(e?.message || ""); - const retriable = msg.includes("AK_TIMEOUT") || msg.includes("ECONNREFUSED") || msg.includes("fetch failed"); - if (!retriable || attempt >= maxAttempts) throw e; - await new Promise(r => setTimeout(r, 500 * 2 ** (attempt - 1))); - } - } - throw lastErr; -} - -// ------------------------------------------------------------ -// Funciones públicas -// ------------------------------------------------------------ - -export async function akFindUserByEmail(email) { - const { akGET } = createAkClient(); - - const needle = String(email ?? "").trim().toLowerCase(); - if (!needle) throw new TypeError("akFindUserByEmail: `email` is required"); - - const PAGE_SIZE = 100; - let page = 1; - const MAX_PAGES = 10; - - while (page <= MAX_PAGES) { - const data = await akGET("/core/users/", { - qs: { search: needle, page_size: PAGE_SIZE, page }, - retries: 2, - }); - - const list = Array.isArray(data) - ? data - : (Array.isArray(data?.results) ? data.results : []); - - const found = list.find(u => String(u?.email || "").toLowerCase() === needle); - if (found) return found || null; - - // Continuar paginando sólo si hay más resultados - const hasNext = - Array.isArray(data) - ? list.length === PAGE_SIZE // array plano: inferimos por tamaño - : Boolean(data?.next); // DRF: link "next" - if (!hasNext) break; - - page += 1; - } - - return null; -} - -export async function akCreateUser(p = {}) { - const { akPOST } = createAkClient(); - - const email = String(p.email ?? "").trim().toLowerCase(); - if (!email) throw new TypeError("akCreateUser: `email` is required"); - - const name = String(p.displayName ?? email).trim() || email; - const tenantUuid = String(p.tenantUuid ?? "").replace(/-/g, "").trim(); - const isActive = p.isActive ?? true; - - const body = { - username: email, - name, - email, - is_active: !!isActive, - attributes: tenantUuid ? { tenant_uuid: tenantUuid } : {}, - }; - - let user; - try { - user = await akPOST("/core/users/", { body, retries: 2 }); - } catch (e) { - const msg = String(e?.message || ""); - if (/409|already\s*exists|unique|duplicate/i.test(msg)) { - // Idempotencia: si ya existe, lo buscamos por email y lo devolvemos - const existing = await akFindUserByEmail(email); - if (existing) return existing; - } - throw e; - } - - // Agregar a grupo (opcional, no rompe el flujo si falla) - const groupId = p.addToGroupId != null ? String(p.addToGroupId).trim() : ""; - if (groupId) { - try { - const userPk = encodeURIComponent(user.pk ?? user.id); - await akPOST(`/core/users/${userPk}/groups/`, { - body: { group: groupId }, - retries: 2, - }); - } catch (err) { - console.warn( - `akCreateUser: could not add user ${user.pk ?? user.id} to group ${groupId}:`, - err?.message || err - ); - } - } - - return user; -} - -export async function akSetPassword(userPk, password, requireChange = true) { - const { akPOST } = createAkClient(); - - const id = String(userPk ?? "").trim(); - if (!id) throw new TypeError("akSetPassword: `userPk` is required"); - - const pwd = String(password ?? ""); - if (!pwd) throw new TypeError("akSetPassword: `password` is required"); - - try { - await akPOST(`/core/users/${encodeURIComponent(id)}/set_password/`, { - body: { password: pwd, require_change: !!requireChange }, - retries: 1, - timeoutMs: 15_000, - }); - return true; - } catch (e) { - const status = e?.status ? `HTTP ${e.status}: ` : ""; - const err = new Error(`akSetPassword: failed to set password (${status}${e?.message || e})`); - err.cause = e; - throw err; - } -} - -export async function akResolveGroupIdByName(name) { - const term = String(name ?? "").trim(); - if (!term) throw new TypeError("akResolveGroupIdByName: `name` is required"); - - const needle = term.toLowerCase(); - const groups = await akListGroups(term); - if (!Array.isArray(groups) || groups.length === 0) return null; - - // Prefer exact slug match, then exact name match - const bySlug = groups.find(g => String(g?.slug ?? "").toLowerCase() === needle); - if (bySlug) return bySlug.pk ?? bySlug.id ?? null; - - const byName = groups.find(g => String(g?.name ?? "").toLowerCase() === needle); - return byName?.pk ?? byName?.id ?? null; -} - -export async function akResolveGroupId({ id, pk, uuid, name, slug } = {}) { - const toPk = (v) => { - if (v == null || v === "") return null; - const n = Number(v); - return Number.isFinite(n) ? n : String(v); - }; - - // 1) Direct pk/id - const direct = pk ?? id; - const directPk = toPk(direct); - if (directPk != null) return directPk; - - const { akGET } = createAkClient(); - - // 2) By UUID (detail endpoint) - const uuidStr = String(uuid ?? "").trim(); - if (uuidStr) { - try { - const g = await akGET(`/core/groups/${encodeURIComponent(uuidStr)}/`, { retries: 1 }); - const fromDetail = toPk(g?.pk ?? g?.id); - if (fromDetail != null) return fromDetail; - } catch { /* continue with name/slug */ } - } - - // 3) By exact name/slug - const needle = String(name ?? slug ?? "").trim(); - if (needle) { - const lower = needle.toLowerCase(); - const list = await akListGroups(needle); // expects [] or {results:[]}, handled in akListGroups - const found = - list.find(g => String(g?.slug ?? "").toLowerCase() === lower) || - list.find(g => String(g?.name ?? "").toLowerCase() === lower); - const fromList = toPk(found?.pk ?? found?.id); - if (fromList != null) return fromList; - } - - return null; -} - -// ------------------------------------------------------------ -// Exportación de constantes -// ------------------------------------------------------------ - -export const akGET = (path, opts) => request("GET", path, opts); -export const akPOST = (path, opts) => request("POST", path, opts); -export const akPUT = (path, opts) => request("PUT", path, opts); -export const akPATCH = (path, opts) => request("PATCH", path, opts); -export const akDELETE = (path, opts) => request("DELETE", path, opts); \ No newline at end of file diff --git a/services/auth/src/api/api.js b/services/auth/src/api/api.js deleted file mode 100644 index d65d8b8..0000000 --- a/services/auth/src/api/api.js +++ /dev/null @@ -1,181 +0,0 @@ -// // ---------------------------------------------------------- -// // API -// // ---------------------------------------------------------- -// app.get('/api/tables', async (_req, res) => { -// res.json(ALLOWED_TABLES); -// }); - -// app.get('/api/schema/:table', async (req, res) => { -// try { -// const table = ensureTable(req.params.table); -// const client = await getClient(); -// try { -// const columns = await loadColumns(client, table); -// const fks = await loadForeignKeys(client, table); -// const enriched = columns.map(c => ({ ...c, foreign: fks[c.column_name] || null })); -// res.json({ table, columns: enriched }); -// } finally { client.release(); } -// } catch (e) { -// res.status(400).json({ error: e.message }); -// } -// }); - -// app.get('/api/options/:table/:column', async (req, res) => { -// try { -// const table = ensureTable(req.params.table); -// const column = req.params.column; -// if (!VALID_IDENT.test(column)) throw new Error('Columna inválida'); - -// const client = await getClient(); -// try { -// const fks = await loadForeignKeys(client, table); -// const fk = fks[column]; -// if (!fk) return res.json([]); - -// const refTable = fk.foreign_table; -// const refId = fk.foreign_column; -// const labelCol = await pickLabelColumn(client, refTable); - -// const sql = `SELECT ${q(refId)} AS id, ${q(labelCol)} AS label FROM ${q(refTable)} ORDER BY ${q(labelCol)} LIMIT 1000`; -// const result = await client.query(sql); -// res.json(result.rows); -// } finally { client.release(); } -// } catch (e) { -// res.status(400).json({ error: e.message }); -// } -// }); - -// app.get('/api/table/:table', async (req, res) => { -// try { -// const table = ensureTable(req.params.table); -// const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000); -// const client = await getClient(); -// try { -// const pks = await loadPrimaryKey(client, table); -// const orderBy = pks.length ? `ORDER BY ${pks.map(q).join(', ')} DESC` : ''; -// const sql = `SELECT * FROM ${q(table)} ${orderBy} LIMIT ${limit}`; -// const result = await client.query(sql); - -// // Normalizar: siempre devolver objetos {col: valor} -// const colNames = result.fields.map(f => f.name); -// let rows = result.rows; -// if (rows.length && Array.isArray(rows[0])) { -// rows = rows.map(r => Object.fromEntries(r.map((v, i) => [colNames[i], v]))); -// } -// res.json(rows); -// } finally { client.release(); } -// } catch (e) { -// res.status(400).json({ error: e.message, code: e.code, detail: e.detail }); -// } -// }); - -// app.post('/api/table/:table', async (req, res) => { -// const table = ensureTable(req.params.table); -// const payload = req.body || {}; -// try { -// const client = await getClient(); -// try { -// const columns = await loadColumns(client, table); -// const insertable = columns.filter(c => -// !c.is_primary && !c.is_identity && !(c.column_default || '').startsWith('nextval(') -// ); -// const allowedCols = new Set(insertable.map(c => c.column_name)); - -// const cols = []; -// const vals = []; -// const params = []; -// let idx = 1; -// for (const [k, v] of Object.entries(payload)) { -// if (!allowedCols.has(k)) continue; -// if (!VALID_IDENT.test(k)) continue; -// cols.push(q(k)); -// vals.push(`$${idx++}`); -// params.push(v); -// } - -// if (!cols.length) { -// const { rows } = await client.query(`INSERT INTO ${q(table)} DEFAULT VALUES RETURNING *`); -// res.status(201).json({ inserted: rows[0] }); -// } else { -// const { rows } = await client.query( -// `INSERT INTO ${q(table)} (${cols.join(', ')}) VALUES (${vals.join(', ')}) RETURNING *`, -// params -// ); -// res.status(201).json({ inserted: rows[0] }); -// } -// } catch (e) { -// if (e.code === '23503') return res.status(400).json({ error: 'Violación de clave foránea', detail: e.detail }); -// if (e.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail }); -// if (e.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail }); -// if (e.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail }); -// throw e; -// } -// } catch (e) { -// res.status(400).json({ error: e.message }); -// } -// }); - -// app.get('/api/comandas', async (req, res, next) => { -// try { -// const estado = (req.query.estado || '').trim() || null; -// const limit = Math.min(parseInt(req.query.limit || '200', 10), 1000); - -// const { rows } = await mainPool.query( -// `SELECT * FROM public.f_comandas_resumen($1, $2)`, -// [estado, limit] -// ); -// res.json(rows); -// } catch (e) { next(e); } -// }); - - -// // Detalle de una comanda (con nombres de productos) -// // GET /api/comandas/:id/detalle -// app.get('/api/comandas/:id/detalle', (req, res, next) => -// mainPool.query( -// `SELECT id_det_comanda, id_producto, producto_nombre, -// cantidad, pre_unitario, subtotal, observaciones -// FROM public.v_comandas_detalle_items -// WHERE id_comanda = $1::int -// ORDER BY id_det_comanda`, -// [req.params.id] -// ) -// .then(r => res.json(r.rows)) -// .catch(next) -// ); - -// // Cerrar comanda (setea estado y fec_cierre en DB) -// app.post('/api/comandas/:id/cerrar', async (req, res, next) => { -// try { -// const id = Number(req.params.id); -// if (!Number.isInteger(id) || id <= 0) { -// return res.status(400).json({ error: 'id inválido' }); -// } -// const { rows } = await mainPool.query( -// `SELECT public.f_cerrar_comanda($1) AS data`, -// [id] -// ); -// if (!rows.length || rows[0].data === null) { -// return res.status(404).json({ error: 'Comanda no encontrada' }); -// } -// res.json(rows[0].data); -// } catch (err) { next(err); } -// }); - -// Abrir (reabrir) comanda -app.post('/api/comandas/:id/abrir', async (req, res, next) => { - try { - const id = Number(req.params.id); - if (!Number.isInteger(id) || id <= 0) { - return res.status(400).json({ error: 'id inválido' }); - } - const { rows } = await mainPool.query( - `SELECT public.f_abrir_comanda($1) AS data`, - [id] - ); - if (!rows.length || rows[0].data === null) { - return res.status(404).json({ error: 'Comanda no encontrada' }); - } - res.json(rows[0].data); - } catch (err) { next(err); } -}); \ No newline at end of file diff --git a/services/auth/src/api/rpc.js b/services/auth/src/api/rpc.js deleted file mode 100644 index 56766d1..0000000 --- a/services/auth/src/api/rpc.js +++ /dev/null @@ -1,230 +0,0 @@ -// // GET producto + receta -// app.get('/api/rpc/get_producto/:id', async (req, res) => { -// const id = Number(req.params.id); -// const { rows } = await mainPool.query('SELECT public.get_producto($1) AS data', [id]); -// res.json(rows[0]?.data || {}); -// }); - -// // POST guardar producto + receta - -// app.post('/api/rpc/save_producto', async (req, res) => { -// try { -// // console.debug('receta payload:', req.body?.receta); // habilitalo si lo necesitás -// const q = 'SELECT public.save_producto($1,$2,$3,$4,$5,$6,$7::jsonb) AS id_producto'; -// const { id_producto = null, nombre, img_producto = null, precio = 0, activo = true, id_categoria = null, receta = [] } = req.body || {}; -// const params = [id_producto, nombre, img_producto, precio, activo, id_categoria, JSON.stringify(receta || [])]; -// const { rows } = await mainPool.query(q, params); -// res.json(rows[0] || {}); -// } catch (e) { -// console.error(e); -// res.status(500).json({ error: 'save_producto failed' }); -// } -// }); - -// // GET MP + proveedores -// app.get('/api/rpc/get_materia/:id', async (req, res) => { -// const id = Number(req.params.id); -// try { -// const { rows } = await mainPool.query('SELECT public.get_materia_prima($1) AS data', [id]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); -// res.status(500).json({ error: 'get_materia failed' }); -// } -// }); - -// // SAVE MP + proveedores (array) -// app.post('/api/rpc/save_materia', async (req, res) => { -// const { id_mat_prima = null, nombre, unidad, activo = true, proveedores = [] } = req.body || {}; -// try { -// const q = 'SELECT public.save_materia_prima($1,$2,$3,$4,$5::jsonb) AS id_mat_prima'; -// const params = [id_mat_prima, nombre, unidad, activo, JSON.stringify(proveedores || [])]; -// const { rows } = await mainPool.query(q, params); -// res.json(rows[0] || {}); -// } catch (e) { -// console.error(e); -// res.status(500).json({ error: 'save_materia failed' }); -// } -// }); - -// // POST /api/rpc/find_usuarios_por_documentos { docs: ["12345678","09123456", ...] } -// app.post('/api/rpc/find_usuarios_por_documentos', async (req, res) => { -// try { -// const docs = Array.isArray(req.body?.docs) ? req.body.docs : []; -// const sql = 'SELECT public.find_usuarios_por_documentos($1::jsonb) AS data'; -// const { rows } = await mainPool.query(sql, [JSON.stringify(docs)]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); -// res.status(500).json({ error: 'find_usuarios_por_documentos failed' }); -// } -// }); - -// // POST /api/rpc/import_asistencia { registros: [...], origen?: "AGL_001.txt" } -// app.post('/api/rpc/import_asistencia', async (req, res) => { -// try { -// const registros = Array.isArray(req.body?.registros) ? req.body.registros : []; -// const origen = req.body?.origen || null; -// const sql = 'SELECT public.import_asistencia($1::jsonb,$2) AS data'; -// const { rows } = await mainPool.query(sql, [JSON.stringify(registros), origen]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); -// res.status(500).json({ error: 'import_asistencia failed' }); -// } -// }); - -// // Consultar datos de asistencia (raw + pares) para un usuario y rango -// app.post('/api/rpc/asistencia_get', async (req, res) => { -// try { -// const { doc, desde, hasta } = req.body || {}; -// const sql = 'SELECT public.asistencia_get($1::text,$2::date,$3::date) AS data'; -// const { rows } = await mainPool.query(sql, [doc, desde, hasta]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); res.status(500).json({ error: 'asistencia_get failed' }); -// } -// }); - -// // Editar un registro crudo y recalcular pares -// app.post('/api/rpc/asistencia_update_raw', async (req, res) => { -// try { -// const { id_raw, fecha, hora, modo } = req.body || {}; -// const sql = 'SELECT public.asistencia_update_raw($1::bigint,$2::date,$3::text,$4::text) AS data'; -// const { rows } = await mainPool.query(sql, [id_raw, fecha, hora, modo ?? null]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); res.status(500).json({ error: 'asistencia_update_raw failed' }); -// } -// }); - -// // Eliminar un registro crudo y recalcular pares -// app.post('/api/rpc/asistencia_delete_raw', async (req, res) => { -// try { -// const { id_raw } = req.body || {}; -// const sql = 'SELECT public.asistencia_delete_raw($1::bigint) AS data'; -// const { rows } = await mainPool.query(sql, [id_raw]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); res.status(500).json({ error: 'asistencia_delete_raw failed' }); -// } -// }); - -// // POST /api/rpc/report_tickets { year } -// app.post('/api/rpc/report_tickets', async (req, res) => { -// try { -// const y = parseInt(req.body?.year ?? req.query?.year, 10); -// const year = (Number.isFinite(y) && y >= 2000 && y <= 2100) -// ? y -// : (new Date()).getFullYear(); - -// const { rows } = await mainPool.query( -// 'SELECT public.report_tickets_year($1::int) AS j', [year] -// ); -// res.json(rows[0].j); -// } catch (e) { -// console.error('report_tickets error:', e); -// res.status(500).json({ -// error: 'report_tickets failed', -// message: e.message, detail: e.detail, where: e.where, code: e.code -// }); -// } -// }); - -// // POST /api/rpc/report_asistencia { desde: 'YYYY-MM-DD', hasta: 'YYYY-MM-DD' } -// app.post('/api/rpc/report_asistencia', async (req, res) => { -// try { -// let { desde, hasta } = req.body || {}; -// // defaults si vienen vacíos/invalidos -// const re = /^\d{4}-\d{2}-\d{2}$/; -// if (!re.test(desde) || !re.test(hasta)) { -// const end = new Date(); -// const start = new Date(end); start.setDate(end.getDate() - 30); -// desde = start.toISOString().slice(0, 10); -// hasta = end.toISOString().slice(0, 10); -// } - -// const { rows } = await mainPool.query( -// 'SELECT public.report_asistencia($1::date,$2::date) AS j', [desde, hasta] -// ); -// res.json(rows[0].j); -// } catch (e) { -// console.error('report_asistencia error:', e); -// res.status(500).json({ -// error: 'report_asistencia failed', -// message: e.message, detail: e.detail, where: e.where, code: e.code -// }); -// } -// }); - -// // Guardar (insert/update) -// app.post('/api/rpc/save_compra', async (req, res) => { -// try { -// const { id_compra, id_proveedor, fec_compra, detalles } = req.body || {}; -// const sql = 'SELECT * FROM public.save_compra($1::int,$2::int,$3::timestamptz,$4::jsonb)'; -// const args = [id_compra ?? null, id_proveedor, fec_compra ? new Date(fec_compra) : null, JSON.stringify(detalles)]; -// const { rows } = await mainPool.query(sql, args); -// res.json(rows[0]); // { id_compra, total } -// } catch (e) { -// console.error('save_compra error:', e); -// res.status(500).json({ error: 'save_compra failed', message: e.message, detail: e.detail, where: e.where, code: e.code }); -// } -// }); - -// // Obtener para editar -// app.post('/api/rpc/get_compra', async (req, res) => { -// try { -// const { id_compra } = req.body || {}; -// const sql = `SELECT public.get_compra($1::int) AS data`; -// const { rows } = await mainPool.query(sql, [id_compra]); -// res.json(rows[0]?.data || {}); -// } catch (e) { -// console.error(e); res.status(500).json({ error: 'get_compra failed' }); -// } -// }); - -// // Eliminar -// app.post('/api/rpc/delete_compra', async (req, res) => { -// try { -// const { id_compra } = req.body || {}; -// await mainPool.query(`SELECT public.delete_compra($1::int)`, [id_compra]); -// res.json({ ok: true }); -// } catch (e) { -// console.error(e); res.status(500).json({ error: 'delete_compra failed' }); -// } -// }); - -// // POST /api/rpc/report_gastos { year: 2025 } -// app.post('/api/rpc/report_gastos', async (req, res) => { -// try { -// const year = parseInt(req.body?.year ?? new Date().getFullYear(), 10); -// const { rows } = await mainPool.query( -// 'SELECT public.report_gastos($1::int) AS j', [year] -// ); -// res.json(rows[0].j); -// } catch (e) { -// console.error('report_gastos error:', e); -// res.status(500).json({ -// error: 'report_gastos failed', -// message: e.message, detail: e.detail, code: e.code -// }); -// } -// }); - -// // (Opcional) GET para probar rápido desde el navegador: -// // /api/rpc/report_gastos?year=2025 -// app.get('/api/rpc/report_gastos', async (req, res) => { -// try { -// const year = parseInt(req.query.year ?? new Date().getFullYear(), 10); -// const { rows } = await mainPool.query( -// 'SELECT public.report_gastos($1::int) AS j', [year] -// ); -// res.json(rows[0].j); -// } catch (e) { -// console.error('report_gastos error:', e); -// res.status(500).json({ -// error: 'report_gastos failed', -// message: e.message, detail: e.detail, code: e.code -// }); -// } -// }); \ No newline at end of file diff --git a/services/auth/src/api/v1/routes/routes.js b/services/auth/src/api/v1/routes/routes.js deleted file mode 100644 index c17f2cc..0000000 --- a/services/auth/src/api/v1/routes/routes.js +++ /dev/null @@ -1,340 +0,0 @@ -// services/manso/src/api/v1/routes/routes.js - -import { Router } from 'express'; -import pool from '#db'; // Pool Singleton -const router = Router(); - -// ========================================================== -// Rutas de API v1 -// ========================================================== - - - -// ---------------------------------------------------------- -// API Comandas -// ---------------------------------------------------------- - -router.route('/comandas').get( async (req, res, next) => { - try { - var client = await pool.getClient() - const estado = (req.query.estado || '').trim() || null; - const limit = Math.min(parseInt(req.query.limit || '200', 10), 1000); - - const { rows } = await client.query( - `SELECT * FROM public.f_comandas_resumen($1, $2)`, - [estado, limit] - ); - res.json(rows); - } catch (e) { - next(e); - } finally { - client.release(); - } -}); - -router.route('/comandas/:id/detalle').get( async (req, res, next) => { - try { - const client = await pool.getClient() - client.query( - `SELECT id_det_comanda, id_producto, producto_nombre, - cantidad, pre_unitario, subtotal, observaciones - FROM public.v_comandas_detalle_items - WHERE id_comanda = $1::int - ORDER BY id_det_comanda`, - [req.params.id] - ) - .then(r => res.json(r.rows)) - .catch(next) - client.release(); - } catch (error) { - next(e); - } -}); - -router.route('/comandas/:id/cerrar').post( async (req, res, next) => { - try { - const client = await pool.getClient() - const id = Number(req.params.id); - if (!Number.isInteger(id) || id <= 0) { - return res.status(400).json({ error: 'id inválido' }); - } - const { rows } = await client.query( - `SELECT public.f_cerrar_comanda($1) AS data`, - [id] - ); - if (!rows.length || rows[0].data === null) { - return res.status(404).json({ error: 'Comanda no encontrada' }); - } - res.json(rows[0].data); - client.release(); - } catch (err) { next(err); } -}); - -router.route('/comandas/:id/abrir').post( async (req, res, next) => { - try { - const client = await pool.getClient() - const id = Number(req.params.id); - if (!Number.isInteger(id) || id <= 0) { - return res.status(400).json({ error: 'id inválido' }); - } - const { rows } = await client.query( - `SELECT public.f_abrir_comanda($1) AS data`, - [id] - ); - if (!rows.length || rows[0].data === null) { - return res.status(404).json({ error: 'Comanda no encontrada' }); - } - res.json(rows[0].data); - client.release(); - } catch (err) { next(err); } -}); - - - -// ---------------------------------------------------------- -// API Productos -// ---------------------------------------------------------- - -// GET producto + receta -router.route('/rpc/get_producto/:id').get( async (req, res) => { - const client = await pool.getClient() - const id = Number(req.params.id); - const { rows } = await client.query('SELECT public.get_producto($1) AS data', [id]); - res.json(rows[0]?.data || {}); - client.release(); -}); - -// POST guardar producto + receta -router.route('/rpc/save_producto').post(async (req, res) => { - try { - // console.debug('receta payload:', req.body?.receta); // habilitalo si lo necesitás - const client = await pool.getClient() - const q = 'SELECT public.save_producto($1,$2,$3,$4,$5,$6,$7::jsonb) AS id_producto'; - const { id_producto=null, nombre, img_producto=null, precio=0, activo=true, id_categoria=null, receta=[] } = req.body || {}; - const params = [id_producto, nombre, img_producto, precio, activo, id_categoria, JSON.stringify(receta||[])]; - const { rows } = await client.query(q, params); - res.json(rows[0] || {}); - client.release(); - } catch(e) { - console.error(e); - res.status(500).json({ error: 'save_producto failed' }); - } -}); - - - -// ---------------------------------------------------------- -// API Materias Primas -// ---------------------------------------------------------- - -// GET MP + proveedores -router.route('/rpc/get_materia/:id').get(async (req, res) => { - const id = Number(req.params.id); - try { - const client = await pool.getClient() - const { rows } = await client.query('SELECT public.get_materia_prima($1) AS data', [id]); - res.json(rows[0]?.data || {}); - client.release(); - } catch (e) { - console.error(e); - res.status(500).json({ error: 'get_materia failed' }); - } -}); - -// SAVE MP + proveedores (array) -router.route('/rpc/save_materia').post( async (req, res) => { - const { id_mat_prima=null, nombre, unidad, activo=true, proveedores=[] } = req.body || {}; - try { - const q = 'SELECT public.save_materia_prima($1,$2,$3,$4,$5::jsonb) AS id_mat_prima'; - const params = [id_mat_prima, nombre, unidad, activo, JSON.stringify(proveedores||[])]; - const { rows } = await pool.query(q, params); - res.json(rows[0] || {}); - } catch (e) { - console.error(e); - res.status(500).json({ error: 'save_materia failed' }); - } -}); - - - -// ---------------------------------------------------------- -// API Usuarios y Asistencias -// ---------------------------------------------------------- - -// POST /api/rpc/find_usuarios_por_documentos { docs: ["12345678","09123456", ...] } -router.route('/rpc/find_usuarios_por_documentos').post( async (req, res) => { - try { - const docs = Array.isArray(req.body?.docs) ? req.body.docs : []; - const sql = 'SELECT public.find_usuarios_por_documentos($1::jsonb) AS data'; - const { rows } = await pool.query(sql, [JSON.stringify(docs)]); - res.json(rows[0]?.data || {}); - } catch (e) { - console.error(e); - res.status(500).json({ error: 'find_usuarios_por_documentos failed' }); - } -}); - -// POST /api/rpc/import_asistencia { registros: [...], origen?: "AGL_001.txt" } -router.route('/rpc/import_asistencia').post( async (req, res) => { - try { - const registros = Array.isArray(req.body?.registros) ? req.body.registros : []; - const origen = req.body?.origen || null; - const sql = 'SELECT public.import_asistencia($1::jsonb,$2) AS data'; - const { rows } = await pool.query(sql, [JSON.stringify(registros), origen]); - res.json(rows[0]?.data || {}); - } catch (e) { - console.error(e); - res.status(500).json({ error: 'import_asistencia failed' }); - } -}); - -// Consultar datos de asistencia (raw + pares) para un usuario y rango -router.route('/rpc/asistencia_get').post( async (req, res) => { - try { - const { doc, desde, hasta } = req.body || {}; - const sql = 'SELECT public.asistencia_get($1::text,$2::date,$3::date) AS data'; - const { rows } = await pool.query(sql, [doc, desde, hasta]); - res.json(rows[0]?.data || {}); - } catch (e) { - console.error(e); res.status(500).json({ error: 'asistencia_get failed' }); - } -}); - -// Editar un registro crudo y recalcular pares -router.route('/rpc/asistencia_update_raw').post( async (req, res) => { - try { - const { id_raw, fecha, hora, modo } = req.body || {}; - const sql = 'SELECT public.asistencia_update_raw($1::bigint,$2::date,$3::text,$4::text) AS data'; - const { rows } = await pool.query(sql, [id_raw, fecha, hora, modo ?? null]); - res.json(rows[0]?.data || {}); - } catch (e) { - console.error(e); res.status(500).json({ error: 'asistencia_update_raw failed' }); - } -}); - -// Eliminar un registro crudo y recalcular pares -router.route('/rpc/asistencia_delete_raw').post( async (req, res) => { - try { - const { id_raw } = req.body || {}; - const sql = 'SELECT public.asistencia_delete_raw($1::bigint) AS data'; - const { rows } = await pool.query(sql, [id_raw]); - res.json(rows[0]?.data || {}); - } catch (e) { - console.error(e); res.status(500).json({ error: 'asistencia_delete_raw failed' }); - } -}); - - -// ---------------------------------------------------------- -// API Reportes -// ---------------------------------------------------------- - -// POST /api/rpc/report_tickets { year } -router.route('/rpc/report_tickets').post( async (req, res) => { - try { - const y = parseInt(req.body?.year ?? req.query?.year, 10); - const year = (Number.isFinite(y) && y >= 2000 && y <= 2100) - ? y - : (new Date()).getFullYear(); - - const { rows } = await pool.query( - 'SELECT public.report_tickets_year($1::int) AS j', [year] - ); - res.json(rows[0].j); - } catch (e) { - console.error('report_tickets error:', e); - res.status(500).json({ - error: 'report_tickets failed', - message: e.message, detail: e.detail, where: e.where, code: e.code - }); - } -}); - -// POST /api/rpc/report_asistencia { desde: 'YYYY-MM-DD', hasta: 'YYYY-MM-DD' } -router.route('/rpc/report_asistencia').post( async (req, res) => { - try { - let { desde, hasta } = req.body || {}; - // defaults si vienen vacíos/invalidos - const re = /^\d{4}-\d{2}-\d{2}$/; - if (!re.test(desde) || !re.test(hasta)) { - const end = new Date(); - const start = new Date(end); start.setDate(end.getDate()-30); - desde = start.toISOString().slice(0,10); - hasta = end.toISOString().slice(0,10); - } - - const { rows } = await pool.query( - 'SELECT public.report_asistencia($1::date,$2::date) AS j', [desde, hasta] - ); - res.json(rows[0].j); - } catch (e) { - console.error('report_asistencia error:', e); - res.status(500).json({ - error: 'report_asistencia failed', - message: e.message, detail: e.detail, where: e.where, code: e.code - }); - } -}); - -// ---------------------------------------------------------- -// API Compras y Gastos -// ---------------------------------------------------------- - -// Guardar (insert/update) -router.route('/rpc/save_compra').post( async (req, res) => { - try { - const { id_compra, id_proveedor, fec_compra, detalles } = req.body || {}; - const sql = 'SELECT * FROM public.save_compra($1::int,$2::int,$3::timestamptz,$4::jsonb)'; - const args = [id_compra ?? null, id_proveedor, fec_compra ? new Date(fec_compra) : null, JSON.stringify(detalles)]; - const { rows } = await pool.query(sql, args); - res.json(rows[0]); // { id_compra, total } - } catch (e) { - console.error('save_compra error:', e); - res.status(500).json({ error: 'save_compra failed', message: e.message, detail: e.detail, where: e.where, code: e.code }); - } -}); - - -// Obtener para editar -router.route('/rpc/get_compra').post( async (req, res) => { - try { - const { id_compra } = req.body || {}; - const sql = `SELECT public.get_compra($1::int) AS data`; - const { rows } = await pool.query(sql, [id_compra]); - res.json(rows[0]?.data || {}); - } catch (e) { - console.error(e); res.status(500).json({ error: 'get_compra failed' }); - } -}); - -// Eliminar -router.route('/rpc/delete_compra').post( async (req, res) => { - try { - const { id_compra } = req.body || {}; - await pool.query(`SELECT public.delete_compra($1::int)`, [id_compra]); - res.json({ ok: true }); - } catch (e) { - console.error(e); res.status(500).json({ error: 'delete_compra failed' }); - } -}); - - -// POST /api/rpc/report_gastos { year: 2025 } -router.route('/rpc/report_gastos').post( async (req, res) => { - try { - const year = parseInt(req.body?.year ?? new Date().getFullYear(), 10); - const { rows } = await pool.query( - 'SELECT public.report_gastos($1::int) AS j', [year] - ); - res.json(rows[0].j); - } catch (e) { - console.error('report_gastos error:', e); - res.status(500).json({ - error: 'report_gastos failed', - message: e.message, detail: e.detail, code: e.code - }); - } -}); - - -export default router; \ No newline at end of file diff --git a/services/auth/src/db/dumpl_manso_250905.sql b/services/auth/src/db/dumpl_manso_250905.sql deleted file mode 100644 index e58a318..0000000 --- a/services/auth/src/db/dumpl_manso_250905.sql +++ /dev/null @@ -1,3071 +0,0 @@ --- --- PostgreSQL database dump --- - -\restrict londHmqT4llS8Wof4ZnceO2dyFhn4jiR5xbaszMgZpMczgr6aVW6xQJxeUdqJwa - --- Dumped from database version 16.10 (Debian 16.10-1.pgdg13+1) --- Dumped by pg_dump version 16.10 (Debian 16.10-1.pgdg13+1) - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - --- --- Name: asistencia_delete_raw(bigint, text); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.asistencia_delete_raw(p_id_raw bigint, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE - v_id_usuario INT; - v_ts TIMESTAMPTZ; - v_t0 TIMESTAMPTZ; - v_t1 TIMESTAMPTZ; - v_del_raw INT; - v_del INT; - v_ins INT; -BEGIN - SELECT id_usuario, ts INTO v_id_usuario, v_ts - FROM public.asistencia_raw WHERE id_raw = p_id_raw; - IF v_id_usuario IS NULL THEN - RETURN jsonb_build_object('deleted',0,'msg','id_raw inexistente'); - END IF; - - v_t0 := v_ts - INTERVAL '1 day'; - v_t1 := v_ts + INTERVAL '1 day'; - - -- borrar raw - DELETE FROM public.asistencia_raw WHERE id_raw = p_id_raw; - GET DIAGNOSTICS v_del_raw = ROW_COUNT; - - -- recomputar pares en ventana - WITH tl AS ( - SELECT ar.ts, - ROW_NUMBER() OVER (ORDER BY ar.ts) AS rn - FROM public.asistencia_raw ar - WHERE ar.id_usuario = v_id_usuario - AND ar.ts BETWEEN v_t0 AND v_t1 - ), - ready AS ( - SELECT (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM tl t1 - JOIN tl t2 ON t2.rn = t1.rn + 1 - WHERE (t1.rn % 2) = 1 AND t2.ts > t1.ts - ), - del AS ( - DELETE FROM public.asistencia_intervalo ai - WHERE ai.id_usuario = v_id_usuario - AND (ai.desde BETWEEN v_t0 AND v_t1 OR ai.hasta BETWEEN v_t0 AND v_t1) - RETURNING 1 - ), - ins AS ( - INSERT INTO public.asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT v_id_usuario, r.fecha, r.desde, r.hasta, r.dur_min, 'delete_adjust' - FROM ready r - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT (SELECT COUNT(*) FROM del), (SELECT COUNT(*) FROM ins) INTO v_del, v_ins; - - RETURN jsonb_build_object('deleted',v_del_raw,'deleted_pairs',v_del,'inserted_pairs',v_ins); -END; -$$; - - -ALTER FUNCTION public.asistencia_delete_raw(p_id_raw bigint, p_tz text) OWNER TO manso; - --- --- Name: asistencia_get(text, date, date, text); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.asistencia_get(p_doc text, p_desde date, p_hasta date, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH u AS ( - SELECT id_usuario, documento, nombre, apellido - FROM public.usuarios - WHERE regexp_replace(documento,'^\s*0+','','g') = regexp_replace(p_doc,'^\s*0+','','g') - LIMIT 1 -), -r AS ( - SELECT ar.id_raw, - (ar.ts AT TIME ZONE p_tz)::date AS fecha, - to_char(ar.ts AT TIME ZONE p_tz,'HH24:MI:SS') AS hora, - COALESCE(ar.modo,'') AS modo, - COALESCE(ar.origen,'') AS origen, - ar.ts - FROM public.asistencia_raw ar - JOIN u USING (id_usuario) - WHERE (ar.ts AT TIME ZONE p_tz)::date BETWEEN p_desde AND p_hasta -), -i AS ( - SELECT ai.id_intervalo, - ai.fecha, - to_char(ai.desde AT TIME ZONE p_tz,'HH24:MI:SS') AS desde_hora, - to_char(ai.hasta AT TIME ZONE p_tz,'HH24:MI:SS') AS hasta_hora, - ai.dur_min - FROM public.asistencia_intervalo ai - JOIN u USING (id_usuario) - WHERE ai.fecha BETWEEN p_desde AND p_hasta -) -SELECT jsonb_build_object( - 'usuario', (SELECT to_jsonb(u.*) FROM u), - 'raw', COALESCE((SELECT jsonb_agg(to_jsonb(r.*) ORDER BY r.ts) FROM r),'[]'::jsonb), - 'intervalos', COALESCE((SELECT jsonb_agg(to_jsonb(i.*) ORDER BY i.fecha, i.id_intervalo) FROM i),'[]'::jsonb) -); -$$; - - -ALTER FUNCTION public.asistencia_get(p_doc text, p_desde date, p_hasta date, p_tz text) OWNER TO manso; - --- --- Name: asistencia_update_raw(bigint, date, text, text, text); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.asistencia_update_raw(p_id_raw bigint, p_fecha date, p_hora text, p_modo text, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE - v_id_usuario INT; - v_ts_old TIMESTAMPTZ; - v_ts_new TIMESTAMPTZ; - v_t0 TIMESTAMPTZ; - v_t1 TIMESTAMPTZ; - v_del INT; - v_ins INT; -BEGIN - -- leer estado previo - SELECT id_usuario, ts INTO v_id_usuario, v_ts_old - FROM public.asistencia_raw WHERE id_raw = p_id_raw; - IF v_id_usuario IS NULL THEN - RETURN jsonb_build_object('updated',0,'msg','id_raw inexistente'); - END IF; - - -- construir ts nuevo - v_ts_new := make_timestamptz( - EXTRACT(YEAR FROM p_fecha)::INT, - EXTRACT(MONTH FROM p_fecha)::INT, - EXTRACT(DAY FROM p_fecha)::INT, - split_part(p_hora,':',1)::INT, - split_part(p_hora,':',2)::INT, - COALESCE(NULLIF(split_part(p_hora,':',3),''), '0')::INT, - p_tz); - - -- aplicar update - UPDATE public.asistencia_raw - SET ts = v_ts_new, - modo = COALESCE(p_modo, modo) - WHERE id_raw = p_id_raw; - - -- ventana de recálculo - v_t0 := LEAST(v_ts_old, v_ts_new) - INTERVAL '1 day'; - v_t1 := GREATEST(v_ts_old, v_ts_new) + INTERVAL '1 day'; - - -- recomputar pares en la ventana: borrar los del rango y reinsertar - WITH tl AS ( - SELECT ar.ts, - ROW_NUMBER() OVER (ORDER BY ar.ts) AS rn - FROM public.asistencia_raw ar - WHERE ar.id_usuario = v_id_usuario - AND ar.ts BETWEEN v_t0 AND v_t1 - ), - ready AS ( - SELECT (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM tl t1 - JOIN tl t2 ON t2.rn = t1.rn + 1 - WHERE (t1.rn % 2) = 1 AND t2.ts > t1.ts - ), - del AS ( - DELETE FROM public.asistencia_intervalo ai - WHERE ai.id_usuario = v_id_usuario - AND (ai.desde BETWEEN v_t0 AND v_t1 OR ai.hasta BETWEEN v_t0 AND v_t1) - RETURNING 1 - ), - ins AS ( - INSERT INTO public.asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT v_id_usuario, r.fecha, r.desde, r.hasta, r.dur_min, 'edit_manual' - FROM ready r - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT (SELECT COUNT(*) FROM del), (SELECT COUNT(*) FROM ins) INTO v_del, v_ins; - - RETURN jsonb_build_object('updated',1,'deleted_pairs',v_del,'inserted_pairs',v_ins); -END; -$$; - - -ALTER FUNCTION public.asistencia_update_raw(p_id_raw bigint, p_fecha date, p_hora text, p_modo text, p_tz text) OWNER TO manso; - --- --- Name: delete_compra(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.delete_compra(p_id_compra integer) RETURNS void - LANGUAGE plpgsql - AS $$ -BEGIN - DELETE FROM public.deta_comp_materias WHERE id_compra = p_id_compra; - DELETE FROM public.deta_comp_producto WHERE id_compra = p_id_compra; - DELETE FROM public.compras WHERE id_compra = p_id_compra; -END; -$$; - - -ALTER FUNCTION public.delete_compra(p_id_compra integer) OWNER TO manso; - --- --- Name: f_abrir_comanda(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.f_abrir_comanda(p_id integer) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE r jsonb; -BEGIN - UPDATE public.comandas - SET estado = 'abierta', - fec_cierre = NULL - WHERE id_comanda = p_id; - - IF NOT FOUND THEN - RETURN NULL; - END IF; - - SELECT to_jsonb(v) INTO r - FROM public.v_comandas_resumen v - WHERE v.id_comanda = p_id; - - RETURN r; -END; -$$; - - -ALTER FUNCTION public.f_abrir_comanda(p_id integer) OWNER TO manso; - --- --- Name: f_cerrar_comanda(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.f_cerrar_comanda(p_id integer) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE r jsonb; -BEGIN - UPDATE public.comandas - SET estado = 'cerrada', - fec_cierre = COALESCE(fec_cierre, NOW()) - WHERE id_comanda = p_id; - - IF NOT FOUND THEN - RETURN NULL; - END IF; - - SELECT to_jsonb(v) INTO r - FROM public.v_comandas_resumen v - WHERE v.id_comanda = p_id; - - RETURN r; -END; -$$; - - -ALTER FUNCTION public.f_cerrar_comanda(p_id integer) OWNER TO manso; - --- --- Name: f_comanda_detalle_json(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.f_comanda_detalle_json(p_id_comanda integer) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH base AS ( - SELECT - c.id_comanda, - c.fec_creacion, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM public.comandas c - JOIN public.usuarios u ON u.id_usuario = c.id_usuario - JOIN public.mesas m ON m.id_mesa = c.id_mesa - LEFT JOIN public.deta_comandas d ON d.id_comanda = c.id_comanda - LEFT JOIN public.productos p ON p.id_producto = d.id_producto - WHERE c.id_comanda = p_id_comanda -), -hdr AS ( - -- 1 sola fila con los datos de cabecera - SELECT DISTINCT - id_comanda, fec_creacion, estado, observaciones, - id_usuario, usuario_nombre, usuario_apellido, - id_mesa, mesa_numero, mesa_apodo - FROM base -), -agg_items AS ( - SELECT - COALESCE( - jsonb_agg( - jsonb_build_object( - 'producto_id', b.id_producto, - 'producto', b.producto_nombre, - 'cantidad', b.cantidad, - 'pre_unitario', b.pre_unitario, - 'subtotal', b.subtotal - ) - ORDER BY b.producto_nombre NULLS LAST - ) FILTER (WHERE b.id_producto IS NOT NULL), - '[]'::jsonb - ) AS items - FROM base b -), -tot AS ( - SELECT - COUNT(*) FILTER (WHERE id_producto IS NOT NULL) AS items, - COALESCE(SUM(subtotal), 0)::numeric AS total - FROM base -) -SELECT - CASE - WHEN EXISTS (SELECT 1 FROM hdr) THEN - jsonb_build_object( - 'id_comanda', h.id_comanda, - 'fec_creacion', h.fec_creacion, - 'estado', h.estado, - 'observaciones',h.observaciones, - 'usuario', jsonb_build_object( - 'id_usuario', h.id_usuario, - 'nombre', h.usuario_nombre, - 'apellido', h.usuario_apellido - ), - 'mesa', jsonb_build_object( - 'id_mesa', h.id_mesa, - 'numero', h.mesa_numero, - 'apodo', h.mesa_apodo - ), - 'items', i.items, - 'totales', jsonb_build_object( - 'items', t.items, - 'total', t.total - ) - ) - ELSE NULL - END -FROM hdr h, agg_items i, tot t; -$$; - - -ALTER FUNCTION public.f_comanda_detalle_json(p_id_comanda integer) OWNER TO manso; - --- --- Name: f_comanda_detalle_rows(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.f_comanda_detalle_rows(p_id_comanda integer) RETURNS TABLE(id_comanda integer, fec_creacion timestamp without time zone, estado text, observaciones text, id_usuario integer, usuario_nombre text, usuario_apellido text, id_mesa integer, mesa_numero integer, mesa_apodo text, producto_id integer, producto_nombre text, cantidad numeric, pre_unitario numeric, subtotal numeric, items integer, total numeric) - LANGUAGE sql - AS $$ -WITH base AS ( - SELECT - c.id_comanda, c.fec_creacion, c.estado, c.observaciones, - u.id_usuario, u.nombre AS usuario_nombre, u.apellido AS usuario_apellido, - m.id_mesa, m.numero AS mesa_numero, m.apodo AS mesa_apodo, - d.id_producto, p.nombre AS producto_nombre, - d.cantidad, d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM public.comandas c - JOIN public.usuarios u ON u.id_usuario = c.id_usuario - JOIN public.mesas m ON m.id_mesa = c.id_mesa - LEFT JOIN public.deta_comandas d ON d.id_comanda = c.id_comanda - LEFT JOIN public.productos p ON p.id_producto = d.id_producto - WHERE c.id_comanda = p_id_comanda -), -tot AS ( - SELECT - COUNT(*) FILTER (WHERE id_producto IS NOT NULL) AS items, - COALESCE(SUM(subtotal), 0) AS total - FROM base -) -SELECT - b.id_comanda, b.fec_creacion, b.estado, b.observaciones, - b.id_usuario, b.usuario_nombre, b.usuario_apellido, - b.id_mesa, b.mesa_numero, b.mesa_apodo, - b.id_producto, b.producto_nombre, - b.cantidad, b.pre_unitario, b.subtotal, - t.items, t.total -FROM base b CROSS JOIN tot t -ORDER BY b.producto_nombre NULLS LAST; -$$; - - -ALTER FUNCTION public.f_comanda_detalle_rows(p_id_comanda integer) OWNER TO manso; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: comandas; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.comandas ( - id_comanda integer NOT NULL, - id_usuario integer NOT NULL, - id_mesa integer NOT NULL, - fec_creacion timestamp without time zone DEFAULT now() NOT NULL, - estado text NOT NULL, - observaciones text, - fec_cierre timestamp with time zone, - CONSTRAINT comandas_estado_check CHECK ((estado = ANY (ARRAY['abierta'::text, 'cerrada'::text, 'pagada'::text, 'anulada'::text]))) -); - - -ALTER TABLE public.comandas OWNER TO manso; - --- --- Name: COLUMN comandas.fec_cierre; Type: COMMENT; Schema: public; Owner: manso --- - -COMMENT ON COLUMN public.comandas.fec_cierre IS 'Fecha/hora de cierre de la comanda (NULL si está abierta)'; - - --- --- Name: deta_comandas; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.deta_comandas ( - id_det_comanda integer NOT NULL, - id_comanda integer NOT NULL, - id_producto integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - observaciones text, - CONSTRAINT deta_comandas_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comandas_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - - -ALTER TABLE public.deta_comandas OWNER TO manso; - --- --- Name: mesas; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.mesas ( - id_mesa integer NOT NULL, - numero integer NOT NULL, - apodo text NOT NULL, - estado text DEFAULT 'libre'::text NOT NULL, - CONSTRAINT mesas_estado_check CHECK ((estado = ANY (ARRAY['libre'::text, 'ocupada'::text, 'reservada'::text]))) -); - - -ALTER TABLE public.mesas OWNER TO manso; - --- --- Name: usuarios; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.usuarios ( - id_usuario integer NOT NULL, - documento text, - img_perfil character varying(255) DEFAULT 'img_perfil.png'::character varying NOT NULL, - nombre text NOT NULL, - apellido text NOT NULL, - correo text, - telefono text, - fec_nacimiento date, - activo boolean DEFAULT true -); - - -ALTER TABLE public.usuarios OWNER TO manso; - --- --- Name: v_comandas_resumen; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW public.v_comandas_resumen AS - WITH items AS ( - SELECT d.id_comanda, - count(*) AS items, - sum((d.cantidad * d.pre_unitario)) AS total - FROM public.deta_comandas d - GROUP BY d.id_comanda - ) - SELECT c.id_comanda, - c.fec_creacion, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - COALESCE(i.items, (0)::bigint) AS items, - COALESCE(i.total, (0)::numeric) AS total, - c.fec_cierre, - CASE - WHEN (c.fec_cierre IS NOT NULL) THEN round((EXTRACT(epoch FROM (c.fec_cierre - (c.fec_creacion)::timestamp with time zone)) / 60.0), 1) - ELSE NULL::numeric - END AS duracion_min - FROM (((public.comandas c - JOIN public.usuarios u ON ((u.id_usuario = c.id_usuario))) - JOIN public.mesas m ON ((m.id_mesa = c.id_mesa))) - LEFT JOIN items i ON ((i.id_comanda = c.id_comanda))); - - -ALTER VIEW public.v_comandas_resumen OWNER TO manso; - --- --- Name: f_comandas_resumen(text, integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.f_comandas_resumen(p_estado text DEFAULT NULL::text, p_limit integer DEFAULT 200) RETURNS SETOF public.v_comandas_resumen - LANGUAGE sql - AS $$ - SELECT * - FROM public.v_comandas_resumen - WHERE (p_estado IS NULL OR estado = p_estado) - ORDER BY id_comanda DESC - LIMIT p_limit; -$$; - - -ALTER FUNCTION public.f_comandas_resumen(p_estado text, p_limit integer) OWNER TO manso; - --- --- Name: find_usuarios_por_documentos(jsonb); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.find_usuarios_por_documentos(p_docs jsonb) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH docs AS ( - SELECT DISTINCT - regexp_replace(value::text, '^\s*0+', '', 'g') AS doc_clean, - value::text AS original - FROM jsonb_array_elements_text(COALESCE(p_docs,'[]')) -), -rows AS ( - SELECT d.original AS documento, - u.nombre, - u.apellido, - (u.id_usuario IS NOT NULL) AS found - FROM docs d - LEFT JOIN public.usuarios u - ON regexp_replace(u.documento, '^\s*0+', '', 'g') = d.doc_clean -) -SELECT COALESCE( - jsonb_object_agg( - documento, - jsonb_build_object( - 'nombre', COALESCE(nombre, ''), - 'apellido', COALESCE(apellido, ''), - 'found', found - ) - ), - '{}'::jsonb -) -FROM rows; -$$; - - -ALTER FUNCTION public.find_usuarios_por_documentos(p_docs jsonb) OWNER TO manso; - --- --- Name: get_compra(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.get_compra(p_id_compra integer) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH cab AS ( - SELECT c.id_compra, c.id_proveedor, c.fec_compra, c.total - FROM public.compras c - WHERE c.id_compra = p_id_compra -), -dm AS ( - SELECT 'MAT'::text AS tipo, d.id_mat_prima AS id, - d.cantidad, d.pre_unitario AS precio - FROM public.deta_comp_materias d WHERE d.id_compra = p_id_compra -), -dp AS ( - SELECT 'PROD'::text AS tipo, d.id_producto AS id, - d.cantidad, d.pre_unitario AS precio - FROM public.deta_comp_producto d WHERE d.id_compra = p_id_compra -), -det AS ( - SELECT jsonb_agg(to_jsonb(x.*)) AS detalles - FROM ( - SELECT * FROM dm - UNION ALL - SELECT * FROM dp - ) x -) -SELECT jsonb_build_object( - 'id_compra', (SELECT id_compra FROM cab), - 'id_proveedor',(SELECT id_proveedor FROM cab), - 'fec_compra', to_char((SELECT fec_compra FROM cab),'YYYY-MM-DD HH24:MI:SS'), - 'total', (SELECT total FROM cab), - 'detalles', COALESCE((SELECT detalles FROM det),'[]'::jsonb) -); -$$; - - -ALTER FUNCTION public.get_compra(p_id_compra integer) OWNER TO manso; - --- --- Name: get_materia_prima(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.get_materia_prima(p_id integer) RETURNS jsonb - LANGUAGE sql - AS $$ -SELECT jsonb_build_object( - 'materia', to_jsonb(mp), - 'proveedores', COALESCE( - ( - SELECT jsonb_agg( - jsonb_build_object( - 'id_proveedor', pr.id_proveedor, - 'raz_social', pr.raz_social, - 'rut', pr.rut, - 'contacto', pr.contacto, - 'direccion', pr.direccion - ) - ) - FROM public.prov_mate_prima pmp - JOIN public.proveedores pr ON pr.id_proveedor = pmp.id_proveedor - WHERE pmp.id_mat_prima = mp.id_mat_prima - ), - '[]'::jsonb - ) -) -FROM public.mate_primas mp -WHERE mp.id_mat_prima = p_id; -$$; - - -ALTER FUNCTION public.get_materia_prima(p_id integer) OWNER TO manso; - --- --- Name: get_producto(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.get_producto(p_id integer) RETURNS jsonb - LANGUAGE sql - AS $$ -SELECT jsonb_build_object( - 'producto', to_jsonb(p), -- el registro completo del producto en JSONB - 'receta', COALESCE( - ( - SELECT jsonb_agg( - jsonb_build_object( - 'id_mat_prima', rp.id_mat_prima, - 'qty_por_unidad', rp.qty_por_unidad, - 'nombre', mp.nombre, - 'unidad', mp.unidad - ) - ) - FROM receta_producto rp - LEFT JOIN mate_primas mp USING (id_mat_prima) - WHERE rp.id_producto = p.id_producto - ), - '[]'::jsonb - ) -) -FROM productos p -WHERE p.id_producto = p_id; -$$; - - -ALTER FUNCTION public.get_producto(p_id integer) OWNER TO manso; - --- --- Name: import_asistencia(jsonb, text, text); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.import_asistencia(p_registros jsonb, p_origen text, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $_$ -DECLARE - v_ins_raw INT; - v_ins_pairs INT; - v_miss JSONB; -BEGIN - WITH - -- 1) JSON -> filas - j AS ( - SELECT - regexp_replace(elem->>'doc','^\s*0+','','g')::TEXT AS doc_clean, - (elem->>'isoDate')::DATE AS d, - elem->>'time' AS time_str, - NULLIF(elem->>'mode','') AS modo - FROM jsonb_array_elements(COALESCE(p_registros,'[]')) elem - ), - -- 2) Vincular a usuarios - u AS ( - SELECT j.*, u.id_usuario - FROM j - LEFT JOIN public.usuarios u - ON regexp_replace(u.documento,'^\s*0+','','g') = j.doc_clean - ), - -- 3) Documentos faltantes - miss AS ( - SELECT jsonb_agg(doc_clean) AS missing - FROM u WHERE id_usuario IS NULL - ), - -- 4) TS determinista en TZ del negocio - parsed AS ( - SELECT - u.id_usuario, - u.modo, - make_timestamptz( - EXTRACT(YEAR FROM u.d)::INT, - EXTRACT(MONTH FROM u.d)::INT, - EXTRACT(DAY FROM u.d)::INT, - split_part(u.time_str,':',1)::INT, - split_part(u.time_str,':',2)::INT, - COALESCE(NULLIF(split_part(u.time_str,':',3),''),'0')::INT, - p_tz - ) AS ts_calc - FROM u - WHERE u.id_usuario IS NOT NULL - ), - -- 5) Ventana por usuario (±1 día de lo importado) - win AS ( - SELECT id_usuario, - (MIN(ts_calc) - INTERVAL '1 day') AS t0, - (MAX(ts_calc) + INTERVAL '1 day') AS t1 - FROM parsed - GROUP BY id_usuario - ), - -- 6) Lo existente en BD dentro de la ventana - existing AS ( - SELECT ar.id_usuario, ar.ts - FROM public.asistencia_raw ar - JOIN win w ON w.id_usuario = ar.id_usuario - AND ar.ts BETWEEN w.t0 AND w.t1 - ), - -- 7) CANDIDATE = existente ∪ archivo (sin duplicados) - candidate AS ( - SELECT id_usuario, ts FROM existing - UNION -- ¡clave para evitar doble click! - SELECT id_usuario, ts_calc AS ts FROM parsed - ), - -- 8) Paridad previa (cuántas marcas había ANTES de la ventana) - before_cnt AS ( - SELECT w.id_usuario, COUNT(*)::int AS cnt - FROM win w - JOIN public.asistencia_raw ar - ON ar.id_usuario = w.id_usuario - AND ar.ts < w.t0 - GROUP BY w.id_usuario - ), - -- 9) Línea de tiempo candidata y pares (1→2, 3→4…), jornada = día local del inicio - timeline AS ( - SELECT - c.id_usuario, - c.ts, - ROW_NUMBER() OVER (PARTITION BY c.id_usuario ORDER BY c.ts) AS rn - FROM candidate c - ), - ready AS ( - SELECT - t1.id_usuario, - (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM timeline t1 - JOIN timeline t2 - ON t2.id_usuario = t1.id_usuario - AND t2.rn = t1.rn + 1 - LEFT JOIN before_cnt b ON b.id_usuario = t1.id_usuario - WHERE ((COALESCE(b.cnt,0) + t1.rn) % 2) = 1 -- t1 es IN global - AND t2.ts > t1.ts - ), - -- 10) INSERT crudo (dedupe) - ins_raw AS ( - INSERT INTO public.asistencia_raw (id_usuario, ts, modo, origen) - SELECT id_usuario, ts_calc, - NULLIF(modo,'')::text, -- puede quedar NULL para auto-etiquetado - p_origen - FROM parsed - ON CONFLICT (id_usuario, ts) DO NOTHING - RETURNING 1 - ), - -- 11) Auto-etiquetar IN/OUT en BD para filas con modo vacío/'1' (tras insertar) - before_cnt2 AS ( - SELECT w.id_usuario, COUNT(*)::int AS cnt - FROM win w - JOIN public.asistencia_raw ar - ON ar.id_usuario = w.id_usuario - AND ar.ts < w.t0 - GROUP BY w.id_usuario - ), - tl2 AS ( - SELECT - ar.id_usuario, ar.ts, - ROW_NUMBER() OVER (PARTITION BY ar.id_usuario ORDER BY ar.ts) AS rn - FROM public.asistencia_raw ar - JOIN win w ON w.id_usuario = ar.id_usuario - AND ar.ts BETWEEN w.t0 AND w.t1 - ), - label2 AS ( - SELECT - t.id_usuario, - t.ts, - CASE WHEN ((COALESCE(b.cnt,0) + t.rn) % 2) = 1 THEN 'IN' ELSE 'OUT' END AS new_mode - FROM tl2 t - LEFT JOIN before_cnt2 b ON b.id_usuario = t.id_usuario - ), - set_mode AS ( - UPDATE public.asistencia_raw ar - SET modo = l.new_mode - FROM label2 l - WHERE ar.id_usuario = l.id_usuario - AND ar.ts = l.ts - AND (ar.modo IS NULL OR btrim(ar.modo) = '' OR ar.modo ~ '^\s*1\s*$') - RETURNING 1 - ), - -- 12) INSERT pares (dedupe) calculados desde CANDIDATE (ya tiene todo el contexto) - ins_pairs AS ( - INSERT INTO public.asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT id_usuario, fecha, desde, hasta, dur_min, p_origen - FROM ready - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT - (SELECT COUNT(*) FROM ins_raw), - (SELECT COUNT(*) FROM ins_pairs), - (SELECT COALESCE(missing,'[]'::jsonb) FROM miss) - INTO v_ins_raw, v_ins_pairs, v_miss; - - RETURN jsonb_build_object( - 'inserted_raw', v_ins_raw, - 'inserted_pairs', v_ins_pairs, - 'missing_docs', v_miss - ); -END; -$_$; - - -ALTER FUNCTION public.import_asistencia(p_registros jsonb, p_origen text, p_tz text) OWNER TO manso; - --- --- Name: report_asistencia(date, date); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.report_asistencia(p_desde date, p_hasta date) RETURNS TABLE(documento text, nombre text, apellido text, fecha date, desde_hora text, hasta_hora text, dur_min numeric) - LANGUAGE sql - AS $$ - SELECT - u.documento, u.nombre, u.apellido, - ai.fecha, - to_char(ai.desde AT TIME ZONE 'America/Montevideo','HH24:MI:SS') AS desde_hora, - to_char(ai.hasta AT TIME ZONE 'America/Montevideo','HH24:MI:SS') AS hasta_hora, - ai.dur_min - FROM public.asistencia_intervalo ai - JOIN public.usuarios u USING (id_usuario) - WHERE ai.fecha BETWEEN p_desde AND p_hasta - ORDER BY u.documento, ai.fecha, ai.desde; -$$; - - -ALTER FUNCTION public.report_asistencia(p_desde date, p_hasta date) OWNER TO manso; - --- --- Name: report_gastos(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.report_gastos(p_year integer) RETURNS jsonb - LANGUAGE sql STABLE - AS $$ -WITH mdata AS ( - SELECT date_trunc('month', c.fec_compra)::date AS m, - SUM(c.total)::numeric AS importe - FROM public.compras c - WHERE EXTRACT(YEAR FROM c.fec_compra) = p_year - GROUP BY 1 -), -mm AS ( - SELECT EXTRACT(MONTH FROM m)::int AS mes, importe - FROM mdata -) -SELECT jsonb_build_object( - 'year', p_year, - 'total', COALESCE((SELECT SUM(importe) FROM mdata), 0), - 'avg', COALESCE((SELECT AVG(importe) FROM mdata), 0), - 'months', - (SELECT jsonb_agg( - jsonb_build_object( - 'mes', gs, - 'nombre', to_char(to_date(gs::text,'MM'),'Mon'), - 'importe', COALESCE(mm.importe,0) - ) - ORDER BY gs - ) - FROM generate_series(1,12) gs - LEFT JOIN mm ON mm.mes = gs) -); -$$; - - -ALTER FUNCTION public.report_gastos(p_year integer) OWNER TO manso; - --- --- Name: report_tickets_year(integer, text); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.report_tickets_year(p_year integer, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE sql STABLE - AS $$ -WITH bounds AS ( - SELECT - make_timestamp(p_year, 1, 1, 0,0,0) AS d0, - make_timestamp(p_year+1, 1, 1, 0,0,0) AS d1, - make_timestamptz(p_year, 1, 1, 0,0,0, p_tz) AS t0, - make_timestamptz(p_year+1, 1, 1, 0,0,0, p_tz) AS t1 -), -base AS ( - SELECT - c.id_comanda, - CASE WHEN c.fec_cierre IS NOT NULL - THEN (c.fec_cierre AT TIME ZONE p_tz) - ELSE c.fec_creacion - END AS fec_local, - v.total - FROM public.comandas c - JOIN public.vw_ticket_total v ON v.id_comanda = c.id_comanda - JOIN bounds b ON TRUE - WHERE - (c.fec_cierre IS NOT NULL AND c.fec_cierre >= b.t0 AND c.fec_cierre < b.t1) - OR - (c.fec_cierre IS NULL AND c.fec_creacion >= b.d0 AND c.fec_creacion < b.d1) -), -m AS ( - SELECT - EXTRACT(MONTH FROM fec_local)::int AS mes, - COUNT(*)::int AS cant, - SUM(total)::numeric AS importe, - AVG(total)::numeric AS avg - FROM base - GROUP BY 1 -), -ytd AS ( - SELECT COUNT(*)::int AS total_ytd, - AVG(total)::numeric AS avg_ticket, - SUM(total)::numeric AS to_date - FROM base -) -SELECT jsonb_build_object( - 'year', p_year, - 'total_ytd', (SELECT total_ytd FROM ytd), - 'avg_ticket', (SELECT avg_ticket FROM ytd), - 'to_date', (SELECT to_date FROM ytd), - 'months', - (SELECT jsonb_agg( - jsonb_build_object( - 'mes', mes, - 'nombre', to_char(to_date(mes::text,'MM'),'Mon'), - 'cant', cant, - 'importe', importe, - 'avg', avg - ) - ORDER BY mes - ) - FROM m) -); -$$; - - -ALTER FUNCTION public.report_tickets_year(p_year integer, p_tz text) OWNER TO manso; - --- --- Name: save_compra(integer, integer, timestamp with time zone, jsonb); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.save_compra(p_id_compra integer, p_id_proveedor integer, p_fec_compra timestamp with time zone, p_detalles jsonb) RETURNS TABLE(id_compra integer, total numeric) - LANGUAGE plpgsql - AS $$ -DECLARE - v_id INT; - v_total numeric := 0; -BEGIN - IF COALESCE(jsonb_array_length(p_detalles),0) = 0 THEN - RAISE EXCEPTION 'No hay renglones en la compra'; - END IF; - - -- Cabecera (insert/update) - IF p_id_compra IS NULL THEN - INSERT INTO public.compras (id_proveedor, fec_compra, total) - VALUES (p_id_proveedor, COALESCE(p_fec_compra, now()), 0) - RETURNING public.compras.id_compra INTO v_id; - ELSE - UPDATE public.compras c - SET id_proveedor = p_id_proveedor, - fec_compra = COALESCE(p_fec_compra, c.fec_compra) - WHERE c.id_compra = p_id_compra - RETURNING c.id_compra INTO v_id; - - -- Reemplazamos los renglones - DELETE FROM public.deta_comp_materias d WHERE d.id_compra = v_id; - DELETE FROM public.deta_comp_producto p WHERE p.id_compra = v_id; - END IF; - - -- Materias primas (sin CTE: parseo JSON inline) - INSERT INTO public.deta_comp_materias (id_compra, id_mat_prima, cantidad, pre_unitario) - SELECT - v_id, - x.id, - x.cantidad, - x.precio - FROM jsonb_to_recordset(COALESCE(p_detalles, '[]'::jsonb)) - AS x(tipo text, id int, cantidad numeric, precio numeric) - WHERE UPPER(TRIM(x.tipo)) = 'MAT'; - - -- Productos (sin CTE) - INSERT INTO public.deta_comp_producto (id_compra, id_producto, cantidad, pre_unitario) - SELECT - v_id, - x.id, - x.cantidad, - x.precio - FROM jsonb_to_recordset(COALESCE(p_detalles, '[]'::jsonb)) - AS x(tipo text, id int, cantidad numeric, precio numeric) - WHERE UPPER(TRIM(x.tipo)) = 'PROD'; - - -- Recalcular total (calificado) y redondear a ENTERO - SELECT - COALESCE( (SELECT SUM(dcm.cantidad*dcm.pre_unitario) - FROM public.deta_comp_materias dcm - WHERE dcm.id_compra = v_id), 0) - + COALESCE( (SELECT SUM(dcp.cantidad*dcp.pre_unitario) - FROM public.deta_comp_producto dcp - WHERE dcp.id_compra = v_id), 0) - INTO v_total; - - UPDATE public.compras c - SET total = round(v_total, 0) - WHERE c.id_compra = v_id; - - RETURN QUERY SELECT v_id, round(v_total, 0); -END; -$$; - - -ALTER FUNCTION public.save_compra(p_id_compra integer, p_id_proveedor integer, p_fec_compra timestamp with time zone, p_detalles jsonb) OWNER TO manso; - --- --- Name: save_materia_prima(integer, text, text, boolean, jsonb); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.save_materia_prima(p_id_mat_prima integer, p_nombre text, p_unidad text, p_activo boolean, p_proveedores jsonb DEFAULT '[]'::jsonb) RETURNS integer - LANGUAGE plpgsql - AS $_$ -DECLARE - v_id INT; -BEGIN - IF p_id_mat_prima IS NULL THEN - INSERT INTO public.mate_primas (nombre, unidad, activo) - VALUES (p_nombre, p_unidad, COALESCE(p_activo, TRUE)) - RETURNING mate_primas.id_mat_prima INTO v_id; - ELSE - UPDATE public.mate_primas mp - SET nombre = p_nombre, - unidad = p_unidad, - activo = COALESCE(p_activo, TRUE) - WHERE mp.id_mat_prima = p_id_mat_prima; - v_id := p_id_mat_prima; - END IF; - - -- Sincronizar proveedores: borrar todos y re-crear a partir de JSONB - DELETE FROM public.prov_mate_prima pmp WHERE pmp.id_mat_prima = v_id; - - INSERT INTO public.prov_mate_prima (id_proveedor, id_mat_prima) - SELECT (e->>0)::INT AS id_proveedor, -- elementos JSON como enteros (array simple) - v_id AS id_mat_prima - FROM jsonb_array_elements(COALESCE(p_proveedores, '[]'::jsonb)) AS e - WHERE (e->>0) ~ '^\d+$'; -- solo enteros - - RETURN v_id; -END; -$_$; - - -ALTER FUNCTION public.save_materia_prima(p_id_mat_prima integer, p_nombre text, p_unidad text, p_activo boolean, p_proveedores jsonb) OWNER TO manso; - --- --- Name: save_producto(integer, text, text, numeric, boolean, integer, jsonb); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION public.save_producto(p_id_producto integer, p_nombre text, p_img_producto text, p_precio numeric, p_activo boolean, p_id_categoria integer, p_receta jsonb DEFAULT '[]'::jsonb) RETURNS integer - LANGUAGE plpgsql - AS $_$ -DECLARE - v_id INT; -BEGIN - IF p_id_producto IS NULL THEN - INSERT INTO public.productos (nombre, img_producto, precio, activo, id_categoria) - VALUES (p_nombre, p_img_producto, p_precio, COALESCE(p_activo, TRUE), p_id_categoria) - RETURNING productos.id_producto INTO v_id; - ELSE - UPDATE public.productos p - SET nombre = p_nombre, - img_producto = p_img_producto, - precio = p_precio, - activo = COALESCE(p_activo, TRUE), - id_categoria = p_id_categoria - WHERE p.id_producto = p_id_producto; - v_id := p_id_producto; - END IF; - - -- Limpia receta actual - DELETE FROM public.receta_producto rp WHERE rp.id_producto = v_id; - - -- Inserta SOLO ítems válidos (id entero positivo y cantidad > 0), redondeo a 3 decimales - INSERT INTO public.receta_producto (id_producto, id_mat_prima, qty_por_unidad) - SELECT - v_id, - (rec->>'id_mat_prima')::INT, - ROUND((rec->>'qty_por_unidad')::NUMERIC, 3) - FROM jsonb_array_elements(COALESCE(p_receta, '[]'::jsonb)) AS rec - WHERE - (rec->>'id_mat_prima') ~ '^\d+$' - AND (rec->>'id_mat_prima')::INT > 0 - AND (rec->>'qty_por_unidad') ~ '^\d+(\.\d+)?$' - AND (rec->>'qty_por_unidad')::NUMERIC > 0; - - RETURN v_id; -END; -$_$; - - -ALTER FUNCTION public.save_producto(p_id_producto integer, p_nombre text, p_img_producto text, p_precio numeric, p_activo boolean, p_id_categoria integer, p_receta jsonb) OWNER TO manso; - --- --- Name: asistencia_intervalo; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.asistencia_intervalo ( - id_intervalo bigint NOT NULL, - id_usuario integer NOT NULL, - fecha date NOT NULL, - desde timestamp with time zone NOT NULL, - hasta timestamp with time zone NOT NULL, - dur_min numeric(10,2) NOT NULL, - origen text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT chk_ai_orden CHECK ((hasta > desde)) -); - - -ALTER TABLE public.asistencia_intervalo OWNER TO manso; - --- --- Name: asistencia_intervalo_id_intervalo_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.asistencia_intervalo_id_intervalo_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.asistencia_intervalo_id_intervalo_seq OWNER TO manso; - --- --- Name: asistencia_intervalo_id_intervalo_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.asistencia_intervalo_id_intervalo_seq OWNED BY public.asistencia_intervalo.id_intervalo; - - --- --- Name: asistencia_raw; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.asistencia_raw ( - id_raw bigint NOT NULL, - id_usuario integer NOT NULL, - ts timestamp with time zone NOT NULL, - modo text, - origen text, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - - -ALTER TABLE public.asistencia_raw OWNER TO manso; - --- --- Name: asistencia_raw_id_raw_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.asistencia_raw_id_raw_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.asistencia_raw_id_raw_seq OWNER TO manso; - --- --- Name: asistencia_raw_id_raw_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.asistencia_raw_id_raw_seq OWNED BY public.asistencia_raw.id_raw; - - --- --- Name: asistencia_resumen_diario; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW public.asistencia_resumen_diario AS - SELECT ai.id_usuario, - u.documento, - u.nombre, - u.apellido, - ai.fecha, - sum(ai.dur_min) AS minutos_dia, - round((sum(ai.dur_min) / 60.0), 2) AS horas_dia, - count(*) AS pares_dia - FROM (public.asistencia_intervalo ai - JOIN public.usuarios u USING (id_usuario)) - GROUP BY ai.id_usuario, u.documento, u.nombre, u.apellido, ai.fecha - ORDER BY ai.id_usuario, ai.fecha; - - -ALTER VIEW public.asistencia_resumen_diario OWNER TO manso; - --- --- Name: categorias; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.categorias ( - id_categoria integer NOT NULL, - nombre text NOT NULL, - visible boolean DEFAULT true -); - - -ALTER TABLE public.categorias OWNER TO manso; - --- --- Name: categorias_id_categoria_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.categorias_id_categoria_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.categorias_id_categoria_seq OWNER TO manso; - --- --- Name: categorias_id_categoria_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.categorias_id_categoria_seq OWNED BY public.categorias.id_categoria; - - --- --- Name: clientes; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.clientes ( - id_cliente integer NOT NULL, - nombre text NOT NULL, - correo text, - telefono text, - fec_nacimiento date, - activo boolean DEFAULT true -); - - -ALTER TABLE public.clientes OWNER TO manso; - --- --- Name: clientes_id_cliente_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.clientes_id_cliente_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.clientes_id_cliente_seq OWNER TO manso; - --- --- Name: clientes_id_cliente_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.clientes_id_cliente_seq OWNED BY public.clientes.id_cliente; - - --- --- Name: comandas_id_comanda_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.comandas_id_comanda_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.comandas_id_comanda_seq OWNER TO manso; - --- --- Name: comandas_id_comanda_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.comandas_id_comanda_seq OWNED BY public.comandas.id_comanda; - - --- --- Name: compras; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.compras ( - id_compra integer NOT NULL, - id_proveedor integer NOT NULL, - fec_compra timestamp without time zone NOT NULL, - total numeric(14,2) -); - - -ALTER TABLE public.compras OWNER TO manso; - --- --- Name: compras_id_compra_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.compras_id_compra_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.compras_id_compra_seq OWNER TO manso; - --- --- Name: compras_id_compra_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.compras_id_compra_seq OWNED BY public.compras.id_compra; - - --- --- Name: deta_comandas_id_det_comanda_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.deta_comandas_id_det_comanda_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.deta_comandas_id_det_comanda_seq OWNER TO manso; - --- --- Name: deta_comandas_id_det_comanda_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.deta_comandas_id_det_comanda_seq OWNED BY public.deta_comandas.id_det_comanda; - - --- --- Name: deta_comp_materias; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.deta_comp_materias ( - id_compra integer NOT NULL, - id_mat_prima integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - CONSTRAINT deta_comp_materias_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comp_materias_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - - -ALTER TABLE public.deta_comp_materias OWNER TO manso; - --- --- Name: deta_comp_producto; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.deta_comp_producto ( - id_compra integer NOT NULL, - id_producto integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - CONSTRAINT deta_comp_producto_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comp_producto_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - - -ALTER TABLE public.deta_comp_producto OWNER TO manso; - --- --- Name: mate_primas; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.mate_primas ( - id_mat_prima integer NOT NULL, - nombre text NOT NULL, - unidad text NOT NULL, - activo boolean DEFAULT true -); - - -ALTER TABLE public.mate_primas OWNER TO manso; - --- --- Name: mate_primas_id_mat_prima_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.mate_primas_id_mat_prima_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.mate_primas_id_mat_prima_seq OWNER TO manso; - --- --- Name: mate_primas_id_mat_prima_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.mate_primas_id_mat_prima_seq OWNED BY public.mate_primas.id_mat_prima; - - --- --- Name: mesas_id_mesa_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.mesas_id_mesa_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.mesas_id_mesa_seq OWNER TO manso; - --- --- Name: mesas_id_mesa_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.mesas_id_mesa_seq OWNED BY public.mesas.id_mesa; - - --- --- Name: productos; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.productos ( - id_producto integer NOT NULL, - nombre text NOT NULL, - img_producto character varying(255) DEFAULT 'img/productos/img_producto.png'::character varying NOT NULL, - precio integer NOT NULL, - activo boolean DEFAULT true, - id_categoria integer NOT NULL, - CONSTRAINT productos_precio_check CHECK (((precio)::numeric >= (0)::numeric)), - CONSTRAINT productos_precio_nn CHECK ((precio >= 0)) -); - - -ALTER TABLE public.productos OWNER TO manso; - --- --- Name: productos_id_producto_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.productos_id_producto_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.productos_id_producto_seq OWNER TO manso; - --- --- Name: productos_id_producto_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.productos_id_producto_seq OWNED BY public.productos.id_producto; - - --- --- Name: prov_mate_prima; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.prov_mate_prima ( - id_proveedor integer NOT NULL, - id_mat_prima integer NOT NULL -); - - -ALTER TABLE public.prov_mate_prima OWNER TO manso; - --- --- Name: prov_producto; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.prov_producto ( - id_proveedor integer NOT NULL, - id_producto integer NOT NULL -); - - -ALTER TABLE public.prov_producto OWNER TO manso; - --- --- Name: proveedores; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.proveedores ( - id_proveedor integer NOT NULL, - rut text, - raz_social text NOT NULL, - direccion text, - contacto text -); - - -ALTER TABLE public.proveedores OWNER TO manso; - --- --- Name: proveedores_id_proveedor_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.proveedores_id_proveedor_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.proveedores_id_proveedor_seq OWNER TO manso; - --- --- Name: proveedores_id_proveedor_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.proveedores_id_proveedor_seq OWNED BY public.proveedores.id_proveedor; - - --- --- Name: receta_producto; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.receta_producto ( - id_producto integer NOT NULL, - id_mat_prima integer NOT NULL, - qty_por_unidad numeric(12,3) NOT NULL, - CONSTRAINT receta_producto_qty_por_unidad_check CHECK ((qty_por_unidad > (0)::numeric)) -); - - -ALTER TABLE public.receta_producto OWNER TO manso; - --- --- Name: roles; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.roles ( - id_rol integer NOT NULL, - nombre text NOT NULL -); - - -ALTER TABLE public.roles OWNER TO manso; - --- --- Name: roles_id_rol_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.roles_id_rol_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.roles_id_rol_seq OWNER TO manso; - --- --- Name: roles_id_rol_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.roles_id_rol_seq OWNED BY public.roles.id_rol; - - --- --- Name: usua_roles; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE public.usua_roles ( - id_usuario integer NOT NULL, - id_rol integer NOT NULL, - fec_asignacion timestamp without time zone DEFAULT now(), - autor integer, - activo boolean DEFAULT true -); - - -ALTER TABLE public.usua_roles OWNER TO manso; - --- --- Name: usuarios_id_usuario_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE public.usuarios_id_usuario_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.usuarios_id_usuario_seq OWNER TO manso; - --- --- Name: usuarios_id_usuario_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE public.usuarios_id_usuario_seq OWNED BY public.usuarios.id_usuario; - - --- --- Name: v_comandas_detalle_base; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW public.v_comandas_detalle_base AS - SELECT c.id_comanda, - c.fec_creacion, - c.fec_cierre, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM ((((public.comandas c - JOIN public.usuarios u ON ((u.id_usuario = c.id_usuario))) - JOIN public.mesas m ON ((m.id_mesa = c.id_mesa))) - LEFT JOIN public.deta_comandas d ON ((d.id_comanda = c.id_comanda))) - LEFT JOIN public.productos p ON ((p.id_producto = d.id_producto))); - - -ALTER VIEW public.v_comandas_detalle_base OWNER TO manso; - --- --- Name: v_comandas_detalle_items; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW public.v_comandas_detalle_items AS - SELECT d.id_comanda, - d.id_det_comanda, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal, - d.observaciones - FROM (public.deta_comandas d - JOIN public.productos p ON ((p.id_producto = d.id_producto))); - - -ALTER VIEW public.v_comandas_detalle_items OWNER TO manso; - --- --- Name: v_comandas_detalle_json; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW public.v_comandas_detalle_json AS - SELECT id_comanda, - jsonb_build_object('id_comanda', id_comanda, 'fec_creacion', fec_creacion, 'fec_cierre', fec_cierre, 'estado', estado, 'observaciones', observaciones, 'usuario', jsonb_build_object('id_usuario', id_usuario, 'nombre', usuario_nombre, 'apellido', usuario_apellido), 'mesa', jsonb_build_object('id_mesa', id_mesa, 'numero', mesa_numero, 'apodo', mesa_apodo), 'items', COALESCE(( SELECT jsonb_agg(jsonb_build_object('producto_id', b.id_producto, 'producto', b.producto_nombre, 'cantidad', b.cantidad, 'pre_unitario', b.pre_unitario, 'subtotal', b.subtotal) ORDER BY b.producto_nombre) AS jsonb_agg - FROM public.v_comandas_detalle_base b - WHERE ((b.id_comanda = h.id_comanda) AND (b.id_producto IS NOT NULL))), '[]'::jsonb), 'totales', jsonb_build_object('items', COALESCE(( SELECT count(*) AS count - FROM public.v_comandas_detalle_base b - WHERE ((b.id_comanda = h.id_comanda) AND (b.id_producto IS NOT NULL))), (0)::bigint), 'total', COALESCE(( SELECT sum(b.subtotal) AS sum - FROM public.v_comandas_detalle_base b - WHERE (b.id_comanda = h.id_comanda)), (0)::numeric))) AS data - FROM ( SELECT DISTINCT v_comandas_detalle_base.id_comanda, - v_comandas_detalle_base.fec_creacion, - v_comandas_detalle_base.fec_cierre, - v_comandas_detalle_base.estado, - v_comandas_detalle_base.observaciones, - v_comandas_detalle_base.id_usuario, - v_comandas_detalle_base.usuario_nombre, - v_comandas_detalle_base.usuario_apellido, - v_comandas_detalle_base.id_mesa, - v_comandas_detalle_base.mesa_numero, - v_comandas_detalle_base.mesa_apodo - FROM public.v_comandas_detalle_base) h; - - -ALTER VIEW public.v_comandas_detalle_json OWNER TO manso; - --- --- Name: vw_compras; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW public.vw_compras AS - SELECT c.id_compra, - c.id_proveedor, - p.raz_social AS proveedor, - c.fec_compra, - c.total - FROM (public.compras c - JOIN public.proveedores p USING (id_proveedor)) - ORDER BY c.fec_compra DESC, c.id_compra DESC; - - -ALTER VIEW public.vw_compras OWNER TO manso; - --- --- Name: vw_ticket_total; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW public.vw_ticket_total AS - WITH lineas AS ( - SELECT c.id_comanda, - COALESCE(c.fec_cierre, (c.fec_creacion)::timestamp with time zone) AS fec_ticket, - (COALESCE(dc.pre_unitario, (p.precio)::numeric, (0)::numeric))::numeric(14,2) AS pu, - (COALESCE(dc.cantidad, (1)::numeric))::numeric(14,3) AS qty - FROM ((public.comandas c - JOIN public.deta_comandas dc ON ((dc.id_comanda = c.id_comanda))) - LEFT JOIN public.productos p ON ((p.id_producto = dc.id_producto))) - ) - SELECT id_comanda, - fec_ticket, - (sum((qty * pu)))::numeric(14,2) AS total - FROM lineas - GROUP BY id_comanda, fec_ticket; - - -ALTER VIEW public.vw_ticket_total OWNER TO manso; - --- --- Name: asistencia_intervalo id_intervalo; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.asistencia_intervalo ALTER COLUMN id_intervalo SET DEFAULT nextval('public.asistencia_intervalo_id_intervalo_seq'::regclass); - - --- --- Name: asistencia_raw id_raw; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.asistencia_raw ALTER COLUMN id_raw SET DEFAULT nextval('public.asistencia_raw_id_raw_seq'::regclass); - - --- --- Name: categorias id_categoria; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.categorias ALTER COLUMN id_categoria SET DEFAULT nextval('public.categorias_id_categoria_seq'::regclass); - - --- --- Name: clientes id_cliente; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.clientes ALTER COLUMN id_cliente SET DEFAULT nextval('public.clientes_id_cliente_seq'::regclass); - - --- --- Name: comandas id_comanda; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.comandas ALTER COLUMN id_comanda SET DEFAULT nextval('public.comandas_id_comanda_seq'::regclass); - - --- --- Name: compras id_compra; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.compras ALTER COLUMN id_compra SET DEFAULT nextval('public.compras_id_compra_seq'::regclass); - - --- --- Name: deta_comandas id_det_comanda; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.deta_comandas ALTER COLUMN id_det_comanda SET DEFAULT nextval('public.deta_comandas_id_det_comanda_seq'::regclass); - - --- --- Name: mate_primas id_mat_prima; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.mate_primas ALTER COLUMN id_mat_prima SET DEFAULT nextval('public.mate_primas_id_mat_prima_seq'::regclass); - - --- --- Name: mesas id_mesa; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.mesas ALTER COLUMN id_mesa SET DEFAULT nextval('public.mesas_id_mesa_seq'::regclass); - - --- --- Name: productos id_producto; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.productos ALTER COLUMN id_producto SET DEFAULT nextval('public.productos_id_producto_seq'::regclass); - - --- --- Name: proveedores id_proveedor; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.proveedores ALTER COLUMN id_proveedor SET DEFAULT nextval('public.proveedores_id_proveedor_seq'::regclass); - - --- --- Name: roles id_rol; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.roles ALTER COLUMN id_rol SET DEFAULT nextval('public.roles_id_rol_seq'::regclass); - - --- --- Name: usuarios id_usuario; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.usuarios ALTER COLUMN id_usuario SET DEFAULT nextval('public.usuarios_id_usuario_seq'::regclass); - - --- --- Data for Name: asistencia_intervalo; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.asistencia_intervalo (id_intervalo, id_usuario, fecha, desde, hasta, dur_min, origen, created_at) FROM stdin; -83 1 2025-08-29 2025-08-30 01:19:38+00 2025-08-30 01:26:19+00 6.68 delete_adjust 2025-08-30 04:42:43.597798+00 -84 1 2025-08-29 2025-08-30 02:30:00+00 2025-08-30 02:46:40+00 16.67 delete_adjust 2025-08-30 04:42:43.597798+00 -85 1 2025-08-30 2025-08-30 03:13:31+00 2025-08-30 03:36:03+00 22.53 delete_adjust 2025-08-30 04:42:43.597798+00 -86 1 2025-08-30 2025-08-30 04:10:00+00 2025-08-30 04:12:00+00 2.00 delete_adjust 2025-08-30 04:42:43.597798+00 -87 1 2025-08-30 2025-08-30 04:24:08+00 2025-08-30 04:38:56+00 14.80 delete_adjust 2025-08-30 04:42:43.597798+00 -88 1 2025-08-30 2025-08-30 05:01:55+00 2025-08-30 05:10:00+00 8.08 delete_adjust 2025-08-30 04:42:43.597798+00 -89 1 2025-08-27 2025-08-27 04:34:55+00 2025-08-27 06:35:08+00 120.22 AGL_001.txt 2025-08-30 04:43:13.749738+00 -90 1 2025-08-29 2025-08-30 00:12:34+00 2025-08-30 00:47:24+00 34.83 AGL_001.txt 2025-08-30 04:43:13.749738+00 -97 2 2025-01-02 2025-01-02 12:12:00+00 2025-01-02 14:48:00+00 156.00 manual_form 2025-08-30 04:45:59.234439+00 -99 2 2025-01-02 2025-01-02 20:50:00+00 2025-01-03 02:48:00+00 358.00 manual_form 2025-08-30 04:46:45.672304+00 -108 1 2025-01-01 2025-01-01 10:00:00+00 2025-01-01 16:00:00+00 360.00 manual_form 2025-09-01 21:03:36.046072+00 -\. - - --- --- Data for Name: asistencia_raw; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.asistencia_raw (id_raw, id_usuario, ts, modo, origen, created_at) FROM stdin; -1 1 2025-08-30 05:10:00+00 OUT manual_form 2025-08-30 04:11:08.227836+00 -2 1 2025-08-30 04:10:00+00 IN manual_form 2025-08-30 04:11:08.227836+00 -3 1 2025-08-30 04:12:00+00 OUT manual_form 2025-08-30 04:12:30.456958+00 -4 1 2025-08-30 02:30:00+00 IN manual_form 2025-08-30 04:12:30.456958+00 -22 1 2025-08-30 01:19:38+00 IN AGL_001.txt 2025-08-30 04:32:09.754679+00 -21 1 2025-08-30 01:26:19+00 OUT AGL_001.txt 2025-08-30 04:32:09.754679+00 -20 1 2025-08-30 02:46:40+00 OUT AGL_001.txt 2025-08-30 04:32:09.754679+00 -19 1 2025-08-30 03:13:31+00 IN AGL_001.txt 2025-08-30 04:32:09.754679+00 -18 1 2025-08-30 03:36:03+00 OUT AGL_001.txt 2025-08-30 04:32:09.754679+00 -17 1 2025-08-30 04:38:56+00 OUT AGL_001.txt 2025-08-30 04:32:09.754679+00 -16 1 2025-08-30 05:01:55+00 IN AGL_001.txt 2025-08-30 04:32:09.754679+00 -15 1 2025-08-30 04:24:08+00 IN AGL_001.txt 2025-08-30 04:32:09.754679+00 -71 2 2025-01-02 14:48:00+00 OUT manual_form 2025-08-30 04:45:59.234439+00 -72 2 2025-01-02 12:12:00+00 IN manual_form 2025-08-30 04:45:59.234439+00 -73 2 2025-01-03 02:48:00+00 OUT manual_form 2025-08-30 04:46:45.672304+00 -74 2 2025-01-02 20:50:00+00 IN manual_form 2025-08-30 04:46:45.672304+00 -60 1 2025-08-27 04:34:55+00 IN AGL_001.txt 2025-08-30 04:43:13.749738+00 -59 1 2025-08-27 06:35:08+00 OUT AGL_001.txt 2025-08-30 04:43:13.749738+00 -70 1 2025-08-30 00:12:34+00 IN AGL_001.txt 2025-08-30 04:43:13.749738+00 -69 1 2025-08-30 00:47:24+00 OUT AGL_001.txt 2025-08-30 04:43:13.749738+00 -87 1 2025-01-01 16:00:00+00 OUT manual_form 2025-09-01 21:03:36.046072+00 -88 1 2025-01-01 10:00:00+00 IN manual_form 2025-09-01 21:03:36.046072+00 -\. - - --- --- Data for Name: categorias; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.categorias (id_categoria, nombre, visible) FROM stdin; -1 Cafetería t -2 Café t -3 Bar t -4 Tragos y Refrescos t -\. - - --- --- Data for Name: clientes; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.clientes (id_cliente, nombre, correo, telefono, fec_nacimiento, activo) FROM stdin; -1 Familia \N \N \N t -\. - - --- --- Data for Name: comandas; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.comandas (id_comanda, id_usuario, id_mesa, fec_creacion, estado, observaciones, fec_cierre) FROM stdin; -2 3 14 2025-08-25 18:47:57.398972 cerrada \N \N -49 1 14 2025-08-29 16:53:42.617246 cerrada Auto blanco 2025-08-29 16:56:44.184737+00 -4 1 1 2025-08-25 19:07:51.695426 cerrada Pedido para una familia grande. \N -3 1 1 2025-08-25 19:07:37.584356 cerrada Pedido para una familia grande. \N -1 3 14 2025-08-25 18:46:18.834688 cerrada \N \N -48 3 14 2025-08-29 16:53:06.77149 cerrada Grande huevo 2025-08-29 16:56:45.225257+00 -47 3 14 2025-08-29 16:52:22.556665 cerrada Grande huevo 2025-08-29 16:56:46.440835+00 -8 3 14 2025-08-29 02:57:22.46956 cerrada \N 2025-08-29 03:47:50.750949+00 -7 1 14 2025-08-29 02:56:29.755449 cerrada \N 2025-08-29 04:31:33.254769+00 -6 2 6 2025-08-25 19:09:25.280339 cerrada \N 2025-08-29 04:32:29.813119+00 -46 3 14 2025-08-29 16:51:23.011327 cerrada Grande huevo 2025-08-29 16:56:47.457248+00 -5 1 1 2025-08-25 19:08:08.596438 cerrada \N 2025-08-29 04:35:24.52745+00 -10 1 3 2025-08-29 04:41:51.354916 cerrada \N 2025-08-29 04:48:01.548441+00 -12 3 14 2025-08-29 04:48:53.292023 cerrada \N 2025-08-29 05:17:41.133298+00 -19 3 14 2025-08-29 05:49:56.258621 cerrada \N 2025-08-29 05:50:10.286428+00 -18 3 14 2025-08-29 05:49:13.120391 cerrada \N 2025-08-29 05:50:11.319667+00 -17 3 14 2025-08-29 05:46:54.548073 cerrada oBSERVACIOOONNN 2025-08-29 05:50:12.331972+00 -16 2 8 2025-08-29 05:46:23.763257 cerrada oBSERVACIOOONNN 2025-08-29 05:50:13.37213+00 -15 2 8 2025-08-29 05:46:18.69042 cerrada oBSERVACIOOONNN 2025-08-29 05:50:14.334797+00 -14 3 14 2025-08-29 05:19:28.908216 cerrada jdwkjklqwndv 2025-08-29 05:50:15.26329+00 -13 3 14 2025-08-29 04:58:36.159791 cerrada hola 2025-08-29 05:50:16.511989+00 -11 1 4 2025-08-29 04:48:36.541902 cerrada \N 2025-08-29 05:50:17.832327+00 -9 1 1 2025-08-29 04:37:56.310221 cerrada Olaaa 2025-08-29 05:50:19.084017+00 -20 3 14 2025-08-29 05:50:25.106438 cerrada \N 2025-08-29 05:50:30.321838+00 -29 2 6 2025-08-29 06:06:44.812528 cerrada \N 2025-08-29 06:08:16.181067+00 -28 3 14 2025-08-29 06:06:13.297627 cerrada \N 2025-08-29 06:08:17.69094+00 -27 1 1 2025-08-29 06:03:10.757812 cerrada \N 2025-08-29 06:08:18.986658+00 -26 1 8 2025-08-29 06:02:25.460776 cerrada \N 2025-08-29 06:08:20.296605+00 -25 1 8 2025-08-29 06:01:26.571144 cerrada Sin gluten 2025-08-29 06:08:22.898867+00 -24 1 1 2025-08-29 05:58:17.922202 cerrada \N 2025-08-29 06:08:24.187839+00 -23 1 1 2025-08-29 05:57:35.418369 cerrada Observacionesssqaishfoiadhfohsdf 2025-08-29 06:08:25.558933+00 -22 3 14 2025-08-29 05:54:44.675905 cerrada \N 2025-08-29 06:08:27.55245+00 -21 3 14 2025-08-29 05:51:54.451937 cerrada \N 2025-08-29 06:08:29.094665+00 -31 1 14 2025-08-29 16:10:31.619565 cerrada TestObs,.-.-. 2025-08-29 16:13:11.693159+00 -30 1 4 2025-08-29 14:46:00.062522 cerrada 123 2025-08-29 16:13:14.63311+00 -52 1 12 2025-08-29 16:55:21.360321 cerrada grande el huevo 2025-08-29 16:56:40.145463+00 -51 1 14 2025-08-29 16:54:44.410546 cerrada auto blanco 2025-08-29 16:56:41.279442+00 -50 1 14 2025-08-29 16:54:08.307324 cerrada auto blanco 2025-08-29 16:56:42.968122+00 -45 3 14 2025-08-29 16:51:08.371592 cerrada Grande huevo 2025-08-29 16:56:48.831883+00 -44 3 14 2025-08-29 16:50:54.483409 cerrada Grande huevo 2025-08-29 16:56:49.85665+00 -43 1 14 2025-08-29 16:50:11.083248 cerrada Grande huevo 2025-08-29 16:56:51.165809+00 -41 1 13 2025-08-29 16:27:13.691181 cerrada Mesa normal 2025-08-29 16:56:52.301133+00 -42 3 12 2025-08-29 16:27:47.766143 cerrada Mesa normal 2025-08-29 16:56:53.405482+00 -40 1 14 2025-08-29 16:26:43.787724 cerrada Observación huevo 2025-08-29 16:56:54.62857+00 -39 3 14 2025-08-29 16:26:09.754868 cerrada Observación del huevo 2025-08-29 16:56:55.865184+00 -38 3 14 2025-08-29 16:25:43.366429 cerrada Observación del huevo 2025-08-29 16:56:57.13223+00 -37 1 14 2025-08-29 16:25:01.413168 cerrada Observación del huevo 2025-08-29 16:56:58.590021+00 -36 1 14 2025-08-29 16:24:23.587871 cerrada Observación del huevo 2025-08-29 16:56:59.863697+00 -35 3 14 2025-08-29 16:23:33.26689 cerrada Observación del huevo 2025-08-29 16:57:01.082691+00 -34 3 14 2025-08-29 16:23:27.385911 cerrada Observación del huevo 2025-08-29 16:57:02.429882+00 -33 1 5 2025-08-29 16:16:28.527735 cerrada Esta es una observación 2025-08-29 16:57:03.761533+00 -32 1 5 2025-08-29 16:15:54.377397 cerrada Esta es una observación 2025-08-29 16:57:05.939418+00 -53 3 8 2025-09-01 20:21:27.553491 abierta \N \N -54 3 14 2025-09-01 21:16:30.760241 abierta Ana \N -55 3 11 2025-09-02 00:22:18.600045 abierta \N \N -\. - - --- --- Data for Name: compras; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.compras (id_compra, id_proveedor, fec_compra, total) FROM stdin; -36 4 2025-07-29 08:02:33.489822 3806.00 -37 1 2025-06-28 06:27:33.489822 2944.00 -38 2 2025-08-22 22:23:33.489822 2866.00 -39 2 2025-07-27 10:50:33.489822 7774.00 -40 4 2025-08-03 21:28:33.489822 2373.00 -41 1 2025-06-13 08:23:33.489822 1556.00 -42 4 2025-07-29 02:30:33.489822 5941.00 -43 4 2025-07-27 04:39:33.489822 3570.00 -44 4 2025-07-10 04:43:33.489822 2648.00 -45 1 2025-07-05 00:59:33.489822 11349.00 -46 3 2025-07-04 18:17:33.489822 1671.00 -47 3 2025-07-11 02:42:33.489822 2423.00 -48 3 2025-07-16 14:47:33.489822 7851.00 -49 3 2025-07-21 01:24:33.489822 4888.00 -50 1 2025-06-05 13:46:33.489822 4985.00 -51 3 2025-08-02 20:32:33.489822 144.00 -52 1 2025-07-27 10:08:33.489822 1840.00 -53 2 2025-08-22 08:01:33.489822 3398.00 -55 2 2025-07-14 10:16:33.489822 632.00 -56 2 2025-07-14 20:17:33.489822 6882.00 -57 1 2025-06-17 15:01:33.489822 2974.00 -58 2 2025-06-19 15:29:33.489822 2644.00 -60 2 2025-06-09 15:01:33.489822 1436.00 -61 3 2025-08-09 00:54:33.489822 6453.00 -62 1 2025-08-26 16:17:33.489822 5450.00 -63 1 2025-08-05 08:39:33.489822 8873.00 -64 1 2025-07-19 12:38:33.489822 4093.00 -65 1 2025-06-23 09:31:33.489822 666.00 -59 1 2025-08-29 16:42:00 3248.00 -54 2 2025-08-26 04:37:00 2483.00 -\. - - --- --- Data for Name: deta_comandas; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.deta_comandas (id_det_comanda, id_comanda, id_producto, cantidad, pre_unitario, observaciones) FROM stdin; -1 1 52 4.000 130.00 \N -2 2 40 1.000 250.00 \N -3 2 49 1.000 230.00 \N -4 2 24 3.000 100.00 \N -5 6 51 5.000 230.00 \N -6 6 33 2.000 550.00 \N -7 7 52 3.000 130.00 \N -8 8 33 3.000 550.00 \N -9 9 37 3.000 320.00 \N -10 10 52 1.000 130.00 \N -11 11 47 3.000 280.00 \N -12 11 43 1.000 230.00 \N -13 11 45 1.000 280.00 \N -14 11 44 1.000 150.00 \N -15 11 24 2.000 100.00 \N -16 12 28 1.000 100.00 \N -17 12 31 13.000 100.00 \N -18 13 51 2.000 230.00 \N -19 13 52 2.000 130.00 \N -20 14 52 1.000 130.00 \N -21 15 52 1.000 130.00 \N -22 15 51 1.000 230.00 \N -23 16 51 1.000 230.00 \N -24 16 52 1.000 130.00 \N -25 17 46 1.000 150.00 \N -26 18 47 1.000 280.00 \N -27 19 51 1.000 230.00 \N -28 19 48 1.000 230.00 \N -29 20 52 1.000 130.00 \N -30 20 49 1.000 230.00 \N -31 20 51 1.000 230.00 \N -32 22 49 1.000 230.00 \N -33 23 52 1.000 130.00 \N -34 23 48 1.000 230.00 \N -35 23 51 1.000 230.00 \N -36 23 50 1.000 230.00 \N -37 24 52 1.000 130.00 \N -38 24 51 1.000 230.00 \N -39 25 47 1.000 280.00 \N -40 25 49 1.000 230.00 \N -41 25 50 1.000 230.00 \N -42 26 50 1.000 230.00 \N -43 26 49 1.000 230.00 \N -44 27 50 3.000 230.00 \N -45 27 49 3.000 230.00 \N -46 28 51 1.000 230.00 \N -47 28 52 1.000 130.00 \N -48 28 45 1.000 280.00 \N -49 28 50 1.000 230.00 \N -50 29 48 2.000 230.00 \N -51 29 49 6.000 230.00 \N -52 29 47 2.000 280.00 \N -54 30 20 3.000 140.00 \N -53 30 16 1.000 100.00 \N -55 30 17 1.000 150.00 \N -56 31 51 1.000 230.00 \N -57 31 23 1.000 180.00 \N -58 31 52 2.000 140.00 \N -59 31 33 1.000 550.00 \N -60 33 9 2.000 120.00 \N -61 33 18 1.000 80.00 \N -62 33 2 1.000 60.00 \N -63 34 3 1.000 60.00 \N -64 34 21 1.000 180.00 \N -65 34 33 1.000 550.00 \N -66 34 23 2.000 180.00 \N -67 36 33 1.000 550.00 \N -68 36 49 1.000 230.00 \N -69 36 3 1.000 60.00 \N -70 36 6 1.000 50.00 \N -71 36 11 1.000 120.00 \N -72 37 3 1.000 60.00 \N -73 37 6 1.000 50.00 \N -74 37 49 1.000 230.00 \N -75 38 38 4.000 250.00 \N -76 38 33 1.000 550.00 \N -77 38 50 1.000 230.00 \N -78 38 10 1.000 120.00 \N -79 38 52 1.000 140.00 \N -80 38 9 1.000 120.00 \N -81 39 33 1.000 550.00 \N -82 39 38 4.000 250.00 \N -83 40 52 1.000 140.00 \N -84 40 29 1.000 80.00 \N -85 40 46 1.000 150.00 \N -86 40 49 1.000 230.00 \N -87 40 10 1.000 120.00 \N -88 40 11 6.000 120.00 \N -89 41 4 1.000 250.00 \N -90 41 8 1.000 120.00 \N -91 42 46 1.000 150.00 \N -92 42 49 1.000 230.00 \N -93 42 44 1.000 150.00 \N -94 43 33 1.000 550.00 \N -95 43 25 1.000 180.00 \N -96 43 16 1.000 100.00 \N -97 43 24 1.000 100.00 \N -98 43 47 2.000 280.00 \N -99 44 46 1.000 150.00 \N -100 45 46 1.000 150.00 \N -101 46 46 1.000 150.00 \N -102 47 51 1.000 230.00 \N -103 48 50 1.000 230.00 \N -104 49 47 1.000 280.00 \N -105 49 33 2.000 550.00 \N -106 51 33 1.000 550.00 \N -107 51 47 1.000 280.00 \N -108 52 24 1.000 100.00 \N -109 52 25 1.000 180.00 \N -110 52 16 1.000 100.00 \N -111 53 52 10.000 130.00 \N -112 53 23 11.000 180.00 \N -113 53 33 10.000 550.00 \N -114 54 50 2.000 230.00 \N -115 54 52 2.000 130.00 \N -116 55 33 2.000 550.00 \N -117 55 52 2.000 130.00 \N -\. - - --- --- Data for Name: deta_comp_materias; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.deta_comp_materias (id_compra, id_mat_prima, cantidad, pre_unitario) FROM stdin; -36 5 1.019 322.00 -36 9 1.370 80.00 -36 10 5.755 232.00 -38 3 7.311 392.00 -39 1 6.840 447.00 -39 10 7.533 397.00 -40 9 2.636 283.00 -40 8 8.262 151.00 -41 7 3.794 410.00 -43 3 5.017 293.00 -44 4 8.652 306.00 -45 6 9.781 426.00 -46 7 3.017 422.00 -48 1 0.735 511.00 -48 3 5.249 373.00 -49 1 9.816 498.00 -50 1 2.517 459.00 -50 7 2.785 239.00 -50 2 3.788 392.00 -52 6 7.542 244.00 -53 3 4.332 522.00 -55 6 4.937 128.00 -56 5 6.062 45.00 -57 9 1.806 465.00 -57 5 4.283 275.00 -57 7 1.861 402.00 -57 3 4.002 52.00 -58 1 3.379 471.00 -60 2 0.588 216.00 -61 10 13.011 327.00 -62 10 4.719 405.00 -62 9 4.646 295.00 -62 1 3.242 338.00 -63 6 8.843 457.00 -63 7 6.086 270.00 -63 8 6.852 394.00 -63 1 1.678 147.00 -65 1 4.788 139.00 -59 9 1.000 590.00 -54 3 2.000 157.00 -54 6 2.000 376.00 -54 9 4.000 67.00 -\. - - --- --- Data for Name: deta_comp_producto; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.deta_comp_producto (id_compra, id_producto, cantidad, pre_unitario) FROM stdin; -36 31 8.500 35.00 -36 33 8.224 211.00 -37 19 2.839 238.00 -37 51 5.961 162.00 -37 36 8.570 152.00 -39 12 2.329 515.00 -39 2 2.859 184.00 -40 13 1.521 63.00 -40 2 2.897 98.00 -42 50 8.152 516.00 -42 15 8.067 215.00 -43 26 2.209 314.00 -43 14 5.892 179.00 -43 33 2.023 174.00 -45 40 9.248 434.00 -45 27 5.979 530.00 -46 46 2.919 87.00 -46 32 0.658 219.00 -47 17 7.792 225.00 -47 34 2.923 229.00 -48 2 8.937 268.00 -48 14 8.415 371.00 -50 9 1.573 516.00 -50 14 1.940 447.00 -51 41 1.598 90.00 -53 40 1.525 416.00 -53 38 2.629 191.00 -56 1 5.752 412.00 -56 42 9.845 378.00 -56 7 2.073 250.00 -58 14 6.745 156.00 -60 34 9.698 135.00 -61 26 3.828 460.00 -61 22 1.325 330.00 -62 8 8.369 105.00 -62 27 0.812 238.00 -63 13 0.730 332.00 -64 46 9.524 257.00 -64 22 1.355 431.00 -64 47 6.969 99.00 -64 43 2.123 175.00 -59 39 6.000 443.00 -54 16 3.000 383.00 -\. - - --- --- Data for Name: mate_primas; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.mate_primas (id_mat_prima, nombre, unidad, activo) FROM stdin; -2 Huevo u t -3 Harina gr t -4 Avena gr t -1 Capsulas u t -9 Queso Azul San Ignacio gr t -10 Panceta gr t -8 Sal gr t -7 Sobres de Azucar u t -6 Azucar gr t -5 Bondiola cocida gr t -\. - - --- --- Data for Name: mesas; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.mesas (id_mesa, numero, apodo, estado) FROM stdin; -1 1 Living princial libre -2 2 Ventanal izquierdo libre -3 3 Primer mesa contra la baranda libre -4 4 Ventanal derecho libre -5 5 Segunda mesa contra la baranda libre -6 6 Junto a Juana libre -7 7 Mostrador/Barra libre -8 8 Booth derecho libre -9 9 Booth izquierdo libre -10 10 Living secundario libre -11 11 Zona del Pool libre -12 12 Cowork libre -13 13 Mesa del fuego libre -14 14 Takeaway libre -\. - - --- --- Data for Name: productos; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.productos (id_producto, nombre, img_producto, precio, activo, id_categoria) FROM stdin; -17 Cheesecake img/productos/img_producto.png 250 t 1 -2 Alfajores de Maicena img/productos/img_producto.png 60 t 1 -3 Alfajores sin TACC img/productos/img_producto.png 60 t 1 -4 Sandwich tostado LyC img/productos/img_producto.png 250 t 1 -5 Sandwich tostado JyQ img/productos/img_producto.png 250 t 1 -6 Scones img/productos/img_producto.png 50 t 1 -7 Medialunas img/productos/img_producto.png 80 t 1 -8 Medialunas Rellenas img/productos/img_producto.png 120 t 1 -9 Cookies de Avena img/productos/img_producto.png 120 t 1 -10 Cookies de chocolate img/productos/img_producto.png 120 t 1 -11 Brownies con helado img/productos/img_producto.png 120 t 1 -12 Budín de banana img/productos/img_producto.png 150 t 1 -13 Budín de naranja img/productos/img_producto.png 150 t 1 -14 Tostadas img/productos/img_producto.png 100 t 1 -15 Tarteletas img/productos/img_producto.png 150 t 1 -16 Chocobomba img/productos/img_producto.png 100 t 1 -18 Carajillo Oriental img/productos/img_producto.png 80 t 2 -20 Latte img/productos/img_producto.png 140 t 2 -21 Latte de DDL img/productos/img_producto.png 180 t 2 -22 Latte de chocolate semi amargo img/productos/img_producto.png 180 t 2 -23 Latte de vainilla img/productos/img_producto.png 180 t 2 -24 Expresso img/productos/img_producto.png 100 t 2 -25 Expresso doble img/productos/img_producto.png 180 t 2 -26 Cortado img/productos/img_producto.png 100 t 2 -27 Lágrima img/productos/img_producto.png 100 t 2 -28 Americano img/productos/img_producto.png 100 t 2 -29 Té img/productos/img_producto.png 80 t 2 -30 Té con leche img/productos/img_producto.png 100 t 2 -31 Submarino img/productos/img_producto.png 100 t 2 -32 Muzzarela clásica img/productos/img_producto.png 450 t 3 -34 Margarita img/productos/img_producto.png 500 t 3 -35 Calzone img/productos/img_producto.png 450 t 3 -36 Fritas img/productos/img_producto.png 250 t 3 -37 Aros de cebolla img/productos/img_producto.png 320 t 3 -38 Papas con cheddar img/productos/img_producto.png 250 t 3 -41 Tostones img/productos/img_producto.png 250 t 3 -42 Corona chica img/productos/img_producto.png 150 t 4 -43 Corona grande img/productos/img_producto.png 230 t 4 -44 Patricia Dunkel (lata) img/productos/img_producto.png 150 t 4 -45 Patricia Dunkel (grande) img/productos/img_producto.png 280 t 4 -46 Zillertal (lata) img/productos/img_producto.png 150 t 4 -47 Zillertal (grande) img/productos/img_producto.png 280 t 4 -48 Patagonia Weisse img/productos/img_producto.png 230 t 4 -19 Cappuccino img/productos/img_producto.png 140 t 2 -50 Patagonia Bohemian img/productos/img_producto.png 230 t 4 -49 Patagonia 24.7 img/productos/img_producto.png 230 t 4 -1 Desayuno americano para dos img/productos/img_producto.png 800 t 1 -51 Patagonia Amber Lager img/productos/img_producto.png 240 t 4 -33 Una Vaina Bien img/productos/img_producto.png 550 t 3 -40 Tequeños img/productos/img_producto.png 250 f 3 -52 Monster img/productos/img_producto.png 140 t 4 -39 Pastelitos img/productos/img_producto.png 250 t 3 -\. - - --- --- Data for Name: prov_mate_prima; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.prov_mate_prima (id_proveedor, id_mat_prima) FROM stdin; -3 2 -2 3 -2 4 -3 1 -2 9 -3 10 -2 8 -3 7 -2 6 -3 5 -\. - - --- --- Data for Name: prov_producto; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.prov_producto (id_proveedor, id_producto) FROM stdin; -\. - - --- --- Data for Name: proveedores; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.proveedores (id_proveedor, rut, raz_social, direccion, contacto) FROM stdin; -1 217795000011 Emilupe S.R.L. \N 091049216 -2 216450470015 Finesa Trading S.A. \N 094426877 -3 \N Otro \N \N -4 \N Gara Gardo S en C \N \N -\. - - --- --- Data for Name: receta_producto; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.receta_producto (id_producto, id_mat_prima, qty_por_unidad) FROM stdin; -28 1 1.000 -19 1 1.000 -50 7 1.000 -49 8 1.000 -17 2 2.000 -17 3 21.500 -1 2 1.000 -1 10 10.000 -51 6 1.000 -33 3 60.000 -33 2 25.000 -52 5 1.000 -39 10 1.000 -39 3 1.000 -\. - - --- --- Data for Name: roles; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.roles (id_rol, nombre) FROM stdin; -1 Dueño -2 Cocinero -3 Barista -4 Barman -5 Bachero -6 Mozo -\. - - --- --- Data for Name: usua_roles; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.usua_roles (id_usuario, id_rol, fec_asignacion, autor, activo) FROM stdin; -1 1 2025-08-25 14:39:39.204513 1 t -1 3 2025-08-25 14:39:39.204513 1 t -2 3 2025-08-25 14:39:39.204513 1 t -2 4 2025-08-25 14:39:39.204513 1 t -3 2 2025-08-25 14:39:39.204513 1 t -\. - - --- --- Data for Name: usuarios; Type: TABLE DATA; Schema: public; Owner: manso --- - -COPY public.usuarios (id_usuario, documento, img_perfil, nombre, apellido, correo, telefono, fec_nacimiento, activo) FROM stdin; -1 52809684 img_perfil.png Mateo Saldain mateosaldain02@gmail.com \N 2002-08-11 t -2 55683627 img_perfil.png Cristopher Moreno \N \N 2001-08-11 t -3 49953084 img_perfil.png Bruno Correa \N \N 1999-08-19 t -\. - - --- --- Name: asistencia_intervalo_id_intervalo_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.asistencia_intervalo_id_intervalo_seq', 108, true); - - --- --- Name: asistencia_raw_id_raw_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.asistencia_raw_id_raw_seq', 88, true); - - --- --- Name: categorias_id_categoria_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.categorias_id_categoria_seq', 4, true); - - --- --- Name: clientes_id_cliente_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.clientes_id_cliente_seq', 1, true); - - --- --- Name: comandas_id_comanda_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.comandas_id_comanda_seq', 55, true); - - --- --- Name: compras_id_compra_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.compras_id_compra_seq', 66, true); - - --- --- Name: deta_comandas_id_det_comanda_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.deta_comandas_id_det_comanda_seq', 117, true); - - --- --- Name: mate_primas_id_mat_prima_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.mate_primas_id_mat_prima_seq', 10, true); - - --- --- Name: mesas_id_mesa_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.mesas_id_mesa_seq', 14, true); - - --- --- Name: productos_id_producto_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.productos_id_producto_seq', 52, true); - - --- --- Name: proveedores_id_proveedor_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.proveedores_id_proveedor_seq', 4, true); - - --- --- Name: roles_id_rol_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.roles_id_rol_seq', 6, true); - - --- --- Name: usuarios_id_usuario_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('public.usuarios_id_usuario_seq', 3, true); - - --- --- Name: asistencia_intervalo asistencia_intervalo_id_usuario_desde_hasta_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_id_usuario_desde_hasta_key UNIQUE (id_usuario, desde, hasta); - - --- --- Name: asistencia_intervalo asistencia_intervalo_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_pkey PRIMARY KEY (id_intervalo); - - --- --- Name: asistencia_raw asistencia_raw_id_usuario_ts_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.asistencia_raw - ADD CONSTRAINT asistencia_raw_id_usuario_ts_key UNIQUE (id_usuario, ts); - - --- --- Name: asistencia_raw asistencia_raw_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.asistencia_raw - ADD CONSTRAINT asistencia_raw_pkey PRIMARY KEY (id_raw); - - --- --- Name: categorias categorias_nombre_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.categorias - ADD CONSTRAINT categorias_nombre_key UNIQUE (nombre); - - --- --- Name: categorias categorias_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.categorias - ADD CONSTRAINT categorias_pkey PRIMARY KEY (id_categoria); - - --- --- Name: clientes clientes_correo_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.clientes - ADD CONSTRAINT clientes_correo_key UNIQUE (correo); - - --- --- Name: clientes clientes_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.clientes - ADD CONSTRAINT clientes_pkey PRIMARY KEY (id_cliente); - - --- --- Name: clientes clientes_telefono_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.clientes - ADD CONSTRAINT clientes_telefono_key UNIQUE (telefono); - - --- --- Name: comandas comandas_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.comandas - ADD CONSTRAINT comandas_pkey PRIMARY KEY (id_comanda); - - --- --- Name: compras compras_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.compras - ADD CONSTRAINT compras_pkey PRIMARY KEY (id_compra); - - --- --- Name: deta_comandas deta_comandas_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.deta_comandas - ADD CONSTRAINT deta_comandas_pkey PRIMARY KEY (id_det_comanda); - - --- --- Name: deta_comp_materias deta_comp_materias_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.deta_comp_materias - ADD CONSTRAINT deta_comp_materias_pkey PRIMARY KEY (id_compra, id_mat_prima); - - --- --- Name: deta_comp_producto deta_comp_producto_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.deta_comp_producto - ADD CONSTRAINT deta_comp_producto_pkey PRIMARY KEY (id_compra, id_producto); - - --- --- Name: mate_primas mate_primas_nombre_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.mate_primas - ADD CONSTRAINT mate_primas_nombre_key UNIQUE (nombre); - - --- --- Name: mate_primas mate_primas_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.mate_primas - ADD CONSTRAINT mate_primas_pkey PRIMARY KEY (id_mat_prima); - - --- --- Name: mesas mesas_apodo_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.mesas - ADD CONSTRAINT mesas_apodo_key UNIQUE (apodo); - - --- --- Name: mesas mesas_numero_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.mesas - ADD CONSTRAINT mesas_numero_key UNIQUE (numero); - - --- --- Name: mesas mesas_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.mesas - ADD CONSTRAINT mesas_pkey PRIMARY KEY (id_mesa); - - --- --- Name: productos productos_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.productos - ADD CONSTRAINT productos_pkey PRIMARY KEY (id_producto); - - --- --- Name: prov_mate_prima prov_mate_prima_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.prov_mate_prima - ADD CONSTRAINT prov_mate_prima_pkey PRIMARY KEY (id_proveedor, id_mat_prima); - - --- --- Name: prov_producto prov_producto_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.prov_producto - ADD CONSTRAINT prov_producto_pkey PRIMARY KEY (id_proveedor, id_producto); - - --- --- Name: proveedores proveedores_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.proveedores - ADD CONSTRAINT proveedores_pkey PRIMARY KEY (id_proveedor); - - --- --- Name: proveedores proveedores_rut_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.proveedores - ADD CONSTRAINT proveedores_rut_key UNIQUE (rut); - - --- --- Name: receta_producto receta_producto_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.receta_producto - ADD CONSTRAINT receta_producto_pkey PRIMARY KEY (id_producto, id_mat_prima); - - --- --- Name: roles roles_nombre_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.roles - ADD CONSTRAINT roles_nombre_key UNIQUE (nombre); - - --- --- Name: roles roles_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.roles - ADD CONSTRAINT roles_pkey PRIMARY KEY (id_rol); - - --- --- Name: usua_roles usua_roles_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.usua_roles - ADD CONSTRAINT usua_roles_pkey PRIMARY KEY (id_usuario, id_rol); - - --- --- Name: usuarios usuarios_documento_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.usuarios - ADD CONSTRAINT usuarios_documento_key UNIQUE (documento); - - --- --- Name: usuarios usuarios_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.usuarios - ADD CONSTRAINT usuarios_pkey PRIMARY KEY (id_usuario); - - --- --- Name: compras_fec_compra_idx; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX compras_fec_compra_idx ON public.compras USING btree (fec_compra); - - --- --- Name: idx_asist_int_usuario_fecha; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX idx_asist_int_usuario_fecha ON public.asistencia_intervalo USING btree (id_usuario, fecha); - - --- --- Name: idx_asist_raw_usuario_ts; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX idx_asist_raw_usuario_ts ON public.asistencia_raw USING btree (id_usuario, ts); - - --- --- Name: idx_detalle_comanda_comanda; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX idx_detalle_comanda_comanda ON public.deta_comandas USING btree (id_comanda); - - --- --- Name: idx_detalle_comanda_producto; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX idx_detalle_comanda_producto ON public.deta_comandas USING btree (id_producto); - - --- --- Name: ix_comandas_fec_cierre; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX ix_comandas_fec_cierre ON public.comandas USING btree (fec_cierre); - - --- --- Name: ix_comandas_id; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX ix_comandas_id ON public.comandas USING btree (id_comanda); - - --- --- Name: ix_deta_comandas_id_comanda; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX ix_deta_comandas_id_comanda ON public.deta_comandas USING btree (id_comanda); - - --- --- Name: ix_deta_comandas_id_producto; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX ix_deta_comandas_id_producto ON public.deta_comandas USING btree (id_producto); - - --- --- Name: asistencia_intervalo asistencia_intervalo_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES public.usuarios(id_usuario) ON DELETE CASCADE; - - --- --- Name: asistencia_raw asistencia_raw_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.asistencia_raw - ADD CONSTRAINT asistencia_raw_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES public.usuarios(id_usuario) ON DELETE CASCADE; - - --- --- Name: comandas comandas_id_mesa_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.comandas - ADD CONSTRAINT comandas_id_mesa_fkey FOREIGN KEY (id_mesa) REFERENCES public.mesas(id_mesa); - - --- --- Name: comandas comandas_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.comandas - ADD CONSTRAINT comandas_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES public.usuarios(id_usuario); - - --- --- Name: compras compras_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.compras - ADD CONSTRAINT compras_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES public.proveedores(id_proveedor); - - --- --- Name: deta_comandas deta_comandas_id_comanda_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.deta_comandas - ADD CONSTRAINT deta_comandas_id_comanda_fkey FOREIGN KEY (id_comanda) REFERENCES public.comandas(id_comanda) ON DELETE CASCADE; - - --- --- Name: deta_comandas deta_comandas_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.deta_comandas - ADD CONSTRAINT deta_comandas_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES public.productos(id_producto); - - --- --- Name: deta_comp_materias deta_comp_materias_id_compra_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.deta_comp_materias - ADD CONSTRAINT deta_comp_materias_id_compra_fkey FOREIGN KEY (id_compra) REFERENCES public.compras(id_compra) ON DELETE CASCADE; - - --- --- Name: deta_comp_materias deta_comp_materias_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.deta_comp_materias - ADD CONSTRAINT deta_comp_materias_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES public.mate_primas(id_mat_prima); - - --- --- Name: deta_comp_producto deta_comp_producto_id_compra_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.deta_comp_producto - ADD CONSTRAINT deta_comp_producto_id_compra_fkey FOREIGN KEY (id_compra) REFERENCES public.compras(id_compra) ON DELETE CASCADE; - - --- --- Name: deta_comp_producto deta_comp_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.deta_comp_producto - ADD CONSTRAINT deta_comp_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES public.productos(id_producto); - - --- --- Name: productos productos_id_categoria_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.productos - ADD CONSTRAINT productos_id_categoria_fkey FOREIGN KEY (id_categoria) REFERENCES public.categorias(id_categoria); - - --- --- Name: prov_mate_prima prov_mate_prima_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.prov_mate_prima - ADD CONSTRAINT prov_mate_prima_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES public.mate_primas(id_mat_prima); - - --- --- Name: prov_mate_prima prov_mate_prima_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.prov_mate_prima - ADD CONSTRAINT prov_mate_prima_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES public.proveedores(id_proveedor) ON DELETE CASCADE; - - --- --- Name: prov_producto prov_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.prov_producto - ADD CONSTRAINT prov_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES public.productos(id_producto); - - --- --- Name: prov_producto prov_producto_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.prov_producto - ADD CONSTRAINT prov_producto_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES public.proveedores(id_proveedor) ON DELETE CASCADE; - - --- --- Name: receta_producto receta_producto_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.receta_producto - ADD CONSTRAINT receta_producto_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES public.mate_primas(id_mat_prima); - - --- --- Name: receta_producto receta_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.receta_producto - ADD CONSTRAINT receta_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES public.productos(id_producto) ON DELETE CASCADE; - - --- --- Name: usua_roles usua_roles_autor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.usua_roles - ADD CONSTRAINT usua_roles_autor_fkey FOREIGN KEY (autor) REFERENCES public.usuarios(id_usuario); - - --- --- Name: usua_roles usua_roles_id_rol_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.usua_roles - ADD CONSTRAINT usua_roles_id_rol_fkey FOREIGN KEY (id_rol) REFERENCES public.roles(id_rol); - - --- --- Name: usua_roles usua_roles_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY public.usua_roles - ADD CONSTRAINT usua_roles_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES public.usuarios(id_usuario) ON DELETE CASCADE; - - --- --- PostgreSQL database dump complete --- - -\unrestrict londHmqT4llS8Wof4ZnceO2dyFhn4jiR5xbaszMgZpMczgr6aVW6xQJxeUdqJwa - diff --git a/services/auth/src/db/initTenant.sql b/services/auth/src/db/initTenant.sql deleted file mode 100644 index a43ab8c..0000000 --- a/services/auth/src/db/initTenant.sql +++ /dev/null @@ -1,2239 +0,0 @@ --- =============================================================== --- SuiteCoffee — Template de inicialización por tenant --- Archivo: 01_init.sql --- Uso (psql): --- \set SCHEMA_NAME schema_tenant_12345678abcd --- CREATE SCHEMA IF NOT EXISTS :"SCHEMA_NAME"; --- -- Opcional: SET ROLE ; --- \i 01_init.sql --- =============================================================== - -BEGIN; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SET check_function_bodies = false; -SET client_min_messages = warning; -SET row_security = off; - --- establece el schema de destino -SET search_path = :"SCHEMA_NAME", public; - --- --- PostgreSQL database dump --- - -\restrict londHmqT4llS8Wof4ZnceO2dyFhn4jiR5xbaszMgZpMczgr6aVW6xQJxeUdqJwa - --- Dumped from database version 16.10 (Debian 16.10-1.pgdg13+1) --- Dumped by pg_dump version 16.10 (Debian 16.10-1.pgdg13+1) - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - --- --- Name: asistencia_delete_raw(bigint, text); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION asistencia_delete_raw(p_id_raw bigint, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE - v_id_usuario INT; - v_ts TIMESTAMPTZ; - v_t0 TIMESTAMPTZ; - v_t1 TIMESTAMPTZ; - v_del_raw INT; - v_del INT; - v_ins INT; -BEGIN - SELECT id_usuario, ts INTO v_id_usuario, v_ts - FROM asistencia_raw WHERE id_raw = p_id_raw; - IF v_id_usuario IS NULL THEN - RETURN jsonb_build_object('deleted',0,'msg','id_raw inexistente'); - END IF; - - v_t0 := v_ts - INTERVAL '1 day'; - v_t1 := v_ts + INTERVAL '1 day'; - - -- borrar raw - DELETE FROM asistencia_raw WHERE id_raw = p_id_raw; - GET DIAGNOSTICS v_del_raw = ROW_COUNT; - - -- recomputar pares en ventana - WITH tl AS ( - SELECT ar.ts, - ROW_NUMBER() OVER (ORDER BY ar.ts) AS rn - FROM asistencia_raw ar - WHERE ar.id_usuario = v_id_usuario - AND ar.ts BETWEEN v_t0 AND v_t1 - ), - ready AS ( - SELECT (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM tl t1 - JOIN tl t2 ON t2.rn = t1.rn + 1 - WHERE (t1.rn % 2) = 1 AND t2.ts > t1.ts - ), - del AS ( - DELETE FROM asistencia_intervalo ai - WHERE ai.id_usuario = v_id_usuario - AND (ai.desde BETWEEN v_t0 AND v_t1 OR ai.hasta BETWEEN v_t0 AND v_t1) - RETURNING 1 - ), - ins AS ( - INSERT INTO asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT v_id_usuario, r.fecha, r.desde, r.hasta, r.dur_min, 'delete_adjust' - FROM ready r - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT (SELECT COUNT(*) FROM del), (SELECT COUNT(*) FROM ins) INTO v_del, v_ins; - - RETURN jsonb_build_object('deleted',v_del_raw,'deleted_pairs',v_del,'inserted_pairs',v_ins); -END; -$$; - --- --- Name: asistencia_get(text, date, date, text); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION asistencia_get(p_doc text, p_desde date, p_hasta date, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH u AS ( - SELECT id_usuario, documento, nombre, apellido - FROM usuarios - WHERE regexp_replace(documento,'^\s*0+','','g') = regexp_replace(p_doc,'^\s*0+','','g') - LIMIT 1 -), -r AS ( - SELECT ar.id_raw, - (ar.ts AT TIME ZONE p_tz)::date AS fecha, - to_char(ar.ts AT TIME ZONE p_tz,'HH24:MI:SS') AS hora, - COALESCE(ar.modo,'') AS modo, - COALESCE(ar.origen,'') AS origen, - ar.ts - FROM asistencia_raw ar - JOIN u USING (id_usuario) - WHERE (ar.ts AT TIME ZONE p_tz)::date BETWEEN p_desde AND p_hasta -), -i AS ( - SELECT ai.id_intervalo, - ai.fecha, - to_char(ai.desde AT TIME ZONE p_tz,'HH24:MI:SS') AS desde_hora, - to_char(ai.hasta AT TIME ZONE p_tz,'HH24:MI:SS') AS hasta_hora, - ai.dur_min - FROM asistencia_intervalo ai - JOIN u USING (id_usuario) - WHERE ai.fecha BETWEEN p_desde AND p_hasta -) -SELECT jsonb_build_object( - 'usuario', (SELECT to_jsonb(u.*) FROM u), - 'raw', COALESCE((SELECT jsonb_agg(to_jsonb(r.*) ORDER BY r.ts) FROM r),'[]'::jsonb), - 'intervalos', COALESCE((SELECT jsonb_agg(to_jsonb(i.*) ORDER BY i.fecha, i.id_intervalo) FROM i),'[]'::jsonb) -); -$$; - --- --- Name: asistencia_update_raw(bigint, date, text, text, text); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION asistencia_update_raw(p_id_raw bigint, p_fecha date, p_hora text, p_modo text, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE - v_id_usuario INT; - v_ts_old TIMESTAMPTZ; - v_ts_new TIMESTAMPTZ; - v_t0 TIMESTAMPTZ; - v_t1 TIMESTAMPTZ; - v_del INT; - v_ins INT; -BEGIN - -- leer estado previo - SELECT id_usuario, ts INTO v_id_usuario, v_ts_old - FROM asistencia_raw WHERE id_raw = p_id_raw; - IF v_id_usuario IS NULL THEN - RETURN jsonb_build_object('updated',0,'msg','id_raw inexistente'); - END IF; - - -- construir ts nuevo - v_ts_new := make_timestamptz( - EXTRACT(YEAR FROM p_fecha)::INT, - EXTRACT(MONTH FROM p_fecha)::INT, - EXTRACT(DAY FROM p_fecha)::INT, - split_part(p_hora,':',1)::INT, - split_part(p_hora,':',2)::INT, - COALESCE(NULLIF(split_part(p_hora,':',3),''), '0')::INT, - p_tz); - - -- aplicar update - UPDATE asistencia_raw - SET ts = v_ts_new, - modo = COALESCE(p_modo, modo) - WHERE id_raw = p_id_raw; - - -- ventana de recálculo - v_t0 := LEAST(v_ts_old, v_ts_new) - INTERVAL '1 day'; - v_t1 := GREATEST(v_ts_old, v_ts_new) + INTERVAL '1 day'; - - -- recomputar pares en la ventana: borrar los del rango y reinsertar - WITH tl AS ( - SELECT ar.ts, - ROW_NUMBER() OVER (ORDER BY ar.ts) AS rn - FROM asistencia_raw ar - WHERE ar.id_usuario = v_id_usuario - AND ar.ts BETWEEN v_t0 AND v_t1 - ), - ready AS ( - SELECT (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM tl t1 - JOIN tl t2 ON t2.rn = t1.rn + 1 - WHERE (t1.rn % 2) = 1 AND t2.ts > t1.ts - ), - del AS ( - DELETE FROM asistencia_intervalo ai - WHERE ai.id_usuario = v_id_usuario - AND (ai.desde BETWEEN v_t0 AND v_t1 OR ai.hasta BETWEEN v_t0 AND v_t1) - RETURNING 1 - ), - ins AS ( - INSERT INTO asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT v_id_usuario, r.fecha, r.desde, r.hasta, r.dur_min, 'edit_manual' - FROM ready r - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT (SELECT COUNT(*) FROM del), (SELECT COUNT(*) FROM ins) INTO v_del, v_ins; - - RETURN jsonb_build_object('updated',1,'deleted_pairs',v_del,'inserted_pairs',v_ins); -END; -$$; - --- --- Name: delete_compra(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION delete_compra(p_id_compra integer) RETURNS void - LANGUAGE plpgsql - AS $$ -BEGIN - DELETE FROM deta_comp_materias WHERE id_compra = p_id_compra; - DELETE FROM deta_comp_producto WHERE id_compra = p_id_compra; - DELETE FROM compras WHERE id_compra = p_id_compra; -END; -$$; - --- --- Name: f_abrir_comanda(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION f_abrir_comanda(p_id integer) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE r jsonb; -BEGIN - UPDATE comandas - SET estado = 'abierta', - fec_cierre = NULL - WHERE id_comanda = p_id; - - IF NOT FOUND THEN - RETURN NULL; - END IF; - - SELECT to_jsonb(v) INTO r - FROM v_comandas_resumen v - WHERE v.id_comanda = p_id; - - RETURN r; -END; -$$; - --- --- Name: f_cerrar_comanda(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION f_cerrar_comanda(p_id integer) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE r jsonb; -BEGIN - UPDATE comandas - SET estado = 'cerrada', - fec_cierre = COALESCE(fec_cierre, NOW()) - WHERE id_comanda = p_id; - - IF NOT FOUND THEN - RETURN NULL; - END IF; - - SELECT to_jsonb(v) INTO r - FROM v_comandas_resumen v - WHERE v.id_comanda = p_id; - - RETURN r; -END; -$$; - --- --- Name: f_comanda_detalle_json(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION f_comanda_detalle_json(p_id_comanda integer) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH base AS ( - SELECT - c.id_comanda, - c.fec_creacion, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM comandas c - JOIN usuarios u ON u.id_usuario = c.id_usuario - JOIN mesas m ON m.id_mesa = c.id_mesa - LEFT JOIN deta_comandas d ON d.id_comanda = c.id_comanda - LEFT JOIN productos p ON p.id_producto = d.id_producto - WHERE c.id_comanda = p_id_comanda -), -hdr AS ( - -- 1 sola fila con los datos de cabecera - SELECT DISTINCT - id_comanda, fec_creacion, estado, observaciones, - id_usuario, usuario_nombre, usuario_apellido, - id_mesa, mesa_numero, mesa_apodo - FROM base -), -agg_items AS ( - SELECT - COALESCE( - jsonb_agg( - jsonb_build_object( - 'producto_id', b.id_producto, - 'producto', b.producto_nombre, - 'cantidad', b.cantidad, - 'pre_unitario', b.pre_unitario, - 'subtotal', b.subtotal - ) - ORDER BY b.producto_nombre NULLS LAST - ) FILTER (WHERE b.id_producto IS NOT NULL), - '[]'::jsonb - ) AS items - FROM base b -), -tot AS ( - SELECT - COUNT(*) FILTER (WHERE id_producto IS NOT NULL) AS items, - COALESCE(SUM(subtotal), 0)::numeric AS total - FROM base -) -SELECT - CASE - WHEN EXISTS (SELECT 1 FROM hdr) THEN - jsonb_build_object( - 'id_comanda', h.id_comanda, - 'fec_creacion', h.fec_creacion, - 'estado', h.estado, - 'observaciones',h.observaciones, - 'usuario', jsonb_build_object( - 'id_usuario', h.id_usuario, - 'nombre', h.usuario_nombre, - 'apellido', h.usuario_apellido - ), - 'mesa', jsonb_build_object( - 'id_mesa', h.id_mesa, - 'numero', h.mesa_numero, - 'apodo', h.mesa_apodo - ), - 'items', i.items, - 'totales', jsonb_build_object( - 'items', t.items, - 'total', t.total - ) - ) - ELSE NULL - END -FROM hdr h, agg_items i, tot t; -$$; - --- --- Name: f_comanda_detalle_rows(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION f_comanda_detalle_rows(p_id_comanda integer) RETURNS TABLE(id_comanda integer, fec_creacion timestamp without time zone, estado text, observaciones text, id_usuario integer, usuario_nombre text, usuario_apellido text, id_mesa integer, mesa_numero integer, mesa_apodo text, producto_id integer, producto_nombre text, cantidad numeric, pre_unitario numeric, subtotal numeric, items integer, total numeric) - LANGUAGE sql - AS $$ -WITH base AS ( - SELECT - c.id_comanda, c.fec_creacion, c.estado, c.observaciones, - u.id_usuario, u.nombre AS usuario_nombre, u.apellido AS usuario_apellido, - m.id_mesa, m.numero AS mesa_numero, m.apodo AS mesa_apodo, - d.id_producto, p.nombre AS producto_nombre, - d.cantidad, d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM comandas c - JOIN usuarios u ON u.id_usuario = c.id_usuario - JOIN mesas m ON m.id_mesa = c.id_mesa - LEFT JOIN deta_comandas d ON d.id_comanda = c.id_comanda - LEFT JOIN productos p ON p.id_producto = d.id_producto - WHERE c.id_comanda = p_id_comanda -), -tot AS ( - SELECT - COUNT(*) FILTER (WHERE id_producto IS NOT NULL) AS items, - COALESCE(SUM(subtotal), 0) AS total - FROM base -) -SELECT - b.id_comanda, b.fec_creacion, b.estado, b.observaciones, - b.id_usuario, b.usuario_nombre, b.usuario_apellido, - b.id_mesa, b.mesa_numero, b.mesa_apodo, - b.id_producto, b.producto_nombre, - b.cantidad, b.pre_unitario, b.subtotal, - t.items, t.total -FROM base b CROSS JOIN tot t -ORDER BY b.producto_nombre NULLS LAST; -$$; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: comandas; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE comandas ( - id_comanda integer NOT NULL, - id_usuario integer NOT NULL, - id_mesa integer NOT NULL, - fec_creacion timestamp without time zone DEFAULT now() NOT NULL, - estado text NOT NULL, - observaciones text, - fec_cierre timestamp with time zone, - CONSTRAINT comandas_estado_check CHECK ((estado = ANY (ARRAY['abierta'::text, 'cerrada'::text, 'pagada'::text, 'anulada'::text]))) -); - --- --- Name: COLUMN comandas.fec_cierre; Type: COMMENT; Schema: public; Owner: manso --- - -COMMENT ON COLUMN comandas.fec_cierre IS 'Fecha/hora de cierre de la comanda (NULL si está abierta)'; - --- --- Name: deta_comandas; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE deta_comandas ( - id_det_comanda integer NOT NULL, - id_comanda integer NOT NULL, - id_producto integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - observaciones text, - CONSTRAINT deta_comandas_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comandas_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - --- --- Name: mesas; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE mesas ( - id_mesa integer NOT NULL, - numero integer NOT NULL, - apodo text NOT NULL, - estado text DEFAULT 'libre'::text NOT NULL, - CONSTRAINT mesas_estado_check CHECK ((estado = ANY (ARRAY['libre'::text, 'ocupada'::text, 'reservada'::text]))) -); - --- --- Name: usuarios; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE usuarios ( - id_usuario integer NOT NULL, - documento text, - img_perfil character varying(255) DEFAULT 'img_perfil.png'::character varying NOT NULL, - nombre text NOT NULL, - apellido text NOT NULL, - correo text, - telefono text, - fec_nacimiento date, - activo boolean DEFAULT true -); - --- --- Name: v_comandas_resumen; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW v_comandas_resumen AS - WITH items AS ( - SELECT d.id_comanda, - count(*) AS items, - sum((d.cantidad * d.pre_unitario)) AS total - FROM deta_comandas d - GROUP BY d.id_comanda - ) - SELECT c.id_comanda, - c.fec_creacion, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - COALESCE(i.items, (0)::bigint) AS items, - COALESCE(i.total, (0)::numeric) AS total, - c.fec_cierre, - CASE - WHEN (c.fec_cierre IS NOT NULL) THEN round((EXTRACT(epoch FROM (c.fec_cierre - (c.fec_creacion)::timestamp with time zone)) / 60.0), 1) - ELSE NULL::numeric - END AS duracion_min - FROM (((comandas c - JOIN usuarios u ON ((u.id_usuario = c.id_usuario))) - JOIN mesas m ON ((m.id_mesa = c.id_mesa))) - LEFT JOIN items i ON ((i.id_comanda = c.id_comanda))); - --- --- Name: f_comandas_resumen(text, integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION f_comandas_resumen(p_estado text DEFAULT NULL::text, p_limit integer DEFAULT 200) RETURNS SETOF v_comandas_resumen - LANGUAGE sql - AS $$ - SELECT * - FROM v_comandas_resumen - WHERE (p_estado IS NULL OR estado = p_estado) - ORDER BY id_comanda DESC - LIMIT p_limit; -$$; - --- --- Name: find_usuarios_por_documentos(jsonb); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION find_usuarios_por_documentos(p_docs jsonb) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH docs AS ( - SELECT DISTINCT - regexp_replace(value::text, '^\s*0+', '', 'g') AS doc_clean, - value::text AS original - FROM jsonb_array_elements_text(COALESCE(p_docs,'[]')) -), -rows AS ( - SELECT d.original AS documento, - u.nombre, - u.apellido, - (u.id_usuario IS NOT NULL) AS found - FROM docs d - LEFT JOIN usuarios u - ON regexp_replace(u.documento, '^\s*0+', '', 'g') = d.doc_clean -) -SELECT COALESCE( - jsonb_object_agg( - documento, - jsonb_build_object( - 'nombre', COALESCE(nombre, ''), - 'apellido', COALESCE(apellido, ''), - 'found', found - ) - ), - '{}'::jsonb -) -FROM rows; -$$; - --- --- Name: get_compra(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION get_compra(p_id_compra integer) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH cab AS ( - SELECT c.id_compra, c.id_proveedor, c.fec_compra, c.total - FROM compras c - WHERE c.id_compra = p_id_compra -), -dm AS ( - SELECT 'MAT'::text AS tipo, d.id_mat_prima AS id, - d.cantidad, d.pre_unitario AS precio - FROM deta_comp_materias d WHERE d.id_compra = p_id_compra -), -dp AS ( - SELECT 'PROD'::text AS tipo, d.id_producto AS id, - d.cantidad, d.pre_unitario AS precio - FROM deta_comp_producto d WHERE d.id_compra = p_id_compra -), -det AS ( - SELECT jsonb_agg(to_jsonb(x.*)) AS detalles - FROM ( - SELECT * FROM dm - UNION ALL - SELECT * FROM dp - ) x -) -SELECT jsonb_build_object( - 'id_compra', (SELECT id_compra FROM cab), - 'id_proveedor',(SELECT id_proveedor FROM cab), - 'fec_compra', to_char((SELECT fec_compra FROM cab),'YYYY-MM-DD HH24:MI:SS'), - 'total', (SELECT total FROM cab), - 'detalles', COALESCE((SELECT detalles FROM det),'[]'::jsonb) -); -$$; - --- --- Name: get_materia_prima(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION get_materia_prima(p_id integer) RETURNS jsonb - LANGUAGE sql - AS $$ -SELECT jsonb_build_object( - 'materia', to_jsonb(mp), - 'proveedores', COALESCE( - ( - SELECT jsonb_agg( - jsonb_build_object( - 'id_proveedor', pr.id_proveedor, - 'raz_social', pr.raz_social, - 'rut', pr.rut, - 'contacto', pr.contacto, - 'direccion', pr.direccion - ) - ) - FROM prov_mate_prima pmp - JOIN proveedores pr ON pr.id_proveedor = pmp.id_proveedor - WHERE pmp.id_mat_prima = mp.id_mat_prima - ), - '[]'::jsonb - ) -) -FROM mate_primas mp -WHERE mp.id_mat_prima = p_id; -$$; - --- --- Name: get_producto(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION get_producto(p_id integer) RETURNS jsonb - LANGUAGE sql - AS $$ -SELECT jsonb_build_object( - 'producto', to_jsonb(p), -- el registro completo del producto en JSONB - 'receta', COALESCE( - ( - SELECT jsonb_agg( - jsonb_build_object( - 'id_mat_prima', rp.id_mat_prima, - 'qty_por_unidad', rp.qty_por_unidad, - 'nombre', mp.nombre, - 'unidad', mp.unidad - ) - ) - FROM receta_producto rp - LEFT JOIN mate_primas mp USING (id_mat_prima) - WHERE rp.id_producto = p.id_producto - ), - '[]'::jsonb - ) -) -FROM productos p -WHERE p.id_producto = p_id; -$$; - --- --- Name: import_asistencia(jsonb, text, text); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION import_asistencia(p_registros jsonb, p_origen text, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $_$ -DECLARE - v_ins_raw INT; - v_ins_pairs INT; - v_miss JSONB; -BEGIN - WITH - -- 1) JSON -> filas - j AS ( - SELECT - regexp_replace(elem->>'doc','^\s*0+','','g')::TEXT AS doc_clean, - (elem->>'isoDate')::DATE AS d, - elem->>'time' AS time_str, - NULLIF(elem->>'mode','') AS modo - FROM jsonb_array_elements(COALESCE(p_registros,'[]')) elem - ), - -- 2) Vincular a usuarios - u AS ( - SELECT j.*, u.id_usuario - FROM j - LEFT JOIN usuarios u - ON regexp_replace(u.documento,'^\s*0+','','g') = j.doc_clean - ), - -- 3) Documentos faltantes - miss AS ( - SELECT jsonb_agg(doc_clean) AS missing - FROM u WHERE id_usuario IS NULL - ), - -- 4) TS determinista en TZ del negocio - parsed AS ( - SELECT - u.id_usuario, - u.modo, - make_timestamptz( - EXTRACT(YEAR FROM u.d)::INT, - EXTRACT(MONTH FROM u.d)::INT, - EXTRACT(DAY FROM u.d)::INT, - split_part(u.time_str,':',1)::INT, - split_part(u.time_str,':',2)::INT, - COALESCE(NULLIF(split_part(u.time_str,':',3),''),'0')::INT, - p_tz - ) AS ts_calc - FROM u - WHERE u.id_usuario IS NOT NULL - ), - -- 5) Ventana por usuario (±1 día de lo importado) - win AS ( - SELECT id_usuario, - (MIN(ts_calc) - INTERVAL '1 day') AS t0, - (MAX(ts_calc) + INTERVAL '1 day') AS t1 - FROM parsed - GROUP BY id_usuario - ), - -- 6) Lo existente en BD dentro de la ventana - existing AS ( - SELECT ar.id_usuario, ar.ts - FROM asistencia_raw ar - JOIN win w ON w.id_usuario = ar.id_usuario - AND ar.ts BETWEEN w.t0 AND w.t1 - ), - -- 7) CANDIDATE = existente ∪ archivo (sin duplicados) - candidate AS ( - SELECT id_usuario, ts FROM existing - UNION -- ¡clave para evitar doble click! - SELECT id_usuario, ts_calc AS ts FROM parsed - ), - -- 8) Paridad previa (cuántas marcas había ANTES de la ventana) - before_cnt AS ( - SELECT w.id_usuario, COUNT(*)::int AS cnt - FROM win w - JOIN asistencia_raw ar - ON ar.id_usuario = w.id_usuario - AND ar.ts < w.t0 - GROUP BY w.id_usuario - ), - -- 9) Línea de tiempo candidata y pares (1→2, 3→4…), jornada = día local del inicio - timeline AS ( - SELECT - c.id_usuario, - c.ts, - ROW_NUMBER() OVER (PARTITION BY c.id_usuario ORDER BY c.ts) AS rn - FROM candidate c - ), - ready AS ( - SELECT - t1.id_usuario, - (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM timeline t1 - JOIN timeline t2 - ON t2.id_usuario = t1.id_usuario - AND t2.rn = t1.rn + 1 - LEFT JOIN before_cnt b ON b.id_usuario = t1.id_usuario - WHERE ((COALESCE(b.cnt,0) + t1.rn) % 2) = 1 -- t1 es IN global - AND t2.ts > t1.ts - ), - -- 10) INSERT crudo (dedupe) - ins_raw AS ( - INSERT INTO asistencia_raw (id_usuario, ts, modo, origen) - SELECT id_usuario, ts_calc, - NULLIF(modo,'')::text, -- puede quedar NULL para auto-etiquetado - p_origen - FROM parsed - ON CONFLICT (id_usuario, ts) DO NOTHING - RETURNING 1 - ), - -- 11) Auto-etiquetar IN/OUT en BD para filas con modo vacío/'1' (tras insertar) - before_cnt2 AS ( - SELECT w.id_usuario, COUNT(*)::int AS cnt - FROM win w - JOIN asistencia_raw ar - ON ar.id_usuario = w.id_usuario - AND ar.ts < w.t0 - GROUP BY w.id_usuario - ), - tl2 AS ( - SELECT - ar.id_usuario, ar.ts, - ROW_NUMBER() OVER (PARTITION BY ar.id_usuario ORDER BY ar.ts) AS rn - FROM asistencia_raw ar - JOIN win w ON w.id_usuario = ar.id_usuario - AND ar.ts BETWEEN w.t0 AND w.t1 - ), - label2 AS ( - SELECT - t.id_usuario, - t.ts, - CASE WHEN ((COALESCE(b.cnt,0) + t.rn) % 2) = 1 THEN 'IN' ELSE 'OUT' END AS new_mode - FROM tl2 t - LEFT JOIN before_cnt2 b ON b.id_usuario = t.id_usuario - ), - set_mode AS ( - UPDATE asistencia_raw ar - SET modo = l.new_mode - FROM label2 l - WHERE ar.id_usuario = l.id_usuario - AND ar.ts = l.ts - AND (ar.modo IS NULL OR btrim(ar.modo) = '' OR ar.modo ~ '^\s*1\s*$') - RETURNING 1 - ), - -- 12) INSERT pares (dedupe) calculados desde CANDIDATE (ya tiene todo el contexto) - ins_pairs AS ( - INSERT INTO asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT id_usuario, fecha, desde, hasta, dur_min, p_origen - FROM ready - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT - (SELECT COUNT(*) FROM ins_raw), - (SELECT COUNT(*) FROM ins_pairs), - (SELECT COALESCE(missing,'[]'::jsonb) FROM miss) - INTO v_ins_raw, v_ins_pairs, v_miss; - - RETURN jsonb_build_object( - 'inserted_raw', v_ins_raw, - 'inserted_pairs', v_ins_pairs, - 'missing_docs', v_miss - ); -END; -$_$; - --- --- Name: report_asistencia(date, date); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION report_asistencia(p_desde date, p_hasta date) RETURNS TABLE(documento text, nombre text, apellido text, fecha date, desde_hora text, hasta_hora text, dur_min numeric) - LANGUAGE sql - AS $$ - SELECT - u.documento, u.nombre, u.apellido, - ai.fecha, - to_char(ai.desde AT TIME ZONE 'America/Montevideo','HH24:MI:SS') AS desde_hora, - to_char(ai.hasta AT TIME ZONE 'America/Montevideo','HH24:MI:SS') AS hasta_hora, - ai.dur_min - FROM asistencia_intervalo ai - JOIN usuarios u USING (id_usuario) - WHERE ai.fecha BETWEEN p_desde AND p_hasta - ORDER BY u.documento, ai.fecha, ai.desde; -$$; - --- --- Name: report_gastos(integer); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION report_gastos(p_year integer) RETURNS jsonb - LANGUAGE sql STABLE - AS $$ -WITH mdata AS ( - SELECT date_trunc('month', c.fec_compra)::date AS m, - SUM(c.total)::numeric AS importe - FROM compras c - WHERE EXTRACT(YEAR FROM c.fec_compra) = p_year - GROUP BY 1 -), -mm AS ( - SELECT EXTRACT(MONTH FROM m)::int AS mes, importe - FROM mdata -) -SELECT jsonb_build_object( - 'year', p_year, - 'total', COALESCE((SELECT SUM(importe) FROM mdata), 0), - 'avg', COALESCE((SELECT AVG(importe) FROM mdata), 0), - 'months', - (SELECT jsonb_agg( - jsonb_build_object( - 'mes', gs, - 'nombre', to_char(to_date(gs::text,'MM'),'Mon'), - 'importe', COALESCE(mm.importe,0) - ) - ORDER BY gs - ) - FROM generate_series(1,12) gs - LEFT JOIN mm ON mm.mes = gs) -); -$$; - --- --- Name: report_tickets_year(integer, text); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION report_tickets_year(p_year integer, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE sql STABLE - AS $$ -WITH bounds AS ( - SELECT - make_timestamp(p_year, 1, 1, 0,0,0) AS d0, - make_timestamp(p_year+1, 1, 1, 0,0,0) AS d1, - make_timestamptz(p_year, 1, 1, 0,0,0, p_tz) AS t0, - make_timestamptz(p_year+1, 1, 1, 0,0,0, p_tz) AS t1 -), -base AS ( - SELECT - c.id_comanda, - CASE WHEN c.fec_cierre IS NOT NULL - THEN (c.fec_cierre AT TIME ZONE p_tz) - ELSE c.fec_creacion - END AS fec_local, - v.total - FROM comandas c - JOIN vw_ticket_total v ON v.id_comanda = c.id_comanda - JOIN bounds b ON TRUE - WHERE - (c.fec_cierre IS NOT NULL AND c.fec_cierre >= b.t0 AND c.fec_cierre < b.t1) - OR - (c.fec_cierre IS NULL AND c.fec_creacion >= b.d0 AND c.fec_creacion < b.d1) -), -m AS ( - SELECT - EXTRACT(MONTH FROM fec_local)::int AS mes, - COUNT(*)::int AS cant, - SUM(total)::numeric AS importe, - AVG(total)::numeric AS avg - FROM base - GROUP BY 1 -), -ytd AS ( - SELECT COUNT(*)::int AS total_ytd, - AVG(total)::numeric AS avg_ticket, - SUM(total)::numeric AS to_date - FROM base -) -SELECT jsonb_build_object( - 'year', p_year, - 'total_ytd', (SELECT total_ytd FROM ytd), - 'avg_ticket', (SELECT avg_ticket FROM ytd), - 'to_date', (SELECT to_date FROM ytd), - 'months', - (SELECT jsonb_agg( - jsonb_build_object( - 'mes', mes, - 'nombre', to_char(to_date(mes::text,'MM'),'Mon'), - 'cant', cant, - 'importe', importe, - 'avg', avg - ) - ORDER BY mes - ) - FROM m) -); -$$; - --- --- Name: save_compra(integer, integer, timestamp with time zone, jsonb); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION save_compra(p_id_compra integer, p_id_proveedor integer, p_fec_compra timestamp with time zone, p_detalles jsonb) RETURNS TABLE(id_compra integer, total numeric) - LANGUAGE plpgsql - AS $$ -DECLARE - v_id INT; - v_total numeric := 0; -BEGIN - IF COALESCE(jsonb_array_length(p_detalles),0) = 0 THEN - RAISE EXCEPTION 'No hay renglones en la compra'; - END IF; - - -- Cabecera (insert/update) - IF p_id_compra IS NULL THEN - INSERT INTO compras (id_proveedor, fec_compra, total) - VALUES (p_id_proveedor, COALESCE(p_fec_compra, now()), 0) - RETURNING compras.id_compra INTO v_id; - ELSE - UPDATE compras c - SET id_proveedor = p_id_proveedor, - fec_compra = COALESCE(p_fec_compra, c.fec_compra) - WHERE c.id_compra = p_id_compra - RETURNING c.id_compra INTO v_id; - - -- Reemplazamos los renglones - DELETE FROM deta_comp_materias d WHERE d.id_compra = v_id; - DELETE FROM deta_comp_producto p WHERE p.id_compra = v_id; - END IF; - - -- Materias primas (sin CTE: parseo JSON inline) - INSERT INTO deta_comp_materias (id_compra, id_mat_prima, cantidad, pre_unitario) - SELECT - v_id, - x.id, - x.cantidad, - x.precio - FROM jsonb_to_recordset(COALESCE(p_detalles, '[]'::jsonb)) - AS x(tipo text, id int, cantidad numeric, precio numeric) - WHERE UPPER(TRIM(x.tipo)) = 'MAT'; - - -- Productos (sin CTE) - INSERT INTO deta_comp_producto (id_compra, id_producto, cantidad, pre_unitario) - SELECT - v_id, - x.id, - x.cantidad, - x.precio - FROM jsonb_to_recordset(COALESCE(p_detalles, '[]'::jsonb)) - AS x(tipo text, id int, cantidad numeric, precio numeric) - WHERE UPPER(TRIM(x.tipo)) = 'PROD'; - - -- Recalcular total (calificado) y redondear a ENTERO - SELECT - COALESCE( (SELECT SUM(dcm.cantidad*dcm.pre_unitario) - FROM deta_comp_materias dcm - WHERE dcm.id_compra = v_id), 0) - + COALESCE( (SELECT SUM(dcp.cantidad*dcp.pre_unitario) - FROM deta_comp_producto dcp - WHERE dcp.id_compra = v_id), 0) - INTO v_total; - - UPDATE compras c - SET total = round(v_total, 0) - WHERE c.id_compra = v_id; - - RETURN QUERY SELECT v_id, round(v_total, 0); -END; -$$; - --- --- Name: save_materia_prima(integer, text, text, boolean, jsonb); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION save_materia_prima(p_id_mat_prima integer, p_nombre text, p_unidad text, p_activo boolean, p_proveedores jsonb DEFAULT '[]'::jsonb) RETURNS integer - LANGUAGE plpgsql - AS $_$ -DECLARE - v_id INT; -BEGIN - IF p_id_mat_prima IS NULL THEN - INSERT INTO mate_primas (nombre, unidad, activo) - VALUES (p_nombre, p_unidad, COALESCE(p_activo, TRUE)) - RETURNING mate_primas.id_mat_prima INTO v_id; - ELSE - UPDATE mate_primas mp - SET nombre = p_nombre, - unidad = p_unidad, - activo = COALESCE(p_activo, TRUE) - WHERE mp.id_mat_prima = p_id_mat_prima; - v_id := p_id_mat_prima; - END IF; - - -- Sincronizar proveedores: borrar todos y re-crear a partir de JSONB - DELETE FROM prov_mate_prima pmp WHERE pmp.id_mat_prima = v_id; - - INSERT INTO prov_mate_prima (id_proveedor, id_mat_prima) - SELECT (e->>0)::INT AS id_proveedor, -- elementos JSON como enteros (array simple) - v_id AS id_mat_prima - FROM jsonb_array_elements(COALESCE(p_proveedores, '[]'::jsonb)) AS e - WHERE (e->>0) ~ '^\d+$'; -- solo enteros - - RETURN v_id; -END; -$_$; - --- --- Name: save_producto(integer, text, text, numeric, boolean, integer, jsonb); Type: FUNCTION; Schema: public; Owner: manso --- - -CREATE FUNCTION save_producto(p_id_producto integer, p_nombre text, p_img_producto text, p_precio numeric, p_activo boolean, p_id_categoria integer, p_receta jsonb DEFAULT '[]'::jsonb) RETURNS integer - LANGUAGE plpgsql - AS $_$ -DECLARE - v_id INT; -BEGIN - IF p_id_producto IS NULL THEN - INSERT INTO productos (nombre, img_producto, precio, activo, id_categoria) - VALUES (p_nombre, p_img_producto, p_precio, COALESCE(p_activo, TRUE), p_id_categoria) - RETURNING productos.id_producto INTO v_id; - ELSE - UPDATE productos p - SET nombre = p_nombre, - img_producto = p_img_producto, - precio = p_precio, - activo = COALESCE(p_activo, TRUE), - id_categoria = p_id_categoria - WHERE p.id_producto = p_id_producto; - v_id := p_id_producto; - END IF; - - -- Limpia receta actual - DELETE FROM receta_producto rp WHERE rp.id_producto = v_id; - - -- Inserta SOLO ítems válidos (id entero positivo y cantidad > 0), redondeo a 3 decimales - INSERT INTO receta_producto (id_producto, id_mat_prima, qty_por_unidad) - SELECT - v_id, - (rec->>'id_mat_prima')::INT, - ROUND((rec->>'qty_por_unidad')::NUMERIC, 3) - FROM jsonb_array_elements(COALESCE(p_receta, '[]'::jsonb)) AS rec - WHERE - (rec->>'id_mat_prima') ~ '^\d+$' - AND (rec->>'id_mat_prima')::INT > 0 - AND (rec->>'qty_por_unidad') ~ '^\d+(\.\d+)?$' - AND (rec->>'qty_por_unidad')::NUMERIC > 0; - - RETURN v_id; -END; -$_$; - --- --- Name: asistencia_intervalo; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE asistencia_intervalo ( - id_intervalo bigint NOT NULL, - id_usuario integer NOT NULL, - fecha date NOT NULL, - desde timestamp with time zone NOT NULL, - hasta timestamp with time zone NOT NULL, - dur_min numeric(10,2) NOT NULL, - origen text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT chk_ai_orden CHECK ((hasta > desde)) -); - --- --- Name: asistencia_intervalo_id_intervalo_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE asistencia_intervalo_id_intervalo_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: asistencia_intervalo_id_intervalo_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE asistencia_intervalo_id_intervalo_seq OWNED BY asistencia_intervalo.id_intervalo; - --- --- Name: asistencia_raw; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE asistencia_raw ( - id_raw bigint NOT NULL, - id_usuario integer NOT NULL, - ts timestamp with time zone NOT NULL, - modo text, - origen text, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: asistencia_raw_id_raw_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE asistencia_raw_id_raw_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: asistencia_raw_id_raw_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE asistencia_raw_id_raw_seq OWNED BY asistencia_raw.id_raw; - --- --- Name: asistencia_resumen_diario; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW asistencia_resumen_diario AS - SELECT ai.id_usuario, - u.documento, - u.nombre, - u.apellido, - ai.fecha, - sum(ai.dur_min) AS minutos_dia, - round((sum(ai.dur_min) / 60.0), 2) AS horas_dia, - count(*) AS pares_dia - FROM (asistencia_intervalo ai - JOIN usuarios u USING (id_usuario)) - GROUP BY ai.id_usuario, u.documento, u.nombre, u.apellido, ai.fecha - ORDER BY ai.id_usuario, ai.fecha; - --- --- Name: categorias; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE categorias ( - id_categoria integer NOT NULL, - nombre text NOT NULL, - visible boolean DEFAULT true -); - --- --- Name: categorias_id_categoria_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE categorias_id_categoria_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: categorias_id_categoria_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE categorias_id_categoria_seq OWNED BY categorias.id_categoria; - --- --- Name: clientes; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE clientes ( - id_cliente integer NOT NULL, - nombre text NOT NULL, - correo text, - telefono text, - fec_nacimiento date, - activo boolean DEFAULT true -); - --- --- Name: clientes_id_cliente_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE clientes_id_cliente_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: clientes_id_cliente_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE clientes_id_cliente_seq OWNED BY clientes.id_cliente; - --- --- Name: comandas_id_comanda_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE comandas_id_comanda_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: comandas_id_comanda_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE comandas_id_comanda_seq OWNED BY comandas.id_comanda; - --- --- Name: compras; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE compras ( - id_compra integer NOT NULL, - id_proveedor integer NOT NULL, - fec_compra timestamp without time zone NOT NULL, - total numeric(14,2) -); - --- --- Name: compras_id_compra_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE compras_id_compra_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: compras_id_compra_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE compras_id_compra_seq OWNED BY compras.id_compra; - --- --- Name: deta_comandas_id_det_comanda_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE deta_comandas_id_det_comanda_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: deta_comandas_id_det_comanda_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE deta_comandas_id_det_comanda_seq OWNED BY deta_comandas.id_det_comanda; - --- --- Name: deta_comp_materias; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE deta_comp_materias ( - id_compra integer NOT NULL, - id_mat_prima integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - CONSTRAINT deta_comp_materias_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comp_materias_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - --- --- Name: deta_comp_producto; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE deta_comp_producto ( - id_compra integer NOT NULL, - id_producto integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - CONSTRAINT deta_comp_producto_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comp_producto_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - --- --- Name: mate_primas; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE mate_primas ( - id_mat_prima integer NOT NULL, - nombre text NOT NULL, - unidad text NOT NULL, - activo boolean DEFAULT true -); - --- --- Name: mate_primas_id_mat_prima_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE mate_primas_id_mat_prima_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: mate_primas_id_mat_prima_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE mate_primas_id_mat_prima_seq OWNED BY mate_primas.id_mat_prima; - --- --- Name: mesas_id_mesa_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE mesas_id_mesa_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: mesas_id_mesa_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE mesas_id_mesa_seq OWNED BY mesas.id_mesa; - --- --- Name: productos; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE productos ( - id_producto integer NOT NULL, - nombre text NOT NULL, - img_producto character varying(255) DEFAULT 'img/productos/img_producto.png'::character varying NOT NULL, - precio integer NOT NULL, - activo boolean DEFAULT true, - id_categoria integer NOT NULL, - CONSTRAINT productos_precio_check CHECK (((precio)::numeric >= (0)::numeric)), - CONSTRAINT productos_precio_nn CHECK ((precio >= 0)) -); - --- --- Name: productos_id_producto_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE productos_id_producto_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: productos_id_producto_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE productos_id_producto_seq OWNED BY productos.id_producto; - --- --- Name: prov_mate_prima; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE prov_mate_prima ( - id_proveedor integer NOT NULL, - id_mat_prima integer NOT NULL -); - --- --- Name: prov_producto; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE prov_producto ( - id_proveedor integer NOT NULL, - id_producto integer NOT NULL -); - --- --- Name: proveedores; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE proveedores ( - id_proveedor integer NOT NULL, - rut text, - raz_social text NOT NULL, - direccion text, - contacto text -); - --- --- Name: proveedores_id_proveedor_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE proveedores_id_proveedor_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: proveedores_id_proveedor_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE proveedores_id_proveedor_seq OWNED BY proveedores.id_proveedor; - --- --- Name: receta_producto; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE receta_producto ( - id_producto integer NOT NULL, - id_mat_prima integer NOT NULL, - qty_por_unidad numeric(12,3) NOT NULL, - CONSTRAINT receta_producto_qty_por_unidad_check CHECK ((qty_por_unidad > (0)::numeric)) -); - --- --- Name: roles; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE roles ( - id_rol integer NOT NULL, - nombre text NOT NULL -); - --- --- Name: roles_id_rol_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE roles_id_rol_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: roles_id_rol_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE roles_id_rol_seq OWNED BY roles.id_rol; - --- --- Name: usua_roles; Type: TABLE; Schema: public; Owner: manso --- - -CREATE TABLE usua_roles ( - id_usuario integer NOT NULL, - id_rol integer NOT NULL, - fec_asignacion timestamp without time zone DEFAULT now(), - autor integer, - activo boolean DEFAULT true -); - --- --- Name: usuarios_id_usuario_seq; Type: SEQUENCE; Schema: public; Owner: manso --- - -CREATE SEQUENCE usuarios_id_usuario_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: usuarios_id_usuario_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: manso --- - -ALTER SEQUENCE usuarios_id_usuario_seq OWNED BY usuarios.id_usuario; - --- --- Name: v_comandas_detalle_base; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW v_comandas_detalle_base AS - SELECT c.id_comanda, - c.fec_creacion, - c.fec_cierre, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM ((((comandas c - JOIN usuarios u ON ((u.id_usuario = c.id_usuario))) - JOIN mesas m ON ((m.id_mesa = c.id_mesa))) - LEFT JOIN deta_comandas d ON ((d.id_comanda = c.id_comanda))) - LEFT JOIN productos p ON ((p.id_producto = d.id_producto))); - --- --- Name: v_comandas_detalle_items; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW v_comandas_detalle_items AS - SELECT d.id_comanda, - d.id_det_comanda, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal, - d.observaciones - FROM (deta_comandas d - JOIN productos p ON ((p.id_producto = d.id_producto))); - --- --- Name: v_comandas_detalle_json; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW v_comandas_detalle_json AS - SELECT id_comanda, - jsonb_build_object('id_comanda', id_comanda, 'fec_creacion', fec_creacion, 'fec_cierre', fec_cierre, 'estado', estado, 'observaciones', observaciones, 'usuario', jsonb_build_object('id_usuario', id_usuario, 'nombre', usuario_nombre, 'apellido', usuario_apellido), 'mesa', jsonb_build_object('id_mesa', id_mesa, 'numero', mesa_numero, 'apodo', mesa_apodo), 'items', COALESCE(( SELECT jsonb_agg(jsonb_build_object('producto_id', b.id_producto, 'producto', b.producto_nombre, 'cantidad', b.cantidad, 'pre_unitario', b.pre_unitario, 'subtotal', b.subtotal) ORDER BY b.producto_nombre) AS jsonb_agg - FROM v_comandas_detalle_base b - WHERE ((b.id_comanda = h.id_comanda) AND (b.id_producto IS NOT NULL))), '[]'::jsonb), 'totales', jsonb_build_object('items', COALESCE(( SELECT count(*) AS count - FROM v_comandas_detalle_base b - WHERE ((b.id_comanda = h.id_comanda) AND (b.id_producto IS NOT NULL))), (0)::bigint), 'total', COALESCE(( SELECT sum(b.subtotal) AS sum - FROM v_comandas_detalle_base b - WHERE (b.id_comanda = h.id_comanda)), (0)::numeric))) AS data - FROM ( SELECT DISTINCT v_comandas_detalle_base.id_comanda, - v_comandas_detalle_base.fec_creacion, - v_comandas_detalle_base.fec_cierre, - v_comandas_detalle_base.estado, - v_comandas_detalle_base.observaciones, - v_comandas_detalle_base.id_usuario, - v_comandas_detalle_base.usuario_nombre, - v_comandas_detalle_base.usuario_apellido, - v_comandas_detalle_base.id_mesa, - v_comandas_detalle_base.mesa_numero, - v_comandas_detalle_base.mesa_apodo - FROM v_comandas_detalle_base) h; - --- --- Name: vw_compras; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW vw_compras AS - SELECT c.id_compra, - c.id_proveedor, - p.raz_social AS proveedor, - c.fec_compra, - c.total - FROM (compras c - JOIN proveedores p USING (id_proveedor)) - ORDER BY c.fec_compra DESC, c.id_compra DESC; - --- --- Name: vw_ticket_total; Type: VIEW; Schema: public; Owner: manso --- - -CREATE VIEW vw_ticket_total AS - WITH lineas AS ( - SELECT c.id_comanda, - COALESCE(c.fec_cierre, (c.fec_creacion)::timestamp with time zone) AS fec_ticket, - (COALESCE(dc.pre_unitario, (p.precio)::numeric, (0)::numeric))::numeric(14,2) AS pu, - (COALESCE(dc.cantidad, (1)::numeric))::numeric(14,3) AS qty - FROM ((comandas c - JOIN deta_comandas dc ON ((dc.id_comanda = c.id_comanda))) - LEFT JOIN productos p ON ((p.id_producto = dc.id_producto))) - ) - SELECT id_comanda, - fec_ticket, - (sum((qty * pu)))::numeric(14,2) AS total - FROM lineas - GROUP BY id_comanda, fec_ticket; - --- --- Name: asistencia_intervalo id_intervalo; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY asistencia_intervalo ALTER COLUMN id_intervalo SET DEFAULT nextval('asistencia_intervalo_id_intervalo_seq'::regclass); - --- --- Name: asistencia_raw id_raw; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY asistencia_raw ALTER COLUMN id_raw SET DEFAULT nextval('asistencia_raw_id_raw_seq'::regclass); - --- --- Name: categorias id_categoria; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY categorias ALTER COLUMN id_categoria SET DEFAULT nextval('categorias_id_categoria_seq'::regclass); - --- --- Name: clientes id_cliente; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY clientes ALTER COLUMN id_cliente SET DEFAULT nextval('clientes_id_cliente_seq'::regclass); - --- --- Name: comandas id_comanda; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY comandas ALTER COLUMN id_comanda SET DEFAULT nextval('comandas_id_comanda_seq'::regclass); - --- --- Name: compras id_compra; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY compras ALTER COLUMN id_compra SET DEFAULT nextval('compras_id_compra_seq'::regclass); - --- --- Name: deta_comandas id_det_comanda; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY deta_comandas ALTER COLUMN id_det_comanda SET DEFAULT nextval('deta_comandas_id_det_comanda_seq'::regclass); - --- --- Name: mate_primas id_mat_prima; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY mate_primas ALTER COLUMN id_mat_prima SET DEFAULT nextval('mate_primas_id_mat_prima_seq'::regclass); - --- --- Name: mesas id_mesa; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY mesas ALTER COLUMN id_mesa SET DEFAULT nextval('mesas_id_mesa_seq'::regclass); - --- --- Name: productos id_producto; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY productos ALTER COLUMN id_producto SET DEFAULT nextval('productos_id_producto_seq'::regclass); - --- --- Name: proveedores id_proveedor; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY proveedores ALTER COLUMN id_proveedor SET DEFAULT nextval('proveedores_id_proveedor_seq'::regclass); - --- --- Name: roles id_rol; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY roles ALTER COLUMN id_rol SET DEFAULT nextval('roles_id_rol_seq'::regclass); - --- --- Name: usuarios id_usuario; Type: DEFAULT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY usuarios ALTER COLUMN id_usuario SET DEFAULT nextval('usuarios_id_usuario_seq'::regclass); - --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- Name: asistencia_intervalo_id_intervalo_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('asistencia_intervalo_id_intervalo_seq', 108, true); - --- --- Name: asistencia_raw_id_raw_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('asistencia_raw_id_raw_seq', 88, true); - --- --- Name: categorias_id_categoria_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('categorias_id_categoria_seq', 4, true); - --- --- Name: clientes_id_cliente_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('clientes_id_cliente_seq', 1, true); - --- --- Name: comandas_id_comanda_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('comandas_id_comanda_seq', 55, true); - --- --- Name: compras_id_compra_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('compras_id_compra_seq', 66, true); - --- --- Name: deta_comandas_id_det_comanda_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('deta_comandas_id_det_comanda_seq', 117, true); - --- --- Name: mate_primas_id_mat_prima_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('mate_primas_id_mat_prima_seq', 10, true); - --- --- Name: mesas_id_mesa_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('mesas_id_mesa_seq', 14, true); - --- --- Name: productos_id_producto_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('productos_id_producto_seq', 52, true); - --- --- Name: proveedores_id_proveedor_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('proveedores_id_proveedor_seq', 4, true); - --- --- Name: roles_id_rol_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('roles_id_rol_seq', 6, true); - --- --- Name: usuarios_id_usuario_seq; Type: SEQUENCE SET; Schema: public; Owner: manso --- - -SELECT pg_catalog.setval('usuarios_id_usuario_seq', 3, true); - --- --- Name: asistencia_intervalo asistencia_intervalo_id_usuario_desde_hasta_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_id_usuario_desde_hasta_key UNIQUE (id_usuario, desde, hasta); - --- --- Name: asistencia_intervalo asistencia_intervalo_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_pkey PRIMARY KEY (id_intervalo); - --- --- Name: asistencia_raw asistencia_raw_id_usuario_ts_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY asistencia_raw - ADD CONSTRAINT asistencia_raw_id_usuario_ts_key UNIQUE (id_usuario, ts); - --- --- Name: asistencia_raw asistencia_raw_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY asistencia_raw - ADD CONSTRAINT asistencia_raw_pkey PRIMARY KEY (id_raw); - --- --- Name: categorias categorias_nombre_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY categorias - ADD CONSTRAINT categorias_nombre_key UNIQUE (nombre); - --- --- Name: categorias categorias_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY categorias - ADD CONSTRAINT categorias_pkey PRIMARY KEY (id_categoria); - --- --- Name: clientes clientes_correo_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY clientes - ADD CONSTRAINT clientes_correo_key UNIQUE (correo); - --- --- Name: clientes clientes_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY clientes - ADD CONSTRAINT clientes_pkey PRIMARY KEY (id_cliente); - --- --- Name: clientes clientes_telefono_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY clientes - ADD CONSTRAINT clientes_telefono_key UNIQUE (telefono); - --- --- Name: comandas comandas_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY comandas - ADD CONSTRAINT comandas_pkey PRIMARY KEY (id_comanda); - --- --- Name: compras compras_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY compras - ADD CONSTRAINT compras_pkey PRIMARY KEY (id_compra); - --- --- Name: deta_comandas deta_comandas_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY deta_comandas - ADD CONSTRAINT deta_comandas_pkey PRIMARY KEY (id_det_comanda); - --- --- Name: deta_comp_materias deta_comp_materias_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY deta_comp_materias - ADD CONSTRAINT deta_comp_materias_pkey PRIMARY KEY (id_compra, id_mat_prima); - --- --- Name: deta_comp_producto deta_comp_producto_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY deta_comp_producto - ADD CONSTRAINT deta_comp_producto_pkey PRIMARY KEY (id_compra, id_producto); - --- --- Name: mate_primas mate_primas_nombre_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY mate_primas - ADD CONSTRAINT mate_primas_nombre_key UNIQUE (nombre); - --- --- Name: mate_primas mate_primas_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY mate_primas - ADD CONSTRAINT mate_primas_pkey PRIMARY KEY (id_mat_prima); - --- --- Name: mesas mesas_apodo_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY mesas - ADD CONSTRAINT mesas_apodo_key UNIQUE (apodo); - --- --- Name: mesas mesas_numero_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY mesas - ADD CONSTRAINT mesas_numero_key UNIQUE (numero); - --- --- Name: mesas mesas_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY mesas - ADD CONSTRAINT mesas_pkey PRIMARY KEY (id_mesa); - --- --- Name: productos productos_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY productos - ADD CONSTRAINT productos_pkey PRIMARY KEY (id_producto); - --- --- Name: prov_mate_prima prov_mate_prima_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY prov_mate_prima - ADD CONSTRAINT prov_mate_prima_pkey PRIMARY KEY (id_proveedor, id_mat_prima); - --- --- Name: prov_producto prov_producto_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY prov_producto - ADD CONSTRAINT prov_producto_pkey PRIMARY KEY (id_proveedor, id_producto); - --- --- Name: proveedores proveedores_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY proveedores - ADD CONSTRAINT proveedores_pkey PRIMARY KEY (id_proveedor); - --- --- Name: proveedores proveedores_rut_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY proveedores - ADD CONSTRAINT proveedores_rut_key UNIQUE (rut); - --- --- Name: receta_producto receta_producto_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY receta_producto - ADD CONSTRAINT receta_producto_pkey PRIMARY KEY (id_producto, id_mat_prima); - --- --- Name: roles roles_nombre_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY roles - ADD CONSTRAINT roles_nombre_key UNIQUE (nombre); - --- --- Name: roles roles_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY roles - ADD CONSTRAINT roles_pkey PRIMARY KEY (id_rol); - --- --- Name: usua_roles usua_roles_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY usua_roles - ADD CONSTRAINT usua_roles_pkey PRIMARY KEY (id_usuario, id_rol); - --- --- Name: usuarios usuarios_documento_key; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY usuarios - ADD CONSTRAINT usuarios_documento_key UNIQUE (documento); - --- --- Name: usuarios usuarios_pkey; Type: CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY usuarios - ADD CONSTRAINT usuarios_pkey PRIMARY KEY (id_usuario); - --- --- Name: compras_fec_compra_idx; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX compras_fec_compra_idx ON compras USING btree (fec_compra); - --- --- Name: idx_asist_int_usuario_fecha; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX idx_asist_int_usuario_fecha ON asistencia_intervalo USING btree (id_usuario, fecha); - --- --- Name: idx_asist_raw_usuario_ts; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX idx_asist_raw_usuario_ts ON asistencia_raw USING btree (id_usuario, ts); - --- --- Name: idx_detalle_comanda_comanda; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX idx_detalle_comanda_comanda ON deta_comandas USING btree (id_comanda); - --- --- Name: idx_detalle_comanda_producto; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX idx_detalle_comanda_producto ON deta_comandas USING btree (id_producto); - --- --- Name: ix_comandas_fec_cierre; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX ix_comandas_fec_cierre ON comandas USING btree (fec_cierre); - --- --- Name: ix_comandas_id; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX ix_comandas_id ON comandas USING btree (id_comanda); - --- --- Name: ix_deta_comandas_id_comanda; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX ix_deta_comandas_id_comanda ON deta_comandas USING btree (id_comanda); - --- --- Name: ix_deta_comandas_id_producto; Type: INDEX; Schema: public; Owner: manso --- - -CREATE INDEX ix_deta_comandas_id_producto ON deta_comandas USING btree (id_producto); - --- --- Name: asistencia_intervalo asistencia_intervalo_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES usuarios(id_usuario) ON DELETE CASCADE; - --- --- Name: asistencia_raw asistencia_raw_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY asistencia_raw - ADD CONSTRAINT asistencia_raw_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES usuarios(id_usuario) ON DELETE CASCADE; - --- --- Name: comandas comandas_id_mesa_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY comandas - ADD CONSTRAINT comandas_id_mesa_fkey FOREIGN KEY (id_mesa) REFERENCES mesas(id_mesa); - --- --- Name: comandas comandas_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY comandas - ADD CONSTRAINT comandas_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES usuarios(id_usuario); - --- --- Name: compras compras_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY compras - ADD CONSTRAINT compras_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES proveedores(id_proveedor); - --- --- Name: deta_comandas deta_comandas_id_comanda_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY deta_comandas - ADD CONSTRAINT deta_comandas_id_comanda_fkey FOREIGN KEY (id_comanda) REFERENCES comandas(id_comanda) ON DELETE CASCADE; - --- --- Name: deta_comandas deta_comandas_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY deta_comandas - ADD CONSTRAINT deta_comandas_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES productos(id_producto); - --- --- Name: deta_comp_materias deta_comp_materias_id_compra_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY deta_comp_materias - ADD CONSTRAINT deta_comp_materias_id_compra_fkey FOREIGN KEY (id_compra) REFERENCES compras(id_compra) ON DELETE CASCADE; - --- --- Name: deta_comp_materias deta_comp_materias_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY deta_comp_materias - ADD CONSTRAINT deta_comp_materias_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES mate_primas(id_mat_prima); - --- --- Name: deta_comp_producto deta_comp_producto_id_compra_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY deta_comp_producto - ADD CONSTRAINT deta_comp_producto_id_compra_fkey FOREIGN KEY (id_compra) REFERENCES compras(id_compra) ON DELETE CASCADE; - --- --- Name: deta_comp_producto deta_comp_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY deta_comp_producto - ADD CONSTRAINT deta_comp_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES productos(id_producto); - --- --- Name: productos productos_id_categoria_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY productos - ADD CONSTRAINT productos_id_categoria_fkey FOREIGN KEY (id_categoria) REFERENCES categorias(id_categoria); - --- --- Name: prov_mate_prima prov_mate_prima_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY prov_mate_prima - ADD CONSTRAINT prov_mate_prima_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES mate_primas(id_mat_prima); - --- --- Name: prov_mate_prima prov_mate_prima_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY prov_mate_prima - ADD CONSTRAINT prov_mate_prima_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES proveedores(id_proveedor) ON DELETE CASCADE; - --- --- Name: prov_producto prov_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY prov_producto - ADD CONSTRAINT prov_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES productos(id_producto); - --- --- Name: prov_producto prov_producto_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY prov_producto - ADD CONSTRAINT prov_producto_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES proveedores(id_proveedor) ON DELETE CASCADE; - --- --- Name: receta_producto receta_producto_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY receta_producto - ADD CONSTRAINT receta_producto_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES mate_primas(id_mat_prima); - --- --- Name: receta_producto receta_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY receta_producto - ADD CONSTRAINT receta_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES productos(id_producto) ON DELETE CASCADE; - --- --- Name: usua_roles usua_roles_autor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY usua_roles - ADD CONSTRAINT usua_roles_autor_fkey FOREIGN KEY (autor) REFERENCES usuarios(id_usuario); - --- --- Name: usua_roles usua_roles_id_rol_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY usua_roles - ADD CONSTRAINT usua_roles_id_rol_fkey FOREIGN KEY (id_rol) REFERENCES roles(id_rol); - --- --- Name: usua_roles usua_roles_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: manso --- - -ALTER TABLE ONLY usua_roles - ADD CONSTRAINT usua_roles_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES usuarios(id_usuario) ON DELETE CASCADE; - --- --- PostgreSQL database dump complete --- - -\unrestrict londHmqT4llS8Wof4ZnceO2dyFhn4jiR5xbaszMgZpMczgr6aVW6xQJxeUdqJwa - - -COMMIT; diff --git a/services/auth/src/db/initTenant_v2.sql b/services/auth/src/db/initTenant_v2.sql deleted file mode 100644 index 2081ece..0000000 --- a/services/auth/src/db/initTenant_v2.sql +++ /dev/null @@ -1,2267 +0,0 @@ --- --- PostgreSQL database dump --- - - --- Dumped from database version 16.10 (Debian 16.10-1.pgdg13+1) --- Dumped by pg_dump version 16.10 (Debian 16.10-1.pgdg13+1) - -BEGIN; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SET row_security = off; -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - - -CREATE FUNCTION asistencia_delete_raw(p_id_raw bigint, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE - v_id_usuario INT; - v_ts TIMESTAMPTZ; - v_t0 TIMESTAMPTZ; - v_t1 TIMESTAMPTZ; - v_del_raw INT; - v_del INT; - v_ins INT; -BEGIN - SELECT id_usuario, ts INTO v_id_usuario, v_ts - FROM asistencia_raw WHERE id_raw = p_id_raw; - IF v_id_usuario IS NULL THEN - RETURN jsonb_build_object('deleted',0,'msg','id_raw inexistente'); - END IF; - - v_t0 := v_ts - INTERVAL '1 day'; - v_t1 := v_ts + INTERVAL '1 day'; - - -- borrar raw - DELETE FROM asistencia_raw WHERE id_raw = p_id_raw; - GET DIAGNOSTICS v_del_raw = ROW_COUNT; - - -- recomputar pares en ventana - WITH tl AS ( - SELECT ar.ts, - ROW_NUMBER() OVER (ORDER BY ar.ts) AS rn - FROM asistencia_raw ar - WHERE ar.id_usuario = v_id_usuario - AND ar.ts BETWEEN v_t0 AND v_t1 - ), - ready AS ( - SELECT (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM tl t1 - JOIN tl t2 ON t2.rn = t1.rn + 1 - WHERE (t1.rn % 2) = 1 AND t2.ts > t1.ts - ), - del AS ( - DELETE FROM asistencia_intervalo ai - WHERE ai.id_usuario = v_id_usuario - AND (ai.desde BETWEEN v_t0 AND v_t1 OR ai.hasta BETWEEN v_t0 AND v_t1) - RETURNING 1 - ), - ins AS ( - INSERT INTO asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT v_id_usuario, r.fecha, r.desde, r.hasta, r.dur_min, 'delete_adjust' - FROM ready r - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT (SELECT COUNT(*) FROM del), (SELECT COUNT(*) FROM ins) INTO v_del, v_ins; - - RETURN jsonb_build_object('deleted',v_del_raw,'deleted_pairs',v_del,'inserted_pairs',v_ins); -END; -$$; - - --- --- Name: asistencia_get(text, date, date, text); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION asistencia_get(p_doc text, p_desde date, p_hasta date, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH u AS ( - SELECT id_usuario, documento, nombre, apellido - FROM usuarios - WHERE regexp_replace(documento,'^\s*0+','','g') = regexp_replace(p_doc,'^\s*0+','','g') - LIMIT 1 -), -r AS ( - SELECT ar.id_raw, - (ar.ts AT TIME ZONE p_tz)::date AS fecha, - to_char(ar.ts AT TIME ZONE p_tz,'HH24:MI:SS') AS hora, - COALESCE(ar.modo,'') AS modo, - COALESCE(ar.origen,'') AS origen, - ar.ts - FROM asistencia_raw ar - JOIN u USING (id_usuario) - WHERE (ar.ts AT TIME ZONE p_tz)::date BETWEEN p_desde AND p_hasta -), -i AS ( - SELECT ai.id_intervalo, - ai.fecha, - to_char(ai.desde AT TIME ZONE p_tz,'HH24:MI:SS') AS desde_hora, - to_char(ai.hasta AT TIME ZONE p_tz,'HH24:MI:SS') AS hasta_hora, - ai.dur_min - FROM asistencia_intervalo ai - JOIN u USING (id_usuario) - WHERE ai.fecha BETWEEN p_desde AND p_hasta -) -SELECT jsonb_build_object( - 'usuario', (SELECT to_jsonb(u.*) FROM u), - 'raw', COALESCE((SELECT jsonb_agg(to_jsonb(r.*) ORDER BY r.ts) FROM r),'[]'::jsonb), - 'intervalos', COALESCE((SELECT jsonb_agg(to_jsonb(i.*) ORDER BY i.fecha, i.id_intervalo) FROM i),'[]'::jsonb) -); -$$; - - --- --- Name: asistencia_update_raw(bigint, date, text, text, text); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION asistencia_update_raw(p_id_raw bigint, p_fecha date, p_hora text, p_modo text, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE - v_id_usuario INT; - v_ts_old TIMESTAMPTZ; - v_ts_new TIMESTAMPTZ; - v_t0 TIMESTAMPTZ; - v_t1 TIMESTAMPTZ; - v_del INT; - v_ins INT; -BEGIN - -- leer estado previo - SELECT id_usuario, ts INTO v_id_usuario, v_ts_old - FROM asistencia_raw WHERE id_raw = p_id_raw; - IF v_id_usuario IS NULL THEN - RETURN jsonb_build_object('updated',0,'msg','id_raw inexistente'); - END IF; - - -- construir ts nuevo - v_ts_new := make_timestamptz( - EXTRACT(YEAR FROM p_fecha)::INT, - EXTRACT(MONTH FROM p_fecha)::INT, - EXTRACT(DAY FROM p_fecha)::INT, - split_part(p_hora,':',1)::INT, - split_part(p_hora,':',2)::INT, - COALESCE(NULLIF(split_part(p_hora,':',3),''), '0')::INT, - p_tz); - - -- aplicar update - UPDATE asistencia_raw - SET ts = v_ts_new, - modo = COALESCE(p_modo, modo) - WHERE id_raw = p_id_raw; - - -- ventana de recálculo - v_t0 := LEAST(v_ts_old, v_ts_new) - INTERVAL '1 day'; - v_t1 := GREATEST(v_ts_old, v_ts_new) + INTERVAL '1 day'; - - -- recomputar pares en la ventana: borrar los del rango y reinsertar - WITH tl AS ( - SELECT ar.ts, - ROW_NUMBER() OVER (ORDER BY ar.ts) AS rn - FROM asistencia_raw ar - WHERE ar.id_usuario = v_id_usuario - AND ar.ts BETWEEN v_t0 AND v_t1 - ), - ready AS ( - SELECT (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM tl t1 - JOIN tl t2 ON t2.rn = t1.rn + 1 - WHERE (t1.rn % 2) = 1 AND t2.ts > t1.ts - ), - del AS ( - DELETE FROM asistencia_intervalo ai - WHERE ai.id_usuario = v_id_usuario - AND (ai.desde BETWEEN v_t0 AND v_t1 OR ai.hasta BETWEEN v_t0 AND v_t1) - RETURNING 1 - ), - ins AS ( - INSERT INTO asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT v_id_usuario, r.fecha, r.desde, r.hasta, r.dur_min, 'edit_manual' - FROM ready r - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT (SELECT COUNT(*) FROM del), (SELECT COUNT(*) FROM ins) INTO v_del, v_ins; - - RETURN jsonb_build_object('updated',1,'deleted_pairs',v_del,'inserted_pairs',v_ins); -END; -$$; - - --- --- Name: delete_compra(integer); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION delete_compra(p_id_compra integer) RETURNS void - LANGUAGE plpgsql - AS $$ -BEGIN - DELETE FROM deta_comp_materias WHERE id_compra = p_id_compra; - DELETE FROM deta_comp_producto WHERE id_compra = p_id_compra; - DELETE FROM compras WHERE id_compra = p_id_compra; -END; -$$; - - --- --- Name: f_abrir_comanda(integer); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION f_abrir_comanda(p_id integer) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE r jsonb; -BEGIN - UPDATE comandas - SET estado = 'abierta', - fec_cierre = NULL - WHERE id_comanda = p_id; - - IF NOT FOUND THEN - RETURN NULL; - END IF; - - SELECT to_jsonb(v) INTO r - FROM v_comandas_resumen v - WHERE v.id_comanda = p_id; - - RETURN r; -END; -$$; - - --- --- Name: f_cerrar_comanda(integer); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION f_cerrar_comanda(p_id integer) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE r jsonb; -BEGIN - UPDATE comandas - SET estado = 'cerrada', - fec_cierre = COALESCE(fec_cierre, NOW()) - WHERE id_comanda = p_id; - - IF NOT FOUND THEN - RETURN NULL; - END IF; - - SELECT to_jsonb(v) INTO r - FROM v_comandas_resumen v - WHERE v.id_comanda = p_id; - - RETURN r; -END; -$$; - - --- --- Name: f_comanda_detalle_json(integer); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION f_comanda_detalle_json(p_id_comanda integer) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH base AS ( - SELECT - c.id_comanda, - c.fec_creacion, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM comandas c - JOIN usuarios u ON u.id_usuario = c.id_usuario - JOIN mesas m ON m.id_mesa = c.id_mesa - LEFT JOIN deta_comandas d ON d.id_comanda = c.id_comanda - LEFT JOIN productos p ON p.id_producto = d.id_producto - WHERE c.id_comanda = p_id_comanda -), -hdr AS ( - -- 1 sola fila con los datos de cabecera - SELECT DISTINCT - id_comanda, fec_creacion, estado, observaciones, - id_usuario, usuario_nombre, usuario_apellido, - id_mesa, mesa_numero, mesa_apodo - FROM base -), -agg_items AS ( - SELECT - COALESCE( - jsonb_agg( - jsonb_build_object( - 'producto_id', b.id_producto, - 'producto', b.producto_nombre, - 'cantidad', b.cantidad, - 'pre_unitario', b.pre_unitario, - 'subtotal', b.subtotal - ) - ORDER BY b.producto_nombre NULLS LAST - ) FILTER (WHERE b.id_producto IS NOT NULL), - '[]'::jsonb - ) AS items - FROM base b -), -tot AS ( - SELECT - COUNT(*) FILTER (WHERE id_producto IS NOT NULL) AS items, - COALESCE(SUM(subtotal), 0)::numeric AS total - FROM base -) -SELECT - CASE - WHEN EXISTS (SELECT 1 FROM hdr) THEN - jsonb_build_object( - 'id_comanda', h.id_comanda, - 'fec_creacion', h.fec_creacion, - 'estado', h.estado, - 'observaciones',h.observaciones, - 'usuario', jsonb_build_object( - 'id_usuario', h.id_usuario, - 'nombre', h.usuario_nombre, - 'apellido', h.usuario_apellido - ), - 'mesa', jsonb_build_object( - 'id_mesa', h.id_mesa, - 'numero', h.mesa_numero, - 'apodo', h.mesa_apodo - ), - 'items', i.items, - 'totales', jsonb_build_object( - 'items', t.items, - 'total', t.total - ) - ) - ELSE NULL - END -FROM hdr h, agg_items i, tot t; -$$; - - --- --- Name: f_comanda_detalle_rows(integer); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION f_comanda_detalle_rows(p_id_comanda integer) RETURNS TABLE(id_comanda integer, fec_creacion timestamp without time zone, estado text, observaciones text, id_usuario integer, usuario_nombre text, usuario_apellido text, id_mesa integer, mesa_numero integer, mesa_apodo text, producto_id integer, producto_nombre text, cantidad numeric, pre_unitario numeric, subtotal numeric, items integer, total numeric) - LANGUAGE sql - AS $$ -WITH base AS ( - SELECT - c.id_comanda, c.fec_creacion, c.estado, c.observaciones, - u.id_usuario, u.nombre AS usuario_nombre, u.apellido AS usuario_apellido, - m.id_mesa, m.numero AS mesa_numero, m.apodo AS mesa_apodo, - d.id_producto, p.nombre AS producto_nombre, - d.cantidad, d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM comandas c - JOIN usuarios u ON u.id_usuario = c.id_usuario - JOIN mesas m ON m.id_mesa = c.id_mesa - LEFT JOIN deta_comandas d ON d.id_comanda = c.id_comanda - LEFT JOIN productos p ON p.id_producto = d.id_producto - WHERE c.id_comanda = p_id_comanda -), -tot AS ( - SELECT - COUNT(*) FILTER (WHERE id_producto IS NOT NULL) AS items, - COALESCE(SUM(subtotal), 0) AS total - FROM base -) -SELECT - b.id_comanda, b.fec_creacion, b.estado, b.observaciones, - b.id_usuario, b.usuario_nombre, b.usuario_apellido, - b.id_mesa, b.mesa_numero, b.mesa_apodo, - b.id_producto, b.producto_nombre, - b.cantidad, b.pre_unitario, b.subtotal, - t.items, t.total -FROM base b CROSS JOIN tot t -ORDER BY b.producto_nombre NULLS LAST; -$$; - - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: comandas; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE comandas ( - id_comanda integer NOT NULL, - id_usuario integer NOT NULL, - id_mesa integer NOT NULL, - fec_creacion timestamp without time zone DEFAULT now() NOT NULL, - estado text NOT NULL, - observaciones text, - fec_cierre timestamp with time zone, - CONSTRAINT comandas_estado_check CHECK ((estado = ANY (ARRAY['abierta'::text, 'cerrada'::text, 'pagada'::text, 'anulada'::text]))) -); - - --- --- Name: COLUMN comandas.fec_cierre; Type: COMMENT; Schema: ; Owner: - --- - -COMMENT ON COLUMN comandas.fec_cierre IS 'Fecha/hora de cierre de la comanda (NULL si está abierta)'; - - --- --- Name: deta_comandas; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE deta_comandas ( - id_det_comanda integer NOT NULL, - id_comanda integer NOT NULL, - id_producto integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - observaciones text, - CONSTRAINT deta_comandas_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comandas_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - - --- --- Name: mesas; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE mesas ( - id_mesa integer NOT NULL, - numero integer NOT NULL, - apodo text NOT NULL, - estado text DEFAULT 'libre'::text NOT NULL, - CONSTRAINT mesas_estado_check CHECK ((estado = ANY (ARRAY['libre'::text, 'ocupada'::text, 'reservada'::text]))) -); - - --- --- Name: usuarios; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE usuarios ( - id_usuario integer NOT NULL, - documento text, - img_perfil character varying(255) DEFAULT 'img_perfil.png'::character varying NOT NULL, - nombre text NOT NULL, - apellido text NOT NULL, - correo text, - telefono text, - fec_nacimiento date, - activo boolean DEFAULT true -); - - --- --- Name: v_comandas_resumen; Type: VIEW; Schema: ; Owner: - --- - -CREATE VIEW v_comandas_resumen AS - WITH items AS ( - SELECT d.id_comanda, - count(*) AS items, - sum((d.cantidad * d.pre_unitario)) AS total - FROM deta_comandas d - GROUP BY d.id_comanda - ) - SELECT c.id_comanda, - c.fec_creacion, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - COALESCE(i.items, (0)::bigint) AS items, - COALESCE(i.total, (0)::numeric) AS total, - c.fec_cierre, - CASE - WHEN (c.fec_cierre IS NOT NULL) THEN round((EXTRACT(epoch FROM (c.fec_cierre - (c.fec_creacion)::timestamp with time zone)) / 60.0), 1) - ELSE NULL::numeric - END AS duracion_min - FROM (((comandas c - JOIN usuarios u ON ((u.id_usuario = c.id_usuario))) - JOIN mesas m ON ((m.id_mesa = c.id_mesa))) - LEFT JOIN items i ON ((i.id_comanda = c.id_comanda))); - - --- --- Name: f_comandas_resumen(text, integer); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION f_comandas_resumen(p_estado text DEFAULT NULL::text, p_limit integer DEFAULT 200) RETURNS SETOF v_comandas_resumen - LANGUAGE sql - AS $$ - SELECT * - FROM v_comandas_resumen - WHERE (p_estado IS NULL OR estado = p_estado) - ORDER BY id_comanda DESC - LIMIT p_limit; -$$; - - --- --- Name: find_usuarios_por_documentos(jsonb); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION find_usuarios_por_documentos(p_docs jsonb) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH docs AS ( - SELECT DISTINCT - regexp_replace(value::text, '^\s*0+', '', 'g') AS doc_clean, - value::text AS original - FROM jsonb_array_elements_text(COALESCE(p_docs,'[]')) -), -rows AS ( - SELECT d.original AS documento, - u.nombre, - u.apellido, - (u.id_usuario IS NOT NULL) AS found - FROM docs d - LEFT JOIN usuarios u - ON regexp_replace(u.documento, '^\s*0+', '', 'g') = d.doc_clean -) -SELECT COALESCE( - jsonb_object_agg( - documento, - jsonb_build_object( - 'nombre', COALESCE(nombre, ''), - 'apellido', COALESCE(apellido, ''), - 'found', found - ) - ), - '{}'::jsonb -) -FROM rows; -$$; - - --- --- Name: get_compra(integer); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION get_compra(p_id_compra integer) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH cab AS ( - SELECT c.id_compra, c.id_proveedor, c.fec_compra, c.total - FROM compras c - WHERE c.id_compra = p_id_compra -), -dm AS ( - SELECT 'MAT'::text AS tipo, d.id_mat_prima AS id, - d.cantidad, d.pre_unitario AS precio - FROM deta_comp_materias d WHERE d.id_compra = p_id_compra -), -dp AS ( - SELECT 'PROD'::text AS tipo, d.id_producto AS id, - d.cantidad, d.pre_unitario AS precio - FROM deta_comp_producto d WHERE d.id_compra = p_id_compra -), -det AS ( - SELECT jsonb_agg(to_jsonb(x.*)) AS detalles - FROM ( - SELECT * FROM dm - UNION ALL - SELECT * FROM dp - ) x -) -SELECT jsonb_build_object( - 'id_compra', (SELECT id_compra FROM cab), - 'id_proveedor',(SELECT id_proveedor FROM cab), - 'fec_compra', to_char((SELECT fec_compra FROM cab),'YYYY-MM-DD HH24:MI:SS'), - 'total', (SELECT total FROM cab), - 'detalles', COALESCE((SELECT detalles FROM det),'[]'::jsonb) -); -$$; - - --- --- Name: get_materia_prima(integer); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION get_materia_prima(p_id integer) RETURNS jsonb - LANGUAGE sql - AS $$ -SELECT jsonb_build_object( - 'materia', to_jsonb(mp), - 'proveedores', COALESCE( - ( - SELECT jsonb_agg( - jsonb_build_object( - 'id_proveedor', pr.id_proveedor, - 'raz_social', pr.raz_social, - 'rut', pr.rut, - 'contacto', pr.contacto, - 'direccion', pr.direccion - ) - ) - FROM prov_mate_prima pmp - JOIN proveedores pr ON pr.id_proveedor = pmp.id_proveedor - WHERE pmp.id_mat_prima = mp.id_mat_prima - ), - '[]'::jsonb - ) -) -FROM mate_primas mp -WHERE mp.id_mat_prima = p_id; -$$; - - --- --- Name: get_producto(integer); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION get_producto(p_id integer) RETURNS jsonb - LANGUAGE sql - AS $$ -SELECT jsonb_build_object( - 'producto', to_jsonb(p), -- el registro completo del producto en JSONB - 'receta', COALESCE( - ( - SELECT jsonb_agg( - jsonb_build_object( - 'id_mat_prima', rp.id_mat_prima, - 'qty_por_unidad', rp.qty_por_unidad, - 'nombre', mp.nombre, - 'unidad', mp.unidad - ) - ) - FROM receta_producto rp - LEFT JOIN mate_primas mp USING (id_mat_prima) - WHERE rp.id_producto = p.id_producto - ), - '[]'::jsonb - ) -) -FROM productos p -WHERE p.id_producto = p_id; -$$; - - --- --- Name: import_asistencia(jsonb, text, text); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION import_asistencia(p_registros jsonb, p_origen text, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $_$ -DECLARE - v_ins_raw INT; - v_ins_pairs INT; - v_miss JSONB; -BEGIN - WITH - -- 1) JSON -> filas - j AS ( - SELECT - regexp_replace(elem->>'doc','^\s*0+','','g')::TEXT AS doc_clean, - (elem->>'isoDate')::DATE AS d, - elem->>'time' AS time_str, - NULLIF(elem->>'mode','') AS modo - FROM jsonb_array_elements(COALESCE(p_registros,'[]')) elem - ), - -- 2) Vincular a usuarios - u AS ( - SELECT j.*, u.id_usuario - FROM j - LEFT JOIN usuarios u - ON regexp_replace(u.documento,'^\s*0+','','g') = j.doc_clean - ), - -- 3) Documentos faltantes - miss AS ( - SELECT jsonb_agg(doc_clean) AS missing - FROM u WHERE id_usuario IS NULL - ), - -- 4) TS determinista en TZ del negocio - parsed AS ( - SELECT - u.id_usuario, - u.modo, - make_timestamptz( - EXTRACT(YEAR FROM u.d)::INT, - EXTRACT(MONTH FROM u.d)::INT, - EXTRACT(DAY FROM u.d)::INT, - split_part(u.time_str,':',1)::INT, - split_part(u.time_str,':',2)::INT, - COALESCE(NULLIF(split_part(u.time_str,':',3),''),'0')::INT, - p_tz - ) AS ts_calc - FROM u - WHERE u.id_usuario IS NOT NULL - ), - -- 5) Ventana por usuario (±1 día de lo importado) - win AS ( - SELECT id_usuario, - (MIN(ts_calc) - INTERVAL '1 day') AS t0, - (MAX(ts_calc) + INTERVAL '1 day') AS t1 - FROM parsed - GROUP BY id_usuario - ), - -- 6) Lo existente en BD dentro de la ventana - existing AS ( - SELECT ar.id_usuario, ar.ts - FROM asistencia_raw ar - JOIN win w ON w.id_usuario = ar.id_usuario - AND ar.ts BETWEEN w.t0 AND w.t1 - ), - -- 7) CANDIDATE = existente ∪ archivo (sin duplicados) - candidate AS ( - SELECT id_usuario, ts FROM existing - UNION -- ¡clave para evitar doble click! - SELECT id_usuario, ts_calc AS ts FROM parsed - ), - -- 8) Paridad previa (cuántas marcas había ANTES de la ventana) - before_cnt AS ( - SELECT w.id_usuario, COUNT(*)::int AS cnt - FROM win w - JOIN asistencia_raw ar - ON ar.id_usuario = w.id_usuario - AND ar.ts < w.t0 - GROUP BY w.id_usuario - ), - -- 9) Línea de tiempo candidata y pares (1→2, 3→4…), jornada = día local del inicio - timeline AS ( - SELECT - c.id_usuario, - c.ts, - ROW_NUMBER() OVER (PARTITION BY c.id_usuario ORDER BY c.ts) AS rn - FROM candidate c - ), - ready AS ( - SELECT - t1.id_usuario, - (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM timeline t1 - JOIN timeline t2 - ON t2.id_usuario = t1.id_usuario - AND t2.rn = t1.rn + 1 - LEFT JOIN before_cnt b ON b.id_usuario = t1.id_usuario - WHERE ((COALESCE(b.cnt,0) + t1.rn) % 2) = 1 -- t1 es IN global - AND t2.ts > t1.ts - ), - -- 10) INSERT crudo (dedupe) - ins_raw AS ( - INSERT INTO asistencia_raw (id_usuario, ts, modo, origen) - SELECT id_usuario, ts_calc, - NULLIF(modo,'')::text, -- puede quedar NULL para auto-etiquetado - p_origen - FROM parsed - ON CONFLICT (id_usuario, ts) DO NOTHING - RETURNING 1 - ), - -- 11) Auto-etiquetar IN/OUT en BD para filas con modo vacío/'1' (tras insertar) - before_cnt2 AS ( - SELECT w.id_usuario, COUNT(*)::int AS cnt - FROM win w - JOIN asistencia_raw ar - ON ar.id_usuario = w.id_usuario - AND ar.ts < w.t0 - GROUP BY w.id_usuario - ), - tl2 AS ( - SELECT - ar.id_usuario, ar.ts, - ROW_NUMBER() OVER (PARTITION BY ar.id_usuario ORDER BY ar.ts) AS rn - FROM asistencia_raw ar - JOIN win w ON w.id_usuario = ar.id_usuario - AND ar.ts BETWEEN w.t0 AND w.t1 - ), - label2 AS ( - SELECT - t.id_usuario, - t.ts, - CASE WHEN ((COALESCE(b.cnt,0) + t.rn) % 2) = 1 THEN 'IN' ELSE 'OUT' END AS new_mode - FROM tl2 t - LEFT JOIN before_cnt2 b ON b.id_usuario = t.id_usuario - ), - set_mode AS ( - UPDATE asistencia_raw ar - SET modo = l.new_mode - FROM label2 l - WHERE ar.id_usuario = l.id_usuario - AND ar.ts = l.ts - AND (ar.modo IS NULL OR btrim(ar.modo) = '' OR ar.modo ~ '^\s*1\s*$') - RETURNING 1 - ), - -- 12) INSERT pares (dedupe) calculados desde CANDIDATE (ya tiene todo el contexto) - ins_pairs AS ( - INSERT INTO asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT id_usuario, fecha, desde, hasta, dur_min, p_origen - FROM ready - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT - (SELECT COUNT(*) FROM ins_raw), - (SELECT COUNT(*) FROM ins_pairs), - (SELECT COALESCE(missing,'[]'::jsonb) FROM miss) - INTO v_ins_raw, v_ins_pairs, v_miss; - - RETURN jsonb_build_object( - 'inserted_raw', v_ins_raw, - 'inserted_pairs', v_ins_pairs, - 'missing_docs', v_miss - ); -END; -$_$; - - --- --- Name: report_asistencia(date, date); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION report_asistencia(p_desde date, p_hasta date) RETURNS TABLE(documento text, nombre text, apellido text, fecha date, desde_hora text, hasta_hora text, dur_min numeric) - LANGUAGE sql - AS $$ - SELECT - u.documento, u.nombre, u.apellido, - ai.fecha, - to_char(ai.desde AT TIME ZONE 'America/Montevideo','HH24:MI:SS') AS desde_hora, - to_char(ai.hasta AT TIME ZONE 'America/Montevideo','HH24:MI:SS') AS hasta_hora, - ai.dur_min - FROM asistencia_intervalo ai - JOIN usuarios u USING (id_usuario) - WHERE ai.fecha BETWEEN p_desde AND p_hasta - ORDER BY u.documento, ai.fecha, ai.desde; -$$; - - --- --- Name: report_gastos(integer); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION report_gastos(p_year integer) RETURNS jsonb - LANGUAGE sql STABLE - AS $$ -WITH mdata AS ( - SELECT date_trunc('month', c.fec_compra)::date AS m, - SUM(c.total)::numeric AS importe - FROM compras c - WHERE EXTRACT(YEAR FROM c.fec_compra) = p_year - GROUP BY 1 -), -mm AS ( - SELECT EXTRACT(MONTH FROM m)::int AS mes, importe - FROM mdata -) -SELECT jsonb_build_object( - 'year', p_year, - 'total', COALESCE((SELECT SUM(importe) FROM mdata), 0), - 'avg', COALESCE((SELECT AVG(importe) FROM mdata), 0), - 'months', - (SELECT jsonb_agg( - jsonb_build_object( - 'mes', gs, - 'nombre', to_char(to_date(gs::text,'MM'),'Mon'), - 'importe', COALESCE(mm.importe,0) - ) - ORDER BY gs - ) - FROM generate_series(1,12) gs - LEFT JOIN mm ON mm.mes = gs) -); -$$; - - --- --- Name: report_tickets_year(integer, text); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION report_tickets_year(p_year integer, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE sql STABLE - AS $$ -WITH bounds AS ( - SELECT - make_timestamp(p_year, 1, 1, 0,0,0) AS d0, - make_timestamp(p_year+1, 1, 1, 0,0,0) AS d1, - make_timestamptz(p_year, 1, 1, 0,0,0, p_tz) AS t0, - make_timestamptz(p_year+1, 1, 1, 0,0,0, p_tz) AS t1 -), -base AS ( - SELECT - c.id_comanda, - CASE WHEN c.fec_cierre IS NOT NULL - THEN (c.fec_cierre AT TIME ZONE p_tz) - ELSE c.fec_creacion - END AS fec_local, - v.total - FROM comandas c - JOIN vw_ticket_total v ON v.id_comanda = c.id_comanda - JOIN bounds b ON TRUE - WHERE - (c.fec_cierre IS NOT NULL AND c.fec_cierre >= b.t0 AND c.fec_cierre < b.t1) - OR - (c.fec_cierre IS NULL AND c.fec_creacion >= b.d0 AND c.fec_creacion < b.d1) -), -m AS ( - SELECT - EXTRACT(MONTH FROM fec_local)::int AS mes, - COUNT(*)::int AS cant, - SUM(total)::numeric AS importe, - AVG(total)::numeric AS avg - FROM base - GROUP BY 1 -), -ytd AS ( - SELECT COUNT(*)::int AS total_ytd, - AVG(total)::numeric AS avg_ticket, - SUM(total)::numeric AS to_date - FROM base -) -SELECT jsonb_build_object( - 'year', p_year, - 'total_ytd', (SELECT total_ytd FROM ytd), - 'avg_ticket', (SELECT avg_ticket FROM ytd), - 'to_date', (SELECT to_date FROM ytd), - 'months', - (SELECT jsonb_agg( - jsonb_build_object( - 'mes', mes, - 'nombre', to_char(to_date(mes::text,'MM'),'Mon'), - 'cant', cant, - 'importe', importe, - 'avg', avg - ) - ORDER BY mes - ) - FROM m) -); -$$; - - --- --- Name: save_compra(integer, integer, timestamp with time zone, jsonb); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION save_compra(p_id_compra integer, p_id_proveedor integer, p_fec_compra timestamp with time zone, p_detalles jsonb) RETURNS TABLE(id_compra integer, total numeric) - LANGUAGE plpgsql - AS $$ -DECLARE - v_id INT; - v_total numeric := 0; -BEGIN - IF COALESCE(jsonb_array_length(p_detalles),0) = 0 THEN - RAISE EXCEPTION 'No hay renglones en la compra'; - END IF; - - -- Cabecera (insert/update) - IF p_id_compra IS NULL THEN - INSERT INTO compras (id_proveedor, fec_compra, total) - VALUES (p_id_proveedor, COALESCE(p_fec_compra, now()), 0) - RETURNING compras.id_compra INTO v_id; - ELSE - UPDATE compras c - SET id_proveedor = p_id_proveedor, - fec_compra = COALESCE(p_fec_compra, c.fec_compra) - WHERE c.id_compra = p_id_compra - RETURNING c.id_compra INTO v_id; - - -- Reemplazamos los renglones - DELETE FROM deta_comp_materias d WHERE d.id_compra = v_id; - DELETE FROM deta_comp_producto p WHERE p.id_compra = v_id; - END IF; - - -- Materias primas (sin CTE: parseo JSON inline) - INSERT INTO deta_comp_materias (id_compra, id_mat_prima, cantidad, pre_unitario) - SELECT - v_id, - x.id, - x.cantidad, - x.precio - FROM jsonb_to_recordset(COALESCE(p_detalles, '[]'::jsonb)) - AS x(tipo text, id int, cantidad numeric, precio numeric) - WHERE UPPER(TRIM(x.tipo)) = 'MAT'; - - -- Productos (sin CTE) - INSERT INTO deta_comp_producto (id_compra, id_producto, cantidad, pre_unitario) - SELECT - v_id, - x.id, - x.cantidad, - x.precio - FROM jsonb_to_recordset(COALESCE(p_detalles, '[]'::jsonb)) - AS x(tipo text, id int, cantidad numeric, precio numeric) - WHERE UPPER(TRIM(x.tipo)) = 'PROD'; - - -- Recalcular total (calificado) y redondear a ENTERO - SELECT - COALESCE( (SELECT SUM(dcm.cantidad*dcm.pre_unitario) - FROM deta_comp_materias dcm - WHERE dcm.id_compra = v_id), 0) - + COALESCE( (SELECT SUM(dcp.cantidad*dcp.pre_unitario) - FROM deta_comp_producto dcp - WHERE dcp.id_compra = v_id), 0) - INTO v_total; - - UPDATE compras c - SET total = round(v_total, 0) - WHERE c.id_compra = v_id; - - RETURN QUERY SELECT v_id, round(v_total, 0); -END; -$$; - - --- --- Name: save_materia_prima(integer, text, text, boolean, jsonb); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION save_materia_prima(p_id_mat_prima integer, p_nombre text, p_unidad text, p_activo boolean, p_proveedores jsonb DEFAULT '[]'::jsonb) RETURNS integer - LANGUAGE plpgsql - AS $_$ -DECLARE - v_id INT; -BEGIN - IF p_id_mat_prima IS NULL THEN - INSERT INTO mate_primas (nombre, unidad, activo) - VALUES (p_nombre, p_unidad, COALESCE(p_activo, TRUE)) - RETURNING mate_primas.id_mat_prima INTO v_id; - ELSE - UPDATE mate_primas mp - SET nombre = p_nombre, - unidad = p_unidad, - activo = COALESCE(p_activo, TRUE) - WHERE mp.id_mat_prima = p_id_mat_prima; - v_id := p_id_mat_prima; - END IF; - - -- Sincronizar proveedores: borrar todos y re-crear a partir de JSONB - DELETE FROM prov_mate_prima pmp WHERE pmp.id_mat_prima = v_id; - - INSERT INTO prov_mate_prima (id_proveedor, id_mat_prima) - SELECT (e->>0)::INT AS id_proveedor, -- elementos JSON como enteros (array simple) - v_id AS id_mat_prima - FROM jsonb_array_elements(COALESCE(p_proveedores, '[]'::jsonb)) AS e - WHERE (e->>0) ~ '^\d+$'; -- solo enteros - - RETURN v_id; -END; -$_$; - - --- --- Name: save_producto(integer, text, text, numeric, boolean, integer, jsonb); Type: FUNCTION; Schema: ; Owner: - --- - -CREATE FUNCTION save_producto(p_id_producto integer, p_nombre text, p_img_producto text, p_precio numeric, p_activo boolean, p_id_categoria integer, p_receta jsonb DEFAULT '[]'::jsonb) RETURNS integer - LANGUAGE plpgsql - AS $_$ -DECLARE - v_id INT; -BEGIN - IF p_id_producto IS NULL THEN - INSERT INTO productos (nombre, img_producto, precio, activo, id_categoria) - VALUES (p_nombre, p_img_producto, p_precio, COALESCE(p_activo, TRUE), p_id_categoria) - RETURNING productos.id_producto INTO v_id; - ELSE - UPDATE productos p - SET nombre = p_nombre, - img_producto = p_img_producto, - precio = p_precio, - activo = COALESCE(p_activo, TRUE), - id_categoria = p_id_categoria - WHERE p.id_producto = p_id_producto; - v_id := p_id_producto; - END IF; - - -- Limpia receta actual - DELETE FROM receta_producto rp WHERE rp.id_producto = v_id; - - -- Inserta SOLO ítems válidos (id entero positivo y cantidad > 0), redondeo a 3 decimales - INSERT INTO receta_producto (id_producto, id_mat_prima, qty_por_unidad) - SELECT - v_id, - (rec->>'id_mat_prima')::INT, - ROUND((rec->>'qty_por_unidad')::NUMERIC, 3) - FROM jsonb_array_elements(COALESCE(p_receta, '[]'::jsonb)) AS rec - WHERE - (rec->>'id_mat_prima') ~ '^\d+$' - AND (rec->>'id_mat_prima')::INT > 0 - AND (rec->>'qty_por_unidad') ~ '^\d+(\.\d+)?$' - AND (rec->>'qty_por_unidad')::NUMERIC > 0; - - RETURN v_id; -END; -$_$; - - --- --- Name: asistencia_intervalo; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE asistencia_intervalo ( - id_intervalo bigint NOT NULL, - id_usuario integer NOT NULL, - fecha date NOT NULL, - desde timestamp with time zone NOT NULL, - hasta timestamp with time zone NOT NULL, - dur_min numeric(10,2) NOT NULL, - origen text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT chk_ai_orden CHECK ((hasta > desde)) -); - - --- --- Name: asistencia_intervalo_id_intervalo_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE asistencia_intervalo_id_intervalo_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: asistencia_intervalo_id_intervalo_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE asistencia_intervalo_id_intervalo_seq OWNED BY asistencia_intervalo.id_intervalo; - - --- --- Name: asistencia_raw; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE asistencia_raw ( - id_raw bigint NOT NULL, - id_usuario integer NOT NULL, - ts timestamp with time zone NOT NULL, - modo text, - origen text, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - - --- --- Name: asistencia_raw_id_raw_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE asistencia_raw_id_raw_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: asistencia_raw_id_raw_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE asistencia_raw_id_raw_seq OWNED BY asistencia_raw.id_raw; - - --- --- Name: asistencia_resumen_diario; Type: VIEW; Schema: ; Owner: - --- - -CREATE VIEW asistencia_resumen_diario AS - SELECT ai.id_usuario, - u.documento, - u.nombre, - u.apellido, - ai.fecha, - sum(ai.dur_min) AS minutos_dia, - round((sum(ai.dur_min) / 60.0), 2) AS horas_dia, - count(*) AS pares_dia - FROM (asistencia_intervalo ai - JOIN usuarios u USING (id_usuario)) - GROUP BY ai.id_usuario, u.documento, u.nombre, u.apellido, ai.fecha - ORDER BY ai.id_usuario, ai.fecha; - - --- --- Name: categorias; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE categorias ( - id_categoria integer NOT NULL, - nombre text NOT NULL, - visible boolean DEFAULT true -); - - --- --- Name: categorias_id_categoria_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE categorias_id_categoria_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: categorias_id_categoria_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE categorias_id_categoria_seq OWNED BY categorias.id_categoria; - - --- --- Name: clientes; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE clientes ( - id_cliente integer NOT NULL, - nombre text NOT NULL, - correo text, - telefono text, - fec_nacimiento date, - activo boolean DEFAULT true -); - - --- --- Name: clientes_id_cliente_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE clientes_id_cliente_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: clientes_id_cliente_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE clientes_id_cliente_seq OWNED BY clientes.id_cliente; - - --- --- Name: comandas_id_comanda_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE comandas_id_comanda_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: comandas_id_comanda_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE comandas_id_comanda_seq OWNED BY comandas.id_comanda; - - --- --- Name: compras; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE compras ( - id_compra integer NOT NULL, - id_proveedor integer NOT NULL, - fec_compra timestamp without time zone NOT NULL, - total numeric(14,2) -); - - --- --- Name: compras_id_compra_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE compras_id_compra_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: compras_id_compra_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE compras_id_compra_seq OWNED BY compras.id_compra; - - --- --- Name: deta_comandas_id_det_comanda_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE deta_comandas_id_det_comanda_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: deta_comandas_id_det_comanda_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE deta_comandas_id_det_comanda_seq OWNED BY deta_comandas.id_det_comanda; - - --- --- Name: deta_comp_materias; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE deta_comp_materias ( - id_compra integer NOT NULL, - id_mat_prima integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - CONSTRAINT deta_comp_materias_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comp_materias_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - - --- --- Name: deta_comp_producto; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE deta_comp_producto ( - id_compra integer NOT NULL, - id_producto integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - CONSTRAINT deta_comp_producto_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comp_producto_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - - --- --- Name: mate_primas; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE mate_primas ( - id_mat_prima integer NOT NULL, - nombre text NOT NULL, - unidad text NOT NULL, - activo boolean DEFAULT true -); - - --- --- Name: mate_primas_id_mat_prima_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE mate_primas_id_mat_prima_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: mate_primas_id_mat_prima_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE mate_primas_id_mat_prima_seq OWNED BY mate_primas.id_mat_prima; - - --- --- Name: mesas_id_mesa_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE mesas_id_mesa_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: mesas_id_mesa_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE mesas_id_mesa_seq OWNED BY mesas.id_mesa; - - --- --- Name: productos; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE productos ( - id_producto integer NOT NULL, - nombre text NOT NULL, - img_producto character varying(255) DEFAULT 'img/productos/img_producto.png'::character varying NOT NULL, - precio integer NOT NULL, - activo boolean DEFAULT true, - id_categoria integer NOT NULL, - CONSTRAINT productos_precio_check CHECK (((precio)::numeric >= (0)::numeric)), - CONSTRAINT productos_precio_nn CHECK ((precio >= 0)) -); - - --- --- Name: productos_id_producto_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE productos_id_producto_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: productos_id_producto_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE productos_id_producto_seq OWNED BY productos.id_producto; - - --- --- Name: prov_mate_prima; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE prov_mate_prima ( - id_proveedor integer NOT NULL, - id_mat_prima integer NOT NULL -); - - --- --- Name: prov_producto; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE prov_producto ( - id_proveedor integer NOT NULL, - id_producto integer NOT NULL -); - - --- --- Name: proveedores; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE proveedores ( - id_proveedor integer NOT NULL, - rut text, - raz_social text NOT NULL, - direccion text, - contacto text -); - - --- --- Name: proveedores_id_proveedor_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE proveedores_id_proveedor_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: proveedores_id_proveedor_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE proveedores_id_proveedor_seq OWNED BY proveedores.id_proveedor; - - --- --- Name: receta_producto; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE receta_producto ( - id_producto integer NOT NULL, - id_mat_prima integer NOT NULL, - qty_por_unidad numeric(12,3) NOT NULL, - CONSTRAINT receta_producto_qty_por_unidad_check CHECK ((qty_por_unidad > (0)::numeric)) -); - - --- --- Name: roles; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE roles ( - id_rol integer NOT NULL, - nombre text NOT NULL -); - - --- --- Name: roles_id_rol_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE roles_id_rol_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: roles_id_rol_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE roles_id_rol_seq OWNED BY roles.id_rol; - - --- --- Name: usua_roles; Type: TABLE; Schema: ; Owner: - --- - -CREATE TABLE usua_roles ( - id_usuario integer NOT NULL, - id_rol integer NOT NULL, - fec_asignacion timestamp without time zone DEFAULT now(), - autor integer, - activo boolean DEFAULT true -); - - --- --- Name: usuarios_id_usuario_seq; Type: SEQUENCE; Schema: ; Owner: - --- - -CREATE SEQUENCE usuarios_id_usuario_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: usuarios_id_usuario_seq; Type: SEQUENCE OWNED BY; Schema: ; Owner: - --- - -ALTER SEQUENCE usuarios_id_usuario_seq OWNED BY usuarios.id_usuario; - - --- --- Name: v_comandas_detalle_base; Type: VIEW; Schema: ; Owner: - --- - -CREATE VIEW v_comandas_detalle_base AS - SELECT c.id_comanda, - c.fec_creacion, - c.fec_cierre, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM ((((comandas c - JOIN usuarios u ON ((u.id_usuario = c.id_usuario))) - JOIN mesas m ON ((m.id_mesa = c.id_mesa))) - LEFT JOIN deta_comandas d ON ((d.id_comanda = c.id_comanda))) - LEFT JOIN productos p ON ((p.id_producto = d.id_producto))); - - --- --- Name: v_comandas_detalle_items; Type: VIEW; Schema: ; Owner: - --- - -CREATE VIEW v_comandas_detalle_items AS - SELECT d.id_comanda, - d.id_det_comanda, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal, - d.observaciones - FROM (deta_comandas d - JOIN productos p ON ((p.id_producto = d.id_producto))); - - --- --- Name: v_comandas_detalle_json; Type: VIEW; Schema: ; Owner: - --- - -CREATE VIEW v_comandas_detalle_json AS - SELECT id_comanda, - jsonb_build_object('id_comanda', id_comanda, 'fec_creacion', fec_creacion, 'fec_cierre', fec_cierre, 'estado', estado, 'observaciones', observaciones, 'usuario', jsonb_build_object('id_usuario', id_usuario, 'nombre', usuario_nombre, 'apellido', usuario_apellido), 'mesa', jsonb_build_object('id_mesa', id_mesa, 'numero', mesa_numero, 'apodo', mesa_apodo), 'items', COALESCE(( SELECT jsonb_agg(jsonb_build_object('producto_id', b.id_producto, 'producto', b.producto_nombre, 'cantidad', b.cantidad, 'pre_unitario', b.pre_unitario, 'subtotal', b.subtotal) ORDER BY b.producto_nombre) AS jsonb_agg - FROM v_comandas_detalle_base b - WHERE ((b.id_comanda = h.id_comanda) AND (b.id_producto IS NOT NULL))), '[]'::jsonb), 'totales', jsonb_build_object('items', COALESCE(( SELECT count(*) AS count - FROM v_comandas_detalle_base b - WHERE ((b.id_comanda = h.id_comanda) AND (b.id_producto IS NOT NULL))), (0)::bigint), 'total', COALESCE(( SELECT sum(b.subtotal) AS sum - FROM v_comandas_detalle_base b - WHERE (b.id_comanda = h.id_comanda)), (0)::numeric))) AS data - FROM ( SELECT DISTINCT v_comandas_detalle_base.id_comanda, - v_comandas_detalle_base.fec_creacion, - v_comandas_detalle_base.fec_cierre, - v_comandas_detalle_base.estado, - v_comandas_detalle_base.observaciones, - v_comandas_detalle_base.id_usuario, - v_comandas_detalle_base.usuario_nombre, - v_comandas_detalle_base.usuario_apellido, - v_comandas_detalle_base.id_mesa, - v_comandas_detalle_base.mesa_numero, - v_comandas_detalle_base.mesa_apodo - FROM v_comandas_detalle_base) h; - - --- --- Name: vw_compras; Type: VIEW; Schema: ; Owner: - --- - -CREATE VIEW vw_compras AS - SELECT c.id_compra, - c.id_proveedor, - p.raz_social AS proveedor, - c.fec_compra, - c.total - FROM (compras c - JOIN proveedores p USING (id_proveedor)) - ORDER BY c.fec_compra DESC, c.id_compra DESC; - - --- --- Name: vw_ticket_total; Type: VIEW; Schema: ; Owner: - --- - -CREATE VIEW vw_ticket_total AS - WITH lineas AS ( - SELECT c.id_comanda, - COALESCE(c.fec_cierre, (c.fec_creacion)::timestamp with time zone) AS fec_ticket, - (COALESCE(dc.pre_unitario, (p.precio)::numeric, (0)::numeric))::numeric(14,2) AS pu, - (COALESCE(dc.cantidad, (1)::numeric))::numeric(14,3) AS qty - FROM ((comandas c - JOIN deta_comandas dc ON ((dc.id_comanda = c.id_comanda))) - LEFT JOIN productos p ON ((p.id_producto = dc.id_producto))) - ) - SELECT id_comanda, - fec_ticket, - (sum((qty * pu)))::numeric(14,2) AS total - FROM lineas - GROUP BY id_comanda, fec_ticket; - - --- --- Name: asistencia_intervalo id_intervalo; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY asistencia_intervalo ALTER COLUMN id_intervalo SET DEFAULT nextval('asistencia_intervalo_id_intervalo_seq'::regclass); - - --- --- Name: asistencia_raw id_raw; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY asistencia_raw ALTER COLUMN id_raw SET DEFAULT nextval('asistencia_raw_id_raw_seq'::regclass); - - --- --- Name: categorias id_categoria; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY categorias ALTER COLUMN id_categoria SET DEFAULT nextval('categorias_id_categoria_seq'::regclass); - - --- --- Name: clientes id_cliente; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY clientes ALTER COLUMN id_cliente SET DEFAULT nextval('clientes_id_cliente_seq'::regclass); - - --- --- Name: comandas id_comanda; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY comandas ALTER COLUMN id_comanda SET DEFAULT nextval('comandas_id_comanda_seq'::regclass); - - --- --- Name: compras id_compra; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY compras ALTER COLUMN id_compra SET DEFAULT nextval('compras_id_compra_seq'::regclass); - - --- --- Name: deta_comandas id_det_comanda; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY deta_comandas ALTER COLUMN id_det_comanda SET DEFAULT nextval('deta_comandas_id_det_comanda_seq'::regclass); - - --- --- Name: mate_primas id_mat_prima; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY mate_primas ALTER COLUMN id_mat_prima SET DEFAULT nextval('mate_primas_id_mat_prima_seq'::regclass); - - --- --- Name: mesas id_mesa; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY mesas ALTER COLUMN id_mesa SET DEFAULT nextval('mesas_id_mesa_seq'::regclass); - - --- --- Name: productos id_producto; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY productos ALTER COLUMN id_producto SET DEFAULT nextval('productos_id_producto_seq'::regclass); - - --- --- Name: proveedores id_proveedor; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY proveedores ALTER COLUMN id_proveedor SET DEFAULT nextval('proveedores_id_proveedor_seq'::regclass); - - --- --- Name: roles id_rol; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY roles ALTER COLUMN id_rol SET DEFAULT nextval('roles_id_rol_seq'::regclass); - - --- --- Name: usuarios id_usuario; Type: DEFAULT; Schema: ; Owner: - --- - -ALTER TABLE ONLY usuarios ALTER COLUMN id_usuario SET DEFAULT nextval('usuarios_id_usuario_seq'::regclass); - - --- --- Name: asistencia_intervalo asistencia_intervalo_id_usuario_desde_hasta_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_id_usuario_desde_hasta_key UNIQUE (id_usuario, desde, hasta); - - --- --- Name: asistencia_intervalo asistencia_intervalo_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_pkey PRIMARY KEY (id_intervalo); - - --- --- Name: asistencia_raw asistencia_raw_id_usuario_ts_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY asistencia_raw - ADD CONSTRAINT asistencia_raw_id_usuario_ts_key UNIQUE (id_usuario, ts); - - --- --- Name: asistencia_raw asistencia_raw_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY asistencia_raw - ADD CONSTRAINT asistencia_raw_pkey PRIMARY KEY (id_raw); - - --- --- Name: categorias categorias_nombre_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY categorias - ADD CONSTRAINT categorias_nombre_key UNIQUE (nombre); - - --- --- Name: categorias categorias_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY categorias - ADD CONSTRAINT categorias_pkey PRIMARY KEY (id_categoria); - - --- --- Name: clientes clientes_correo_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY clientes - ADD CONSTRAINT clientes_correo_key UNIQUE (correo); - - --- --- Name: clientes clientes_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY clientes - ADD CONSTRAINT clientes_pkey PRIMARY KEY (id_cliente); - - --- --- Name: clientes clientes_telefono_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY clientes - ADD CONSTRAINT clientes_telefono_key UNIQUE (telefono); - - --- --- Name: comandas comandas_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY comandas - ADD CONSTRAINT comandas_pkey PRIMARY KEY (id_comanda); - - --- --- Name: compras compras_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY compras - ADD CONSTRAINT compras_pkey PRIMARY KEY (id_compra); - - --- --- Name: deta_comandas deta_comandas_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY deta_comandas - ADD CONSTRAINT deta_comandas_pkey PRIMARY KEY (id_det_comanda); - - --- --- Name: deta_comp_materias deta_comp_materias_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY deta_comp_materias - ADD CONSTRAINT deta_comp_materias_pkey PRIMARY KEY (id_compra, id_mat_prima); - - --- --- Name: deta_comp_producto deta_comp_producto_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY deta_comp_producto - ADD CONSTRAINT deta_comp_producto_pkey PRIMARY KEY (id_compra, id_producto); - - --- --- Name: mate_primas mate_primas_nombre_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY mate_primas - ADD CONSTRAINT mate_primas_nombre_key UNIQUE (nombre); - - --- --- Name: mate_primas mate_primas_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY mate_primas - ADD CONSTRAINT mate_primas_pkey PRIMARY KEY (id_mat_prima); - - --- --- Name: mesas mesas_apodo_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY mesas - ADD CONSTRAINT mesas_apodo_key UNIQUE (apodo); - - --- --- Name: mesas mesas_numero_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY mesas - ADD CONSTRAINT mesas_numero_key UNIQUE (numero); - - --- --- Name: mesas mesas_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY mesas - ADD CONSTRAINT mesas_pkey PRIMARY KEY (id_mesa); - - --- --- Name: productos productos_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY productos - ADD CONSTRAINT productos_pkey PRIMARY KEY (id_producto); - - --- --- Name: prov_mate_prima prov_mate_prima_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY prov_mate_prima - ADD CONSTRAINT prov_mate_prima_pkey PRIMARY KEY (id_proveedor, id_mat_prima); - - --- --- Name: prov_producto prov_producto_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY prov_producto - ADD CONSTRAINT prov_producto_pkey PRIMARY KEY (id_proveedor, id_producto); - - --- --- Name: proveedores proveedores_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY proveedores - ADD CONSTRAINT proveedores_pkey PRIMARY KEY (id_proveedor); - - --- --- Name: proveedores proveedores_rut_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY proveedores - ADD CONSTRAINT proveedores_rut_key UNIQUE (rut); - - --- --- Name: receta_producto receta_producto_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY receta_producto - ADD CONSTRAINT receta_producto_pkey PRIMARY KEY (id_producto, id_mat_prima); - - --- --- Name: roles roles_nombre_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY roles - ADD CONSTRAINT roles_nombre_key UNIQUE (nombre); - - --- --- Name: roles roles_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY roles - ADD CONSTRAINT roles_pkey PRIMARY KEY (id_rol); - - --- --- Name: usua_roles usua_roles_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY usua_roles - ADD CONSTRAINT usua_roles_pkey PRIMARY KEY (id_usuario, id_rol); - - --- --- Name: usuarios usuarios_documento_key; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY usuarios - ADD CONSTRAINT usuarios_documento_key UNIQUE (documento); - - --- --- Name: usuarios usuarios_pkey; Type: CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY usuarios - ADD CONSTRAINT usuarios_pkey PRIMARY KEY (id_usuario); - - --- --- Name: compras_fec_compra_idx; Type: INDEX; Schema: ; Owner: - --- - -CREATE INDEX compras_fec_compra_idx ON compras USING btree (fec_compra); - - --- --- Name: idx_asist_int_usuario_fecha; Type: INDEX; Schema: ; Owner: - --- - -CREATE INDEX idx_asist_int_usuario_fecha ON asistencia_intervalo USING btree (id_usuario, fecha); - - --- --- Name: idx_asist_raw_usuario_ts; Type: INDEX; Schema: ; Owner: - --- - -CREATE INDEX idx_asist_raw_usuario_ts ON asistencia_raw USING btree (id_usuario, ts); - - --- --- Name: idx_detalle_comanda_comanda; Type: INDEX; Schema: ; Owner: - --- - -CREATE INDEX idx_detalle_comanda_comanda ON deta_comandas USING btree (id_comanda); - - --- --- Name: idx_detalle_comanda_producto; Type: INDEX; Schema: ; Owner: - --- - -CREATE INDEX idx_detalle_comanda_producto ON deta_comandas USING btree (id_producto); - - --- --- Name: ix_comandas_fec_cierre; Type: INDEX; Schema: ; Owner: - --- - -CREATE INDEX ix_comandas_fec_cierre ON comandas USING btree (fec_cierre); - - --- --- Name: ix_comandas_id; Type: INDEX; Schema: ; Owner: - --- - -CREATE INDEX ix_comandas_id ON comandas USING btree (id_comanda); - - --- --- Name: ix_deta_comandas_id_comanda; Type: INDEX; Schema: ; Owner: - --- - -CREATE INDEX ix_deta_comandas_id_comanda ON deta_comandas USING btree (id_comanda); - - --- --- Name: ix_deta_comandas_id_producto; Type: INDEX; Schema: ; Owner: - --- - -CREATE INDEX ix_deta_comandas_id_producto ON deta_comandas USING btree (id_producto); - - --- --- Name: asistencia_intervalo asistencia_intervalo_id_usuario_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES usuarios(id_usuario) ON DELETE CASCADE; - - --- --- Name: asistencia_raw asistencia_raw_id_usuario_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY asistencia_raw - ADD CONSTRAINT asistencia_raw_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES usuarios(id_usuario) ON DELETE CASCADE; - - --- --- Name: comandas comandas_id_mesa_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY comandas - ADD CONSTRAINT comandas_id_mesa_fkey FOREIGN KEY (id_mesa) REFERENCES mesas(id_mesa); - - --- --- Name: comandas comandas_id_usuario_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY comandas - ADD CONSTRAINT comandas_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES usuarios(id_usuario); - - --- --- Name: compras compras_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY compras - ADD CONSTRAINT compras_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES proveedores(id_proveedor); - - --- --- Name: deta_comandas deta_comandas_id_comanda_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY deta_comandas - ADD CONSTRAINT deta_comandas_id_comanda_fkey FOREIGN KEY (id_comanda) REFERENCES comandas(id_comanda) ON DELETE CASCADE; - - --- --- Name: deta_comandas deta_comandas_id_producto_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY deta_comandas - ADD CONSTRAINT deta_comandas_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES productos(id_producto); - - --- --- Name: deta_comp_materias deta_comp_materias_id_compra_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY deta_comp_materias - ADD CONSTRAINT deta_comp_materias_id_compra_fkey FOREIGN KEY (id_compra) REFERENCES compras(id_compra) ON DELETE CASCADE; - - --- --- Name: deta_comp_materias deta_comp_materias_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY deta_comp_materias - ADD CONSTRAINT deta_comp_materias_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES mate_primas(id_mat_prima); - - --- --- Name: deta_comp_producto deta_comp_producto_id_compra_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY deta_comp_producto - ADD CONSTRAINT deta_comp_producto_id_compra_fkey FOREIGN KEY (id_compra) REFERENCES compras(id_compra) ON DELETE CASCADE; - - --- --- Name: deta_comp_producto deta_comp_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY deta_comp_producto - ADD CONSTRAINT deta_comp_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES productos(id_producto); - - --- --- Name: productos productos_id_categoria_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY productos - ADD CONSTRAINT productos_id_categoria_fkey FOREIGN KEY (id_categoria) REFERENCES categorias(id_categoria); - - --- --- Name: prov_mate_prima prov_mate_prima_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY prov_mate_prima - ADD CONSTRAINT prov_mate_prima_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES mate_primas(id_mat_prima); - - --- --- Name: prov_mate_prima prov_mate_prima_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY prov_mate_prima - ADD CONSTRAINT prov_mate_prima_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES proveedores(id_proveedor) ON DELETE CASCADE; - - --- --- Name: prov_producto prov_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY prov_producto - ADD CONSTRAINT prov_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES productos(id_producto); - - --- --- Name: prov_producto prov_producto_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY prov_producto - ADD CONSTRAINT prov_producto_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES proveedores(id_proveedor) ON DELETE CASCADE; - - --- --- Name: receta_producto receta_producto_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY receta_producto - ADD CONSTRAINT receta_producto_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES mate_primas(id_mat_prima); - - --- --- Name: receta_producto receta_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY receta_producto - ADD CONSTRAINT receta_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES productos(id_producto) ON DELETE CASCADE; - - --- --- Name: usua_roles usua_roles_autor_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY usua_roles - ADD CONSTRAINT usua_roles_autor_fkey FOREIGN KEY (autor) REFERENCES usuarios(id_usuario); - - --- --- Name: usua_roles usua_roles_id_rol_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY usua_roles - ADD CONSTRAINT usua_roles_id_rol_fkey FOREIGN KEY (id_rol) REFERENCES roles(id_rol); - - --- --- Name: usua_roles usua_roles_id_usuario_fkey; Type: FK CONSTRAINT; Schema: ; Owner: - --- - -ALTER TABLE ONLY usua_roles - ADD CONSTRAINT usua_roles_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES usuarios(id_usuario) ON DELETE CASCADE; - - --- --- PostgreSQL database dump complete --- - - -COMMIT; diff --git a/services/auth/src/db/initTenant_v3.sql b/services/auth/src/db/initTenant_v3.sql deleted file mode 100644 index 36c2575..0000000 --- a/services/auth/src/db/initTenant_v3.sql +++ /dev/null @@ -1,2284 +0,0 @@ --- --- PostgreSQL database dump --- - - --- Dumped from database version 16.10 (Debian 16.10-1.pgdg13+1) --- Dumped by pg_dump version 16.10 (Debian 16.10-1.pgdg13+1) - -BEGIN; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SET row_security = off; -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - --- --- Name: public; Type: SCHEMA; Schema: -; Owner: - --- - -CREATE SCHEMA public; - - --- --- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: - --- - -COMMENT ON SCHEMA public IS 'standard public schema'; - - --- --- Name: asistencia_delete_raw(bigint, text); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.asistencia_delete_raw(p_id_raw bigint, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE - v_id_usuario INT; - v_ts TIMESTAMPTZ; - v_t0 TIMESTAMPTZ; - v_t1 TIMESTAMPTZ; - v_del_raw INT; - v_del INT; - v_ins INT; -BEGIN - SELECT id_usuario, ts INTO v_id_usuario, v_ts - FROM public.asistencia_raw WHERE id_raw = p_id_raw; - IF v_id_usuario IS NULL THEN - RETURN jsonb_build_object('deleted',0,'msg','id_raw inexistente'); - END IF; - - v_t0 := v_ts - INTERVAL '1 day'; - v_t1 := v_ts + INTERVAL '1 day'; - - -- borrar raw - DELETE FROM public.asistencia_raw WHERE id_raw = p_id_raw; - GET DIAGNOSTICS v_del_raw = ROW_COUNT; - - -- recomputar pares en ventana - WITH tl AS ( - SELECT ar.ts, - ROW_NUMBER() OVER (ORDER BY ar.ts) AS rn - FROM public.asistencia_raw ar - WHERE ar.id_usuario = v_id_usuario - AND ar.ts BETWEEN v_t0 AND v_t1 - ), - ready AS ( - SELECT (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM tl t1 - JOIN tl t2 ON t2.rn = t1.rn + 1 - WHERE (t1.rn % 2) = 1 AND t2.ts > t1.ts - ), - del AS ( - DELETE FROM public.asistencia_intervalo ai - WHERE ai.id_usuario = v_id_usuario - AND (ai.desde BETWEEN v_t0 AND v_t1 OR ai.hasta BETWEEN v_t0 AND v_t1) - RETURNING 1 - ), - ins AS ( - INSERT INTO public.asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT v_id_usuario, r.fecha, r.desde, r.hasta, r.dur_min, 'delete_adjust' - FROM ready r - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT (SELECT COUNT(*) FROM del), (SELECT COUNT(*) FROM ins) INTO v_del, v_ins; - - RETURN jsonb_build_object('deleted',v_del_raw,'deleted_pairs',v_del,'inserted_pairs',v_ins); -END; -$$; - - --- --- Name: asistencia_get(text, date, date, text); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.asistencia_get(p_doc text, p_desde date, p_hasta date, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH u AS ( - SELECT id_usuario, documento, nombre, apellido - FROM public.usuarios - WHERE regexp_replace(documento,'^\s*0+','','g') = regexp_replace(p_doc,'^\s*0+','','g') - LIMIT 1 -), -r AS ( - SELECT ar.id_raw, - (ar.ts AT TIME ZONE p_tz)::date AS fecha, - to_char(ar.ts AT TIME ZONE p_tz,'HH24:MI:SS') AS hora, - COALESCE(ar.modo,'') AS modo, - COALESCE(ar.origen,'') AS origen, - ar.ts - FROM public.asistencia_raw ar - JOIN u USING (id_usuario) - WHERE (ar.ts AT TIME ZONE p_tz)::date BETWEEN p_desde AND p_hasta -), -i AS ( - SELECT ai.id_intervalo, - ai.fecha, - to_char(ai.desde AT TIME ZONE p_tz,'HH24:MI:SS') AS desde_hora, - to_char(ai.hasta AT TIME ZONE p_tz,'HH24:MI:SS') AS hasta_hora, - ai.dur_min - FROM public.asistencia_intervalo ai - JOIN u USING (id_usuario) - WHERE ai.fecha BETWEEN p_desde AND p_hasta -) -SELECT jsonb_build_object( - 'usuario', (SELECT to_jsonb(u.*) FROM u), - 'raw', COALESCE((SELECT jsonb_agg(to_jsonb(r.*) ORDER BY r.ts) FROM r),'[]'::jsonb), - 'intervalos', COALESCE((SELECT jsonb_agg(to_jsonb(i.*) ORDER BY i.fecha, i.id_intervalo) FROM i),'[]'::jsonb) -); -$$; - - --- --- Name: asistencia_update_raw(bigint, date, text, text, text); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.asistencia_update_raw(p_id_raw bigint, p_fecha date, p_hora text, p_modo text, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE - v_id_usuario INT; - v_ts_old TIMESTAMPTZ; - v_ts_new TIMESTAMPTZ; - v_t0 TIMESTAMPTZ; - v_t1 TIMESTAMPTZ; - v_del INT; - v_ins INT; -BEGIN - -- leer estado previo - SELECT id_usuario, ts INTO v_id_usuario, v_ts_old - FROM public.asistencia_raw WHERE id_raw = p_id_raw; - IF v_id_usuario IS NULL THEN - RETURN jsonb_build_object('updated',0,'msg','id_raw inexistente'); - END IF; - - -- construir ts nuevo - v_ts_new := make_timestamptz( - EXTRACT(YEAR FROM p_fecha)::INT, - EXTRACT(MONTH FROM p_fecha)::INT, - EXTRACT(DAY FROM p_fecha)::INT, - split_part(p_hora,':',1)::INT, - split_part(p_hora,':',2)::INT, - COALESCE(NULLIF(split_part(p_hora,':',3),''), '0')::INT, - p_tz); - - -- aplicar update - UPDATE public.asistencia_raw - SET ts = v_ts_new, - modo = COALESCE(p_modo, modo) - WHERE id_raw = p_id_raw; - - -- ventana de recálculo - v_t0 := LEAST(v_ts_old, v_ts_new) - INTERVAL '1 day'; - v_t1 := GREATEST(v_ts_old, v_ts_new) + INTERVAL '1 day'; - - -- recomputar pares en la ventana: borrar los del rango y reinsertar - WITH tl AS ( - SELECT ar.ts, - ROW_NUMBER() OVER (ORDER BY ar.ts) AS rn - FROM public.asistencia_raw ar - WHERE ar.id_usuario = v_id_usuario - AND ar.ts BETWEEN v_t0 AND v_t1 - ), - ready AS ( - SELECT (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM tl t1 - JOIN tl t2 ON t2.rn = t1.rn + 1 - WHERE (t1.rn % 2) = 1 AND t2.ts > t1.ts - ), - del AS ( - DELETE FROM public.asistencia_intervalo ai - WHERE ai.id_usuario = v_id_usuario - AND (ai.desde BETWEEN v_t0 AND v_t1 OR ai.hasta BETWEEN v_t0 AND v_t1) - RETURNING 1 - ), - ins AS ( - INSERT INTO public.asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT v_id_usuario, r.fecha, r.desde, r.hasta, r.dur_min, 'edit_manual' - FROM ready r - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT (SELECT COUNT(*) FROM del), (SELECT COUNT(*) FROM ins) INTO v_del, v_ins; - - RETURN jsonb_build_object('updated',1,'deleted_pairs',v_del,'inserted_pairs',v_ins); -END; -$$; - - --- --- Name: delete_compra(integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.delete_compra(p_id_compra integer) RETURNS void - LANGUAGE plpgsql - AS $$ -BEGIN - DELETE FROM public.deta_comp_materias WHERE id_compra = p_id_compra; - DELETE FROM public.deta_comp_producto WHERE id_compra = p_id_compra; - DELETE FROM public.compras WHERE id_compra = p_id_compra; -END; -$$; - - --- --- Name: f_abrir_comanda(integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.f_abrir_comanda(p_id integer) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE r jsonb; -BEGIN - UPDATE public.comandas - SET estado = 'abierta', - fec_cierre = NULL - WHERE id_comanda = p_id; - - IF NOT FOUND THEN - RETURN NULL; - END IF; - - SELECT to_jsonb(v) INTO r - FROM public.v_comandas_resumen v - WHERE v.id_comanda = p_id; - - RETURN r; -END; -$$; - - --- --- Name: f_cerrar_comanda(integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.f_cerrar_comanda(p_id integer) RETURNS jsonb - LANGUAGE plpgsql - AS $$ -DECLARE r jsonb; -BEGIN - UPDATE public.comandas - SET estado = 'cerrada', - fec_cierre = COALESCE(fec_cierre, NOW()) - WHERE id_comanda = p_id; - - IF NOT FOUND THEN - RETURN NULL; - END IF; - - SELECT to_jsonb(v) INTO r - FROM public.v_comandas_resumen v - WHERE v.id_comanda = p_id; - - RETURN r; -END; -$$; - - --- --- Name: f_comanda_detalle_json(integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.f_comanda_detalle_json(p_id_comanda integer) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH base AS ( - SELECT - c.id_comanda, - c.fec_creacion, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM public.comandas c - JOIN public.usuarios u ON u.id_usuario = c.id_usuario - JOIN public.mesas m ON m.id_mesa = c.id_mesa - LEFT JOIN public.deta_comandas d ON d.id_comanda = c.id_comanda - LEFT JOIN public.productos p ON p.id_producto = d.id_producto - WHERE c.id_comanda = p_id_comanda -), -hdr AS ( - -- 1 sola fila con los datos de cabecera - SELECT DISTINCT - id_comanda, fec_creacion, estado, observaciones, - id_usuario, usuario_nombre, usuario_apellido, - id_mesa, mesa_numero, mesa_apodo - FROM base -), -agg_items AS ( - SELECT - COALESCE( - jsonb_agg( - jsonb_build_object( - 'producto_id', b.id_producto, - 'producto', b.producto_nombre, - 'cantidad', b.cantidad, - 'pre_unitario', b.pre_unitario, - 'subtotal', b.subtotal - ) - ORDER BY b.producto_nombre NULLS LAST - ) FILTER (WHERE b.id_producto IS NOT NULL), - '[]'::jsonb - ) AS items - FROM base b -), -tot AS ( - SELECT - COUNT(*) FILTER (WHERE id_producto IS NOT NULL) AS items, - COALESCE(SUM(subtotal), 0)::numeric AS total - FROM base -) -SELECT - CASE - WHEN EXISTS (SELECT 1 FROM hdr) THEN - jsonb_build_object( - 'id_comanda', h.id_comanda, - 'fec_creacion', h.fec_creacion, - 'estado', h.estado, - 'observaciones',h.observaciones, - 'usuario', jsonb_build_object( - 'id_usuario', h.id_usuario, - 'nombre', h.usuario_nombre, - 'apellido', h.usuario_apellido - ), - 'mesa', jsonb_build_object( - 'id_mesa', h.id_mesa, - 'numero', h.mesa_numero, - 'apodo', h.mesa_apodo - ), - 'items', i.items, - 'totales', jsonb_build_object( - 'items', t.items, - 'total', t.total - ) - ) - ELSE NULL - END -FROM hdr h, agg_items i, tot t; -$$; - - --- --- Name: f_comanda_detalle_rows(integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.f_comanda_detalle_rows(p_id_comanda integer) RETURNS TABLE(id_comanda integer, fec_creacion timestamp without time zone, estado text, observaciones text, id_usuario integer, usuario_nombre text, usuario_apellido text, id_mesa integer, mesa_numero integer, mesa_apodo text, producto_id integer, producto_nombre text, cantidad numeric, pre_unitario numeric, subtotal numeric, items integer, total numeric) - LANGUAGE sql - AS $$ -WITH base AS ( - SELECT - c.id_comanda, c.fec_creacion, c.estado, c.observaciones, - u.id_usuario, u.nombre AS usuario_nombre, u.apellido AS usuario_apellido, - m.id_mesa, m.numero AS mesa_numero, m.apodo AS mesa_apodo, - d.id_producto, p.nombre AS producto_nombre, - d.cantidad, d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM public.comandas c - JOIN public.usuarios u ON u.id_usuario = c.id_usuario - JOIN public.mesas m ON m.id_mesa = c.id_mesa - LEFT JOIN public.deta_comandas d ON d.id_comanda = c.id_comanda - LEFT JOIN public.productos p ON p.id_producto = d.id_producto - WHERE c.id_comanda = p_id_comanda -), -tot AS ( - SELECT - COUNT(*) FILTER (WHERE id_producto IS NOT NULL) AS items, - COALESCE(SUM(subtotal), 0) AS total - FROM base -) -SELECT - b.id_comanda, b.fec_creacion, b.estado, b.observaciones, - b.id_usuario, b.usuario_nombre, b.usuario_apellido, - b.id_mesa, b.mesa_numero, b.mesa_apodo, - b.id_producto, b.producto_nombre, - b.cantidad, b.pre_unitario, b.subtotal, - t.items, t.total -FROM base b CROSS JOIN tot t -ORDER BY b.producto_nombre NULLS LAST; -$$; - - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: comandas; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.comandas ( - id_comanda integer NOT NULL, - id_usuario integer NOT NULL, - id_mesa integer NOT NULL, - fec_creacion timestamp without time zone DEFAULT now() NOT NULL, - estado text NOT NULL, - observaciones text, - fec_cierre timestamp with time zone, - CONSTRAINT comandas_estado_check CHECK ((estado = ANY (ARRAY['abierta'::text, 'cerrada'::text, 'pagada'::text, 'anulada'::text]))) -); - - --- --- Name: COLUMN comandas.fec_cierre; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.comandas.fec_cierre IS 'Fecha/hora de cierre de la comanda (NULL si está abierta)'; - - --- --- Name: deta_comandas; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.deta_comandas ( - id_det_comanda integer NOT NULL, - id_comanda integer NOT NULL, - id_producto integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - observaciones text, - CONSTRAINT deta_comandas_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comandas_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - - --- --- Name: mesas; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.mesas ( - id_mesa integer NOT NULL, - numero integer NOT NULL, - apodo text NOT NULL, - estado text DEFAULT 'libre'::text NOT NULL, - CONSTRAINT mesas_estado_check CHECK ((estado = ANY (ARRAY['libre'::text, 'ocupada'::text, 'reservada'::text]))) -); - - --- --- Name: usuarios; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.usuarios ( - id_usuario integer NOT NULL, - documento text, - img_perfil character varying(255) DEFAULT 'img_perfil.png'::character varying NOT NULL, - nombre text NOT NULL, - apellido text NOT NULL, - correo text, - telefono text, - fec_nacimiento date, - activo boolean DEFAULT true -); - - --- --- Name: v_comandas_resumen; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.v_comandas_resumen AS - WITH items AS ( - SELECT d.id_comanda, - count(*) AS items, - sum((d.cantidad * d.pre_unitario)) AS total - FROM public.deta_comandas d - GROUP BY d.id_comanda - ) - SELECT c.id_comanda, - c.fec_creacion, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - COALESCE(i.items, (0)::bigint) AS items, - COALESCE(i.total, (0)::numeric) AS total, - c.fec_cierre, - CASE - WHEN (c.fec_cierre IS NOT NULL) THEN round((EXTRACT(epoch FROM (c.fec_cierre - (c.fec_creacion)::timestamp with time zone)) / 60.0), 1) - ELSE NULL::numeric - END AS duracion_min - FROM (((public.comandas c - JOIN public.usuarios u ON ((u.id_usuario = c.id_usuario))) - JOIN public.mesas m ON ((m.id_mesa = c.id_mesa))) - LEFT JOIN items i ON ((i.id_comanda = c.id_comanda))); - - --- --- Name: f_comandas_resumen(text, integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.f_comandas_resumen(p_estado text DEFAULT NULL::text, p_limit integer DEFAULT 200) RETURNS SETOF public.v_comandas_resumen - LANGUAGE sql - AS $$ - SELECT * - FROM public.v_comandas_resumen - WHERE (p_estado IS NULL OR estado = p_estado) - ORDER BY id_comanda DESC - LIMIT p_limit; -$$; - - --- --- Name: find_usuarios_por_documentos(jsonb); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.find_usuarios_por_documentos(p_docs jsonb) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH docs AS ( - SELECT DISTINCT - regexp_replace(value::text, '^\s*0+', '', 'g') AS doc_clean, - value::text AS original - FROM jsonb_array_elements_text(COALESCE(p_docs,'[]')) -), -rows AS ( - SELECT d.original AS documento, - u.nombre, - u.apellido, - (u.id_usuario IS NOT NULL) AS found - FROM docs d - LEFT JOIN public.usuarios u - ON regexp_replace(u.documento, '^\s*0+', '', 'g') = d.doc_clean -) -SELECT COALESCE( - jsonb_object_agg( - documento, - jsonb_build_object( - 'nombre', COALESCE(nombre, ''), - 'apellido', COALESCE(apellido, ''), - 'found', found - ) - ), - '{}'::jsonb -) -FROM rows; -$$; - - --- --- Name: get_compra(integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.get_compra(p_id_compra integer) RETURNS jsonb - LANGUAGE sql - AS $$ -WITH cab AS ( - SELECT c.id_compra, c.id_proveedor, c.fec_compra, c.total - FROM public.compras c - WHERE c.id_compra = p_id_compra -), -dm AS ( - SELECT 'MAT'::text AS tipo, d.id_mat_prima AS id, - d.cantidad, d.pre_unitario AS precio - FROM public.deta_comp_materias d WHERE d.id_compra = p_id_compra -), -dp AS ( - SELECT 'PROD'::text AS tipo, d.id_producto AS id, - d.cantidad, d.pre_unitario AS precio - FROM public.deta_comp_producto d WHERE d.id_compra = p_id_compra -), -det AS ( - SELECT jsonb_agg(to_jsonb(x.*)) AS detalles - FROM ( - SELECT * FROM dm - UNION ALL - SELECT * FROM dp - ) x -) -SELECT jsonb_build_object( - 'id_compra', (SELECT id_compra FROM cab), - 'id_proveedor',(SELECT id_proveedor FROM cab), - 'fec_compra', to_char((SELECT fec_compra FROM cab),'YYYY-MM-DD HH24:MI:SS'), - 'total', (SELECT total FROM cab), - 'detalles', COALESCE((SELECT detalles FROM det),'[]'::jsonb) -); -$$; - - --- --- Name: get_materia_prima(integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.get_materia_prima(p_id integer) RETURNS jsonb - LANGUAGE sql - AS $$ -SELECT jsonb_build_object( - 'materia', to_jsonb(mp), - 'proveedores', COALESCE( - ( - SELECT jsonb_agg( - jsonb_build_object( - 'id_proveedor', pr.id_proveedor, - 'raz_social', pr.raz_social, - 'rut', pr.rut, - 'contacto', pr.contacto, - 'direccion', pr.direccion - ) - ) - FROM public.prov_mate_prima pmp - JOIN public.proveedores pr ON pr.id_proveedor = pmp.id_proveedor - WHERE pmp.id_mat_prima = mp.id_mat_prima - ), - '[]'::jsonb - ) -) -FROM public.mate_primas mp -WHERE mp.id_mat_prima = p_id; -$$; - - --- --- Name: get_producto(integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.get_producto(p_id integer) RETURNS jsonb - LANGUAGE sql - AS $$ -SELECT jsonb_build_object( - 'producto', to_jsonb(p), -- el registro completo del producto en JSONB - 'receta', COALESCE( - ( - SELECT jsonb_agg( - jsonb_build_object( - 'id_mat_prima', rp.id_mat_prima, - 'qty_por_unidad', rp.qty_por_unidad, - 'nombre', mp.nombre, - 'unidad', mp.unidad - ) - ) - FROM receta_producto rp - LEFT JOIN mate_primas mp USING (id_mat_prima) - WHERE rp.id_producto = p.id_producto - ), - '[]'::jsonb - ) -) -FROM productos p -WHERE p.id_producto = p_id; -$$; - - --- --- Name: import_asistencia(jsonb, text, text); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.import_asistencia(p_registros jsonb, p_origen text, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE plpgsql - AS $_$ -DECLARE - v_ins_raw INT; - v_ins_pairs INT; - v_miss JSONB; -BEGIN - WITH - -- 1) JSON -> filas - j AS ( - SELECT - regexp_replace(elem->>'doc','^\s*0+','','g')::TEXT AS doc_clean, - (elem->>'isoDate')::DATE AS d, - elem->>'time' AS time_str, - NULLIF(elem->>'mode','') AS modo - FROM jsonb_array_elements(COALESCE(p_registros,'[]')) elem - ), - -- 2) Vincular a usuarios - u AS ( - SELECT j.*, u.id_usuario - FROM j - LEFT JOIN public.usuarios u - ON regexp_replace(u.documento,'^\s*0+','','g') = j.doc_clean - ), - -- 3) Documentos faltantes - miss AS ( - SELECT jsonb_agg(doc_clean) AS missing - FROM u WHERE id_usuario IS NULL - ), - -- 4) TS determinista en TZ del negocio - parsed AS ( - SELECT - u.id_usuario, - u.modo, - make_timestamptz( - EXTRACT(YEAR FROM u.d)::INT, - EXTRACT(MONTH FROM u.d)::INT, - EXTRACT(DAY FROM u.d)::INT, - split_part(u.time_str,':',1)::INT, - split_part(u.time_str,':',2)::INT, - COALESCE(NULLIF(split_part(u.time_str,':',3),''),'0')::INT, - p_tz - ) AS ts_calc - FROM u - WHERE u.id_usuario IS NOT NULL - ), - -- 5) Ventana por usuario (±1 día de lo importado) - win AS ( - SELECT id_usuario, - (MIN(ts_calc) - INTERVAL '1 day') AS t0, - (MAX(ts_calc) + INTERVAL '1 day') AS t1 - FROM parsed - GROUP BY id_usuario - ), - -- 6) Lo existente en BD dentro de la ventana - existing AS ( - SELECT ar.id_usuario, ar.ts - FROM public.asistencia_raw ar - JOIN win w ON w.id_usuario = ar.id_usuario - AND ar.ts BETWEEN w.t0 AND w.t1 - ), - -- 7) CANDIDATE = existente ∪ archivo (sin duplicados) - candidate AS ( - SELECT id_usuario, ts FROM existing - UNION -- ¡clave para evitar doble click! - SELECT id_usuario, ts_calc AS ts FROM parsed - ), - -- 8) Paridad previa (cuántas marcas había ANTES de la ventana) - before_cnt AS ( - SELECT w.id_usuario, COUNT(*)::int AS cnt - FROM win w - JOIN public.asistencia_raw ar - ON ar.id_usuario = w.id_usuario - AND ar.ts < w.t0 - GROUP BY w.id_usuario - ), - -- 9) Línea de tiempo candidata y pares (1→2, 3→4…), jornada = día local del inicio - timeline AS ( - SELECT - c.id_usuario, - c.ts, - ROW_NUMBER() OVER (PARTITION BY c.id_usuario ORDER BY c.ts) AS rn - FROM candidate c - ), - ready AS ( - SELECT - t1.id_usuario, - (t1.ts AT TIME ZONE p_tz)::date AS fecha, - t1.ts AS desde, - t2.ts AS hasta, - EXTRACT(EPOCH FROM (t2.ts - t1.ts))/60.0 AS dur_min - FROM timeline t1 - JOIN timeline t2 - ON t2.id_usuario = t1.id_usuario - AND t2.rn = t1.rn + 1 - LEFT JOIN before_cnt b ON b.id_usuario = t1.id_usuario - WHERE ((COALESCE(b.cnt,0) + t1.rn) % 2) = 1 -- t1 es IN global - AND t2.ts > t1.ts - ), - -- 10) INSERT crudo (dedupe) - ins_raw AS ( - INSERT INTO public.asistencia_raw (id_usuario, ts, modo, origen) - SELECT id_usuario, ts_calc, - NULLIF(modo,'')::text, -- puede quedar NULL para auto-etiquetado - p_origen - FROM parsed - ON CONFLICT (id_usuario, ts) DO NOTHING - RETURNING 1 - ), - -- 11) Auto-etiquetar IN/OUT en BD para filas con modo vacío/'1' (tras insertar) - before_cnt2 AS ( - SELECT w.id_usuario, COUNT(*)::int AS cnt - FROM win w - JOIN public.asistencia_raw ar - ON ar.id_usuario = w.id_usuario - AND ar.ts < w.t0 - GROUP BY w.id_usuario - ), - tl2 AS ( - SELECT - ar.id_usuario, ar.ts, - ROW_NUMBER() OVER (PARTITION BY ar.id_usuario ORDER BY ar.ts) AS rn - FROM public.asistencia_raw ar - JOIN win w ON w.id_usuario = ar.id_usuario - AND ar.ts BETWEEN w.t0 AND w.t1 - ), - label2 AS ( - SELECT - t.id_usuario, - t.ts, - CASE WHEN ((COALESCE(b.cnt,0) + t.rn) % 2) = 1 THEN 'IN' ELSE 'OUT' END AS new_mode - FROM tl2 t - LEFT JOIN before_cnt2 b ON b.id_usuario = t.id_usuario - ), - set_mode AS ( - UPDATE public.asistencia_raw ar - SET modo = l.new_mode - FROM label2 l - WHERE ar.id_usuario = l.id_usuario - AND ar.ts = l.ts - AND (ar.modo IS NULL OR btrim(ar.modo) = '' OR ar.modo ~ '^\s*1\s*$') - RETURNING 1 - ), - -- 12) INSERT pares (dedupe) calculados desde CANDIDATE (ya tiene todo el contexto) - ins_pairs AS ( - INSERT INTO public.asistencia_intervalo (id_usuario, fecha, desde, hasta, dur_min, origen) - SELECT id_usuario, fecha, desde, hasta, dur_min, p_origen - FROM ready - ON CONFLICT (id_usuario, desde, hasta) DO NOTHING - RETURNING 1 - ) - SELECT - (SELECT COUNT(*) FROM ins_raw), - (SELECT COUNT(*) FROM ins_pairs), - (SELECT COALESCE(missing,'[]'::jsonb) FROM miss) - INTO v_ins_raw, v_ins_pairs, v_miss; - - RETURN jsonb_build_object( - 'inserted_raw', v_ins_raw, - 'inserted_pairs', v_ins_pairs, - 'missing_docs', v_miss - ); -END; -$_$; - - --- --- Name: report_asistencia(date, date); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.report_asistencia(p_desde date, p_hasta date) RETURNS TABLE(documento text, nombre text, apellido text, fecha date, desde_hora text, hasta_hora text, dur_min numeric) - LANGUAGE sql - AS $$ - SELECT - u.documento, u.nombre, u.apellido, - ai.fecha, - to_char(ai.desde AT TIME ZONE 'America/Montevideo','HH24:MI:SS') AS desde_hora, - to_char(ai.hasta AT TIME ZONE 'America/Montevideo','HH24:MI:SS') AS hasta_hora, - ai.dur_min - FROM public.asistencia_intervalo ai - JOIN public.usuarios u USING (id_usuario) - WHERE ai.fecha BETWEEN p_desde AND p_hasta - ORDER BY u.documento, ai.fecha, ai.desde; -$$; - - --- --- Name: report_gastos(integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.report_gastos(p_year integer) RETURNS jsonb - LANGUAGE sql STABLE - AS $$ -WITH mdata AS ( - SELECT date_trunc('month', c.fec_compra)::date AS m, - SUM(c.total)::numeric AS importe - FROM public.compras c - WHERE EXTRACT(YEAR FROM c.fec_compra) = p_year - GROUP BY 1 -), -mm AS ( - SELECT EXTRACT(MONTH FROM m)::int AS mes, importe - FROM mdata -) -SELECT jsonb_build_object( - 'year', p_year, - 'total', COALESCE((SELECT SUM(importe) FROM mdata), 0), - 'avg', COALESCE((SELECT AVG(importe) FROM mdata), 0), - 'months', - (SELECT jsonb_agg( - jsonb_build_object( - 'mes', gs, - 'nombre', to_char(to_date(gs::text,'MM'),'Mon'), - 'importe', COALESCE(mm.importe,0) - ) - ORDER BY gs - ) - FROM generate_series(1,12) gs - LEFT JOIN mm ON mm.mes = gs) -); -$$; - - --- --- Name: report_tickets_year(integer, text); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.report_tickets_year(p_year integer, p_tz text DEFAULT 'America/Montevideo'::text) RETURNS jsonb - LANGUAGE sql STABLE - AS $$ -WITH bounds AS ( - SELECT - make_timestamp(p_year, 1, 1, 0,0,0) AS d0, - make_timestamp(p_year+1, 1, 1, 0,0,0) AS d1, - make_timestamptz(p_year, 1, 1, 0,0,0, p_tz) AS t0, - make_timestamptz(p_year+1, 1, 1, 0,0,0, p_tz) AS t1 -), -base AS ( - SELECT - c.id_comanda, - CASE WHEN c.fec_cierre IS NOT NULL - THEN (c.fec_cierre AT TIME ZONE p_tz) - ELSE c.fec_creacion - END AS fec_local, - v.total - FROM public.comandas c - JOIN public.vw_ticket_total v ON v.id_comanda = c.id_comanda - JOIN bounds b ON TRUE - WHERE - (c.fec_cierre IS NOT NULL AND c.fec_cierre >= b.t0 AND c.fec_cierre < b.t1) - OR - (c.fec_cierre IS NULL AND c.fec_creacion >= b.d0 AND c.fec_creacion < b.d1) -), -m AS ( - SELECT - EXTRACT(MONTH FROM fec_local)::int AS mes, - COUNT(*)::int AS cant, - SUM(total)::numeric AS importe, - AVG(total)::numeric AS avg - FROM base - GROUP BY 1 -), -ytd AS ( - SELECT COUNT(*)::int AS total_ytd, - AVG(total)::numeric AS avg_ticket, - SUM(total)::numeric AS to_date - FROM base -) -SELECT jsonb_build_object( - 'year', p_year, - 'total_ytd', (SELECT total_ytd FROM ytd), - 'avg_ticket', (SELECT avg_ticket FROM ytd), - 'to_date', (SELECT to_date FROM ytd), - 'months', - (SELECT jsonb_agg( - jsonb_build_object( - 'mes', mes, - 'nombre', to_char(to_date(mes::text,'MM'),'Mon'), - 'cant', cant, - 'importe', importe, - 'avg', avg - ) - ORDER BY mes - ) - FROM m) -); -$$; - - --- --- Name: save_compra(integer, integer, timestamp with time zone, jsonb); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.save_compra(p_id_compra integer, p_id_proveedor integer, p_fec_compra timestamp with time zone, p_detalles jsonb) RETURNS TABLE(id_compra integer, total numeric) - LANGUAGE plpgsql - AS $$ -DECLARE - v_id INT; - v_total numeric := 0; -BEGIN - IF COALESCE(jsonb_array_length(p_detalles),0) = 0 THEN - RAISE EXCEPTION 'No hay renglones en la compra'; - END IF; - - -- Cabecera (insert/update) - IF p_id_compra IS NULL THEN - INSERT INTO public.compras (id_proveedor, fec_compra, total) - VALUES (p_id_proveedor, COALESCE(p_fec_compra, now()), 0) - RETURNING public.compras.id_compra INTO v_id; - ELSE - UPDATE public.compras c - SET id_proveedor = p_id_proveedor, - fec_compra = COALESCE(p_fec_compra, c.fec_compra) - WHERE c.id_compra = p_id_compra - RETURNING c.id_compra INTO v_id; - - -- Reemplazamos los renglones - DELETE FROM public.deta_comp_materias d WHERE d.id_compra = v_id; - DELETE FROM public.deta_comp_producto p WHERE p.id_compra = v_id; - END IF; - - -- Materias primas (sin CTE: parseo JSON inline) - INSERT INTO public.deta_comp_materias (id_compra, id_mat_prima, cantidad, pre_unitario) - SELECT - v_id, - x.id, - x.cantidad, - x.precio - FROM jsonb_to_recordset(COALESCE(p_detalles, '[]'::jsonb)) - AS x(tipo text, id int, cantidad numeric, precio numeric) - WHERE UPPER(TRIM(x.tipo)) = 'MAT'; - - -- Productos (sin CTE) - INSERT INTO public.deta_comp_producto (id_compra, id_producto, cantidad, pre_unitario) - SELECT - v_id, - x.id, - x.cantidad, - x.precio - FROM jsonb_to_recordset(COALESCE(p_detalles, '[]'::jsonb)) - AS x(tipo text, id int, cantidad numeric, precio numeric) - WHERE UPPER(TRIM(x.tipo)) = 'PROD'; - - -- Recalcular total (calificado) y redondear a ENTERO - SELECT - COALESCE( (SELECT SUM(dcm.cantidad*dcm.pre_unitario) - FROM public.deta_comp_materias dcm - WHERE dcm.id_compra = v_id), 0) - + COALESCE( (SELECT SUM(dcp.cantidad*dcp.pre_unitario) - FROM public.deta_comp_producto dcp - WHERE dcp.id_compra = v_id), 0) - INTO v_total; - - UPDATE public.compras c - SET total = round(v_total, 0) - WHERE c.id_compra = v_id; - - RETURN QUERY SELECT v_id, round(v_total, 0); -END; -$$; - - --- --- Name: save_materia_prima(integer, text, text, boolean, jsonb); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.save_materia_prima(p_id_mat_prima integer, p_nombre text, p_unidad text, p_activo boolean, p_proveedores jsonb DEFAULT '[]'::jsonb) RETURNS integer - LANGUAGE plpgsql - AS $_$ -DECLARE - v_id INT; -BEGIN - IF p_id_mat_prima IS NULL THEN - INSERT INTO public.mate_primas (nombre, unidad, activo) - VALUES (p_nombre, p_unidad, COALESCE(p_activo, TRUE)) - RETURNING mate_primas.id_mat_prima INTO v_id; - ELSE - UPDATE public.mate_primas mp - SET nombre = p_nombre, - unidad = p_unidad, - activo = COALESCE(p_activo, TRUE) - WHERE mp.id_mat_prima = p_id_mat_prima; - v_id := p_id_mat_prima; - END IF; - - -- Sincronizar proveedores: borrar todos y re-crear a partir de JSONB - DELETE FROM public.prov_mate_prima pmp WHERE pmp.id_mat_prima = v_id; - - INSERT INTO public.prov_mate_prima (id_proveedor, id_mat_prima) - SELECT (e->>0)::INT AS id_proveedor, -- elementos JSON como enteros (array simple) - v_id AS id_mat_prima - FROM jsonb_array_elements(COALESCE(p_proveedores, '[]'::jsonb)) AS e - WHERE (e->>0) ~ '^\d+$'; -- solo enteros - - RETURN v_id; -END; -$_$; - - --- --- Name: save_producto(integer, text, text, numeric, boolean, integer, jsonb); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.save_producto(p_id_producto integer, p_nombre text, p_img_producto text, p_precio numeric, p_activo boolean, p_id_categoria integer, p_receta jsonb DEFAULT '[]'::jsonb) RETURNS integer - LANGUAGE plpgsql - AS $_$ -DECLARE - v_id INT; -BEGIN - IF p_id_producto IS NULL THEN - INSERT INTO public.productos (nombre, img_producto, precio, activo, id_categoria) - VALUES (p_nombre, p_img_producto, p_precio, COALESCE(p_activo, TRUE), p_id_categoria) - RETURNING productos.id_producto INTO v_id; - ELSE - UPDATE public.productos p - SET nombre = p_nombre, - img_producto = p_img_producto, - precio = p_precio, - activo = COALESCE(p_activo, TRUE), - id_categoria = p_id_categoria - WHERE p.id_producto = p_id_producto; - v_id := p_id_producto; - END IF; - - -- Limpia receta actual - DELETE FROM public.receta_producto rp WHERE rp.id_producto = v_id; - - -- Inserta SOLO ítems válidos (id entero positivo y cantidad > 0), redondeo a 3 decimales - INSERT INTO public.receta_producto (id_producto, id_mat_prima, qty_por_unidad) - SELECT - v_id, - (rec->>'id_mat_prima')::INT, - ROUND((rec->>'qty_por_unidad')::NUMERIC, 3) - FROM jsonb_array_elements(COALESCE(p_receta, '[]'::jsonb)) AS rec - WHERE - (rec->>'id_mat_prima') ~ '^\d+$' - AND (rec->>'id_mat_prima')::INT > 0 - AND (rec->>'qty_por_unidad') ~ '^\d+(\.\d+)?$' - AND (rec->>'qty_por_unidad')::NUMERIC > 0; - - RETURN v_id; -END; -$_$; - - --- --- Name: asistencia_intervalo; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.asistencia_intervalo ( - id_intervalo bigint NOT NULL, - id_usuario integer NOT NULL, - fecha date NOT NULL, - desde timestamp with time zone NOT NULL, - hasta timestamp with time zone NOT NULL, - dur_min numeric(10,2) NOT NULL, - origen text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT chk_ai_orden CHECK ((hasta > desde)) -); - - --- --- Name: asistencia_intervalo_id_intervalo_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.asistencia_intervalo_id_intervalo_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: asistencia_intervalo_id_intervalo_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.asistencia_intervalo_id_intervalo_seq OWNED BY public.asistencia_intervalo.id_intervalo; - - --- --- Name: asistencia_raw; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.asistencia_raw ( - id_raw bigint NOT NULL, - id_usuario integer NOT NULL, - ts timestamp with time zone NOT NULL, - modo text, - origen text, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - - --- --- Name: asistencia_raw_id_raw_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.asistencia_raw_id_raw_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: asistencia_raw_id_raw_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.asistencia_raw_id_raw_seq OWNED BY public.asistencia_raw.id_raw; - - --- --- Name: asistencia_resumen_diario; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.asistencia_resumen_diario AS - SELECT ai.id_usuario, - u.documento, - u.nombre, - u.apellido, - ai.fecha, - sum(ai.dur_min) AS minutos_dia, - round((sum(ai.dur_min) / 60.0), 2) AS horas_dia, - count(*) AS pares_dia - FROM (public.asistencia_intervalo ai - JOIN public.usuarios u USING (id_usuario)) - GROUP BY ai.id_usuario, u.documento, u.nombre, u.apellido, ai.fecha - ORDER BY ai.id_usuario, ai.fecha; - - --- --- Name: categorias; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.categorias ( - id_categoria integer NOT NULL, - nombre text NOT NULL, - visible boolean DEFAULT true -); - - --- --- Name: categorias_id_categoria_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.categorias_id_categoria_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: categorias_id_categoria_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.categorias_id_categoria_seq OWNED BY public.categorias.id_categoria; - - --- --- Name: clientes; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.clientes ( - id_cliente integer NOT NULL, - nombre text NOT NULL, - correo text, - telefono text, - fec_nacimiento date, - activo boolean DEFAULT true -); - - --- --- Name: clientes_id_cliente_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.clientes_id_cliente_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: clientes_id_cliente_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.clientes_id_cliente_seq OWNED BY public.clientes.id_cliente; - - --- --- Name: comandas_id_comanda_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.comandas_id_comanda_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: comandas_id_comanda_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.comandas_id_comanda_seq OWNED BY public.comandas.id_comanda; - - --- --- Name: compras; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.compras ( - id_compra integer NOT NULL, - id_proveedor integer NOT NULL, - fec_compra timestamp without time zone NOT NULL, - total numeric(14,2) -); - - --- --- Name: compras_id_compra_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.compras_id_compra_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: compras_id_compra_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.compras_id_compra_seq OWNED BY public.compras.id_compra; - - --- --- Name: deta_comandas_id_det_comanda_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.deta_comandas_id_det_comanda_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: deta_comandas_id_det_comanda_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.deta_comandas_id_det_comanda_seq OWNED BY public.deta_comandas.id_det_comanda; - - --- --- Name: deta_comp_materias; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.deta_comp_materias ( - id_compra integer NOT NULL, - id_mat_prima integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - CONSTRAINT deta_comp_materias_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comp_materias_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - - --- --- Name: deta_comp_producto; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.deta_comp_producto ( - id_compra integer NOT NULL, - id_producto integer NOT NULL, - cantidad numeric(12,3) NOT NULL, - pre_unitario numeric(12,2) NOT NULL, - CONSTRAINT deta_comp_producto_cantidad_check CHECK ((cantidad > (0)::numeric)), - CONSTRAINT deta_comp_producto_pre_unitario_check CHECK ((pre_unitario >= (0)::numeric)) -); - - --- --- Name: mate_primas; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.mate_primas ( - id_mat_prima integer NOT NULL, - nombre text NOT NULL, - unidad text NOT NULL, - activo boolean DEFAULT true -); - - --- --- Name: mate_primas_id_mat_prima_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.mate_primas_id_mat_prima_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: mate_primas_id_mat_prima_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.mate_primas_id_mat_prima_seq OWNED BY public.mate_primas.id_mat_prima; - - --- --- Name: mesas_id_mesa_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.mesas_id_mesa_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: mesas_id_mesa_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.mesas_id_mesa_seq OWNED BY public.mesas.id_mesa; - - --- --- Name: productos; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.productos ( - id_producto integer NOT NULL, - nombre text NOT NULL, - img_producto character varying(255) DEFAULT 'img/productos/img_producto.png'::character varying NOT NULL, - precio integer NOT NULL, - activo boolean DEFAULT true, - id_categoria integer NOT NULL, - CONSTRAINT productos_precio_check CHECK (((precio)::numeric >= (0)::numeric)), - CONSTRAINT productos_precio_nn CHECK ((precio >= 0)) -); - - --- --- Name: productos_id_producto_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.productos_id_producto_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: productos_id_producto_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.productos_id_producto_seq OWNED BY public.productos.id_producto; - - --- --- Name: prov_mate_prima; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.prov_mate_prima ( - id_proveedor integer NOT NULL, - id_mat_prima integer NOT NULL -); - - --- --- Name: prov_producto; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.prov_producto ( - id_proveedor integer NOT NULL, - id_producto integer NOT NULL -); - - --- --- Name: proveedores; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.proveedores ( - id_proveedor integer NOT NULL, - rut text, - raz_social text NOT NULL, - direccion text, - contacto text -); - - --- --- Name: proveedores_id_proveedor_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.proveedores_id_proveedor_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: proveedores_id_proveedor_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.proveedores_id_proveedor_seq OWNED BY public.proveedores.id_proveedor; - - --- --- Name: receta_producto; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.receta_producto ( - id_producto integer NOT NULL, - id_mat_prima integer NOT NULL, - qty_por_unidad numeric(12,3) NOT NULL, - CONSTRAINT receta_producto_qty_por_unidad_check CHECK ((qty_por_unidad > (0)::numeric)) -); - - --- --- Name: roles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.roles ( - id_rol integer NOT NULL, - nombre text NOT NULL -); - - --- --- Name: roles_id_rol_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.roles_id_rol_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: roles_id_rol_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.roles_id_rol_seq OWNED BY public.roles.id_rol; - - --- --- Name: usua_roles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.usua_roles ( - id_usuario integer NOT NULL, - id_rol integer NOT NULL, - fec_asignacion timestamp without time zone DEFAULT now(), - autor integer, - activo boolean DEFAULT true -); - - --- --- Name: usuarios_id_usuario_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.usuarios_id_usuario_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: usuarios_id_usuario_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.usuarios_id_usuario_seq OWNED BY public.usuarios.id_usuario; - - --- --- Name: v_comandas_detalle_base; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.v_comandas_detalle_base AS - SELECT c.id_comanda, - c.fec_creacion, - c.fec_cierre, - c.estado, - c.observaciones, - u.id_usuario, - u.nombre AS usuario_nombre, - u.apellido AS usuario_apellido, - m.id_mesa, - m.numero AS mesa_numero, - m.apodo AS mesa_apodo, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal - FROM ((((public.comandas c - JOIN public.usuarios u ON ((u.id_usuario = c.id_usuario))) - JOIN public.mesas m ON ((m.id_mesa = c.id_mesa))) - LEFT JOIN public.deta_comandas d ON ((d.id_comanda = c.id_comanda))) - LEFT JOIN public.productos p ON ((p.id_producto = d.id_producto))); - - --- --- Name: v_comandas_detalle_items; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.v_comandas_detalle_items AS - SELECT d.id_comanda, - d.id_det_comanda, - d.id_producto, - p.nombre AS producto_nombre, - d.cantidad, - d.pre_unitario, - (d.cantidad * d.pre_unitario) AS subtotal, - d.observaciones - FROM (public.deta_comandas d - JOIN public.productos p ON ((p.id_producto = d.id_producto))); - - --- --- Name: v_comandas_detalle_json; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.v_comandas_detalle_json AS - SELECT id_comanda, - jsonb_build_object('id_comanda', id_comanda, 'fec_creacion', fec_creacion, 'fec_cierre', fec_cierre, 'estado', estado, 'observaciones', observaciones, 'usuario', jsonb_build_object('id_usuario', id_usuario, 'nombre', usuario_nombre, 'apellido', usuario_apellido), 'mesa', jsonb_build_object('id_mesa', id_mesa, 'numero', mesa_numero, 'apodo', mesa_apodo), 'items', COALESCE(( SELECT jsonb_agg(jsonb_build_object('producto_id', b.id_producto, 'producto', b.producto_nombre, 'cantidad', b.cantidad, 'pre_unitario', b.pre_unitario, 'subtotal', b.subtotal) ORDER BY b.producto_nombre) AS jsonb_agg - FROM public.v_comandas_detalle_base b - WHERE ((b.id_comanda = h.id_comanda) AND (b.id_producto IS NOT NULL))), '[]'::jsonb), 'totales', jsonb_build_object('items', COALESCE(( SELECT count(*) AS count - FROM public.v_comandas_detalle_base b - WHERE ((b.id_comanda = h.id_comanda) AND (b.id_producto IS NOT NULL))), (0)::bigint), 'total', COALESCE(( SELECT sum(b.subtotal) AS sum - FROM public.v_comandas_detalle_base b - WHERE (b.id_comanda = h.id_comanda)), (0)::numeric))) AS data - FROM ( SELECT DISTINCT v_comandas_detalle_base.id_comanda, - v_comandas_detalle_base.fec_creacion, - v_comandas_detalle_base.fec_cierre, - v_comandas_detalle_base.estado, - v_comandas_detalle_base.observaciones, - v_comandas_detalle_base.id_usuario, - v_comandas_detalle_base.usuario_nombre, - v_comandas_detalle_base.usuario_apellido, - v_comandas_detalle_base.id_mesa, - v_comandas_detalle_base.mesa_numero, - v_comandas_detalle_base.mesa_apodo - FROM public.v_comandas_detalle_base) h; - - --- --- Name: vw_compras; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.vw_compras AS - SELECT c.id_compra, - c.id_proveedor, - p.raz_social AS proveedor, - c.fec_compra, - c.total - FROM (public.compras c - JOIN public.proveedores p USING (id_proveedor)) - ORDER BY c.fec_compra DESC, c.id_compra DESC; - - --- --- Name: vw_ticket_total; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.vw_ticket_total AS - WITH lineas AS ( - SELECT c.id_comanda, - COALESCE(c.fec_cierre, (c.fec_creacion)::timestamp with time zone) AS fec_ticket, - (COALESCE(dc.pre_unitario, (p.precio)::numeric, (0)::numeric))::numeric(14,2) AS pu, - (COALESCE(dc.cantidad, (1)::numeric))::numeric(14,3) AS qty - FROM ((public.comandas c - JOIN public.deta_comandas dc ON ((dc.id_comanda = c.id_comanda))) - LEFT JOIN public.productos p ON ((p.id_producto = dc.id_producto))) - ) - SELECT id_comanda, - fec_ticket, - (sum((qty * pu)))::numeric(14,2) AS total - FROM lineas - GROUP BY id_comanda, fec_ticket; - - --- --- Name: asistencia_intervalo id_intervalo; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.asistencia_intervalo ALTER COLUMN id_intervalo SET DEFAULT nextval('public.asistencia_intervalo_id_intervalo_seq'::regclass); - - --- --- Name: asistencia_raw id_raw; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.asistencia_raw ALTER COLUMN id_raw SET DEFAULT nextval('public.asistencia_raw_id_raw_seq'::regclass); - - --- --- Name: categorias id_categoria; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.categorias ALTER COLUMN id_categoria SET DEFAULT nextval('public.categorias_id_categoria_seq'::regclass); - - --- --- Name: clientes id_cliente; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.clientes ALTER COLUMN id_cliente SET DEFAULT nextval('public.clientes_id_cliente_seq'::regclass); - - --- --- Name: comandas id_comanda; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.comandas ALTER COLUMN id_comanda SET DEFAULT nextval('public.comandas_id_comanda_seq'::regclass); - - --- --- Name: compras id_compra; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.compras ALTER COLUMN id_compra SET DEFAULT nextval('public.compras_id_compra_seq'::regclass); - - --- --- Name: deta_comandas id_det_comanda; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.deta_comandas ALTER COLUMN id_det_comanda SET DEFAULT nextval('public.deta_comandas_id_det_comanda_seq'::regclass); - - --- --- Name: mate_primas id_mat_prima; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mate_primas ALTER COLUMN id_mat_prima SET DEFAULT nextval('public.mate_primas_id_mat_prima_seq'::regclass); - - --- --- Name: mesas id_mesa; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mesas ALTER COLUMN id_mesa SET DEFAULT nextval('public.mesas_id_mesa_seq'::regclass); - - --- --- Name: productos id_producto; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.productos ALTER COLUMN id_producto SET DEFAULT nextval('public.productos_id_producto_seq'::regclass); - - --- --- Name: proveedores id_proveedor; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.proveedores ALTER COLUMN id_proveedor SET DEFAULT nextval('public.proveedores_id_proveedor_seq'::regclass); - - --- --- Name: roles id_rol; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.roles ALTER COLUMN id_rol SET DEFAULT nextval('public.roles_id_rol_seq'::regclass); - - --- --- Name: usuarios id_usuario; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.usuarios ALTER COLUMN id_usuario SET DEFAULT nextval('public.usuarios_id_usuario_seq'::regclass); - - --- --- Name: asistencia_intervalo asistencia_intervalo_id_usuario_desde_hasta_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_id_usuario_desde_hasta_key UNIQUE (id_usuario, desde, hasta); - - --- --- Name: asistencia_intervalo asistencia_intervalo_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_pkey PRIMARY KEY (id_intervalo); - - --- --- Name: asistencia_raw asistencia_raw_id_usuario_ts_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.asistencia_raw - ADD CONSTRAINT asistencia_raw_id_usuario_ts_key UNIQUE (id_usuario, ts); - - --- --- Name: asistencia_raw asistencia_raw_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.asistencia_raw - ADD CONSTRAINT asistencia_raw_pkey PRIMARY KEY (id_raw); - - --- --- Name: categorias categorias_nombre_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.categorias - ADD CONSTRAINT categorias_nombre_key UNIQUE (nombre); - - --- --- Name: categorias categorias_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.categorias - ADD CONSTRAINT categorias_pkey PRIMARY KEY (id_categoria); - - --- --- Name: clientes clientes_correo_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.clientes - ADD CONSTRAINT clientes_correo_key UNIQUE (correo); - - --- --- Name: clientes clientes_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.clientes - ADD CONSTRAINT clientes_pkey PRIMARY KEY (id_cliente); - - --- --- Name: clientes clientes_telefono_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.clientes - ADD CONSTRAINT clientes_telefono_key UNIQUE (telefono); - - --- --- Name: comandas comandas_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.comandas - ADD CONSTRAINT comandas_pkey PRIMARY KEY (id_comanda); - - --- --- Name: compras compras_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.compras - ADD CONSTRAINT compras_pkey PRIMARY KEY (id_compra); - - --- --- Name: deta_comandas deta_comandas_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.deta_comandas - ADD CONSTRAINT deta_comandas_pkey PRIMARY KEY (id_det_comanda); - - --- --- Name: deta_comp_materias deta_comp_materias_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.deta_comp_materias - ADD CONSTRAINT deta_comp_materias_pkey PRIMARY KEY (id_compra, id_mat_prima); - - --- --- Name: deta_comp_producto deta_comp_producto_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.deta_comp_producto - ADD CONSTRAINT deta_comp_producto_pkey PRIMARY KEY (id_compra, id_producto); - - --- --- Name: mate_primas mate_primas_nombre_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mate_primas - ADD CONSTRAINT mate_primas_nombre_key UNIQUE (nombre); - - --- --- Name: mate_primas mate_primas_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mate_primas - ADD CONSTRAINT mate_primas_pkey PRIMARY KEY (id_mat_prima); - - --- --- Name: mesas mesas_apodo_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mesas - ADD CONSTRAINT mesas_apodo_key UNIQUE (apodo); - - --- --- Name: mesas mesas_numero_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mesas - ADD CONSTRAINT mesas_numero_key UNIQUE (numero); - - --- --- Name: mesas mesas_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mesas - ADD CONSTRAINT mesas_pkey PRIMARY KEY (id_mesa); - - --- --- Name: productos productos_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.productos - ADD CONSTRAINT productos_pkey PRIMARY KEY (id_producto); - - --- --- Name: prov_mate_prima prov_mate_prima_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.prov_mate_prima - ADD CONSTRAINT prov_mate_prima_pkey PRIMARY KEY (id_proveedor, id_mat_prima); - - --- --- Name: prov_producto prov_producto_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.prov_producto - ADD CONSTRAINT prov_producto_pkey PRIMARY KEY (id_proveedor, id_producto); - - --- --- Name: proveedores proveedores_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.proveedores - ADD CONSTRAINT proveedores_pkey PRIMARY KEY (id_proveedor); - - --- --- Name: proveedores proveedores_rut_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.proveedores - ADD CONSTRAINT proveedores_rut_key UNIQUE (rut); - - --- --- Name: receta_producto receta_producto_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.receta_producto - ADD CONSTRAINT receta_producto_pkey PRIMARY KEY (id_producto, id_mat_prima); - - --- --- Name: roles roles_nombre_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.roles - ADD CONSTRAINT roles_nombre_key UNIQUE (nombre); - - --- --- Name: roles roles_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.roles - ADD CONSTRAINT roles_pkey PRIMARY KEY (id_rol); - - --- --- Name: usua_roles usua_roles_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.usua_roles - ADD CONSTRAINT usua_roles_pkey PRIMARY KEY (id_usuario, id_rol); - - --- --- Name: usuarios usuarios_documento_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.usuarios - ADD CONSTRAINT usuarios_documento_key UNIQUE (documento); - - --- --- Name: usuarios usuarios_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.usuarios - ADD CONSTRAINT usuarios_pkey PRIMARY KEY (id_usuario); - - --- --- Name: compras_fec_compra_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX compras_fec_compra_idx ON public.compras USING btree (fec_compra); - - --- --- Name: idx_asist_int_usuario_fecha; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_asist_int_usuario_fecha ON public.asistencia_intervalo USING btree (id_usuario, fecha); - - --- --- Name: idx_asist_raw_usuario_ts; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_asist_raw_usuario_ts ON public.asistencia_raw USING btree (id_usuario, ts); - - --- --- Name: idx_detalle_comanda_comanda; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_detalle_comanda_comanda ON public.deta_comandas USING btree (id_comanda); - - --- --- Name: idx_detalle_comanda_producto; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_detalle_comanda_producto ON public.deta_comandas USING btree (id_producto); - - --- --- Name: ix_comandas_fec_cierre; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX ix_comandas_fec_cierre ON public.comandas USING btree (fec_cierre); - - --- --- Name: ix_comandas_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX ix_comandas_id ON public.comandas USING btree (id_comanda); - - --- --- Name: ix_deta_comandas_id_comanda; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX ix_deta_comandas_id_comanda ON public.deta_comandas USING btree (id_comanda); - - --- --- Name: ix_deta_comandas_id_producto; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX ix_deta_comandas_id_producto ON public.deta_comandas USING btree (id_producto); - - --- --- Name: asistencia_intervalo asistencia_intervalo_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.asistencia_intervalo - ADD CONSTRAINT asistencia_intervalo_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES public.usuarios(id_usuario) ON DELETE CASCADE; - - --- --- Name: asistencia_raw asistencia_raw_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.asistencia_raw - ADD CONSTRAINT asistencia_raw_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES public.usuarios(id_usuario) ON DELETE CASCADE; - - --- --- Name: comandas comandas_id_mesa_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.comandas - ADD CONSTRAINT comandas_id_mesa_fkey FOREIGN KEY (id_mesa) REFERENCES public.mesas(id_mesa); - - --- --- Name: comandas comandas_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.comandas - ADD CONSTRAINT comandas_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES public.usuarios(id_usuario); - - --- --- Name: compras compras_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.compras - ADD CONSTRAINT compras_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES public.proveedores(id_proveedor); - - --- --- Name: deta_comandas deta_comandas_id_comanda_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.deta_comandas - ADD CONSTRAINT deta_comandas_id_comanda_fkey FOREIGN KEY (id_comanda) REFERENCES public.comandas(id_comanda) ON DELETE CASCADE; - - --- --- Name: deta_comandas deta_comandas_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.deta_comandas - ADD CONSTRAINT deta_comandas_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES public.productos(id_producto); - - --- --- Name: deta_comp_materias deta_comp_materias_id_compra_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.deta_comp_materias - ADD CONSTRAINT deta_comp_materias_id_compra_fkey FOREIGN KEY (id_compra) REFERENCES public.compras(id_compra) ON DELETE CASCADE; - - --- --- Name: deta_comp_materias deta_comp_materias_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.deta_comp_materias - ADD CONSTRAINT deta_comp_materias_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES public.mate_primas(id_mat_prima); - - --- --- Name: deta_comp_producto deta_comp_producto_id_compra_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.deta_comp_producto - ADD CONSTRAINT deta_comp_producto_id_compra_fkey FOREIGN KEY (id_compra) REFERENCES public.compras(id_compra) ON DELETE CASCADE; - - --- --- Name: deta_comp_producto deta_comp_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.deta_comp_producto - ADD CONSTRAINT deta_comp_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES public.productos(id_producto); - - --- --- Name: productos productos_id_categoria_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.productos - ADD CONSTRAINT productos_id_categoria_fkey FOREIGN KEY (id_categoria) REFERENCES public.categorias(id_categoria); - - --- --- Name: prov_mate_prima prov_mate_prima_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.prov_mate_prima - ADD CONSTRAINT prov_mate_prima_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES public.mate_primas(id_mat_prima); - - --- --- Name: prov_mate_prima prov_mate_prima_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.prov_mate_prima - ADD CONSTRAINT prov_mate_prima_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES public.proveedores(id_proveedor) ON DELETE CASCADE; - - --- --- Name: prov_producto prov_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.prov_producto - ADD CONSTRAINT prov_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES public.productos(id_producto); - - --- --- Name: prov_producto prov_producto_id_proveedor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.prov_producto - ADD CONSTRAINT prov_producto_id_proveedor_fkey FOREIGN KEY (id_proveedor) REFERENCES public.proveedores(id_proveedor) ON DELETE CASCADE; - - --- --- Name: receta_producto receta_producto_id_mat_prima_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.receta_producto - ADD CONSTRAINT receta_producto_id_mat_prima_fkey FOREIGN KEY (id_mat_prima) REFERENCES public.mate_primas(id_mat_prima); - - --- --- Name: receta_producto receta_producto_id_producto_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.receta_producto - ADD CONSTRAINT receta_producto_id_producto_fkey FOREIGN KEY (id_producto) REFERENCES public.productos(id_producto) ON DELETE CASCADE; - - --- --- Name: usua_roles usua_roles_autor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.usua_roles - ADD CONSTRAINT usua_roles_autor_fkey FOREIGN KEY (autor) REFERENCES public.usuarios(id_usuario); - - --- --- Name: usua_roles usua_roles_id_rol_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.usua_roles - ADD CONSTRAINT usua_roles_id_rol_fkey FOREIGN KEY (id_rol) REFERENCES public.roles(id_rol); - - --- --- Name: usua_roles usua_roles_id_usuario_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.usua_roles - ADD CONSTRAINT usua_roles_id_usuario_fkey FOREIGN KEY (id_usuario) REFERENCES public.usuarios(id_usuario) ON DELETE CASCADE; - - --- --- PostgreSQL database dump complete --- - - -COMMIT; diff --git a/services/auth/src/db/poolSingleton.js b/services/auth/src/db/poolSingleton.js deleted file mode 100644 index a0935c2..0000000 --- a/services/auth/src/db/poolSingleton.js +++ /dev/null @@ -1,83 +0,0 @@ -// Coneción Singleton a base de datos. - -import { Pool } from 'pg'; - -class DatabaseCore { - constructor() { - - if (DatabaseCore.instance) { - return Database.instance; - } - - const config = { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - port: process.env.DB_LOCAL_PORT ? Number(process.env.DB_LOCAL_PORT) : undefined, - ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined, - }; - - this.connection = new Pool(config); - - DatabaseCore.instance = this; - } - async query(sql, params) { - return this.connection.query(sql,params); - } - - async connect() { /* Definida solo para evitar errores */ - return this.connection.connect(); - } - async getClient() { - return this.connection.connect(); - } - - async release() { - await this.connection.end(); - } -} -class DatabaseTenants { - constructor() { - - if (DatabaseTenants.instance) { - return Database.instance; - } - - const config = { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - port: process.env.DB_LOCAL_PORT ? Number(process.env.DB_LOCAL_PORT) : undefined, - ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined, - }; - - this.connection = new Pool(config); - - DatabaseTenants.instance = this; - } - async query(sql, params) { - return this.connection.query(sql,params); - } - - async connect() { /* Definida solo para evitar errores */ - return this.connection.connect(); - } - async getClient() { - return this.connection.connect(); - } - - async release() { - await this.connection.end(); - } -} - -// const db = new Database(); -// db.query('SELECT * FROM users'); - -const poolCore = new DatabaseCore(); -const poolTenants = new DatabaseTenants(); -export default {poolCore, poolTenants}; -export { poolCore, poolTenants }; -//export { DatabaseCore, DatabaseTenants }; \ No newline at end of file diff --git a/services/auth/src/index.mjs b/services/auth/src/index.mjs index 3f3442d..2968268 100644 --- a/services/auth/src/index.mjs +++ b/services/auth/src/index.mjs @@ -1,240 +1,328 @@ -// services/auth/src/index.js +// services/auth/src/index.mjs // ------------------------------------------------------------ // SuiteCoffee — Servicio de Autenticación (Express + OIDC) // ------------------------------------------------------------ import 'dotenv/config'; -import express from 'express'; // Framework para enderizado de apps Web -import expressLayouts from 'express-ejs-layouts'; -// import { poolCore, poolTenants } from '@suitecoffee/db'; // dbCore y dbTenants desde módulo -import { poolCore, poolTenants } from '#db'; // dbCore y dbTenants -import v1Router from '#v1Router'; // Rutas API v1 -import expressPages from '#pages'; // Rutas "/", "/dashboard", ... +import express from 'express'; + +import session from 'express-session'; +import { RedisStore } from 'connect-redis'; +import { generators } from 'openid-client'; +// import { initOIDCFromEnv } from '@suitecoffee/oidc'; +import { initOIDCFromEnv, getOIDC } from '@suitecoffee/oidc'; + +import { verificarConexionCore, verificarConexionTenants } from '@suitecoffee/db'; +import { redisAuthentik, verificarConexionRedisAuthentik } from '@suitecoffee/redis'; + +import { checkRequiredEnvVars } from '@suitecoffee/scripts'; import path from 'path'; -import { fileURLToPath } from 'url'; // Converts a file:// URL string or URL object into a platform-specific file +import { fileURLToPath } from 'url'; import cookieParser from 'cookie-parser'; +import { ensureUserAndTenantOnFirstLogin } from './registration/bootstrap.mjs'; + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -import fs from 'node:fs/promises'; -import crypto from 'node:crypto'; -import fetch from "node-fetch"; - - // ----------------------------------------------------------------------------- -// Validación de entorno mínimo (ajusta nombres si difieren) +// Validación de entorno mínimo // ----------------------------------------------------------------------------- -// Función para verificar que ciertas variables de entorno estén definida -function checkRequiredEnvVars(...requiredKeys) { - const missingKeys = requiredKeys.filter((key) => !process.env[key]); // Filtramos las que NO existen en process.env - if (missingKeys.length > 0) { // Si falta alguna, mostramos una advertencia - console.warn( - `[APP] No se encontraron las siguientes variables de entorno: \n\n-> ${missingKeys.join('\n-> ')}`+ - `\n` - ); - } -} - checkRequiredEnvVars( 'PORT', 'APP_BASE_URL', - 'CORE_DB_HOST', 'CORE_DB_PORT', 'CORE_DB_NAME', - 'TENANTS_DB_HOST', 'TENANTS_DB_PORT', 'TENANTS_DB_NAME', - - 'OIDC_LOGIN_URL', 'OIDC_REDIRECT_URI', - 'OIDC_CLIEN_ID', 'OIDC_CONFIG_URL', 'OIDC_ISSUER', - 'OIDC_ISSUER_DISCOVERY', 'OIDC_AUTHORIZE_URL', 'OIDC_TOKEN_URL', - 'OIDC_USERINFO_URL', 'OIDC_LOGOUT_URL', 'OIDC_JWKS_URL', - - 'SESSION_SECRET', 'SESSION_COOKIE_NAME', - 'AK_REDIS_URL', 'AK_TOKEN' + 'SESSION_SECRET', 'SESSION_NAME', 'AK_REDIS_URL', + 'OIDC_CLIENT_ID', 'OIDC_REDIRECT_URI', + 'OIDC_CONFIG_URL' // o 'OIDC_ISSUER' ); - - - - // ---------------------------------------------------------- // Variables del sistema // ---------------------------------------------------------- - -// De entorno -const PORT = process.env.PORT; -const APP_BASE_URL = process.env.APP_BASE_URL; - -const CORE_DB_HOST = process.env.CORE_DB_HOST; -const CORE_DB_PORT = process.env.CORE_DB_PORT; -const CORE_DB_NAME = process.env.CORE_DB_NAME; - -const TENANTS_DB_HOST = process.env.TENANTS_DB_HOST; -const TENANTS_DB_PORT = process.env.TENANTS_DB_PORT; -const TENANTS_DB_NAME = process.env.TENANTS_DB_NAME; - -const OIDC_LOGIN_URL = process.env.OIDC_LOGIN_URL; -const OIDC_REDIRECT_URI = process.env.OIDC_REDIRECT_URI; - -const OIDC_CLIEN_ID = process.env.OIDC_CLIEN_ID; -const OIDC_CONFIG_URL = process.env.OIDC_CONFIG_URL; -const OIDC_ISSUER = process.env.OIDC_ISSUER; -const OIDC_ISSUER_DISCOVERY = process.env.OIDC_ISSUER_DISCOVERY; -const OIDC_AUTHORIZE_URL = process.env.OIDC_AUTHORIZE_URL; -const OIDC_TOKEN_URL = process.env.OIDC_TOKEN_URL; -const OIDC_USERINFO_URL = process.env.OIDC_USERINFO_URL; -const OIDC_LOGOUT_URL = process.env.OIDC_LOGOUT_URL; -const OIDC_JWKS_URL = process.env.OIDC_JWKS_URL; - -const AK_SESSION_SECRET = process.env.AK_SESSION_SECRET; -const AK_SESSION_COOKIE_NAME = process.env.AK_SESSION_COOKIE_NAME; -const AK_REDIS_URL = process.env.AK_REDIS_URL; - - +const PORT = process.env.PORT; +const SESSION_NAME = process.env.SESSION_NAME; +const SESSION_SECRET = process.env.SESSION_SECRET; +const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN; +const LOGGED_OUT_PATH = process.env.LOGGED_OUT_PATH || '/logged-out'; +const APP_BASE_URL = process.env.APP_BASE_URL; // ----------------------------------------------------------------------------- -// Utilidades / Helpers +// Config Express // ----------------------------------------------------------------------------- - - - - - -// ----------------------------------------------------------------------------- -// Configuración Express -// ----------------------------------------------------------------------------- - const app = express(); app.set('trust proxy', true); -app.set("views", path.join(__dirname, "views")); -app.set("view engine", "ejs"); -app.set("layout", "layouts/main"); -app.disable("x-powered-by"); - app.use(express.json()); -app.use(express.json({ limit: '1mb' })); -app.use(express.urlencoded({ extended: true })); -// Archivos estáticos que fuerzan la re-descarga de arhivos -app.use(express.static(path.join(__dirname, "public"), { - etag: false, maxAge: 0, - setHeaders: (res, path) => { - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); - } +app.use(cookieParser(SESSION_SECRET)); + +// ----------------------------------------------------------------------------- +// Sesión (Redis) +// ----------------------------------------------------------------------------- +await redisAuthentik.connect(); +const redisClient = redisAuthentik.getClient(); + +app.use(session({ + name: SESSION_NAME, + store: new RedisStore({ client: redisClient, prefix: 'sess:' }), + secret: SESSION_SECRET, + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + ...(COOKIE_DOMAIN ? { domain: COOKIE_DOMAIN } : {}), + }, })); -app.use(cookieParser(process.env.SESSION_SECRET)); -app.use(expressPages); // Renderizado trae las paginas desde ./services/manso/src/routes/routes.js - - - - - - - -// ---------------------------------------------------------- -// Middleware para datos globales -// ---------------------------------------------------------- -app.use((req, res, next) => { - res.locals.currentPath = req.path; - res.locals.pageTitle = "SuiteCoffee"; - res.locals.pageId = ""; - next(); -}); - - - -// ---------------------------------------------------------- -// Verificación de conexión -// ---------------------------------------------------------- - -async function verificarConexionCore() { - try { - console.log(`[APP] Comprobando accesibilidad a la db ${CORE_DB_NAME} del host ${CORE_DB_HOST} ...`); - const client = await poolCore.connect(); - const { rows } = await client.query('SELECT NOW() AS ahora'); - console.log(`\n[APP] Conexión con ${CORE_DB_NAME} OK. Hora DB:`, rows[0].ahora); - client.release(); - } catch (error) { - console.error('[APP] Error al conectar con la base de datos al iniciar:', error.message); - console.error('[APP] Revisar credenciales, accesos de red y firewall.'); - } -} -async function verificarConexionTenants() { - try { - console.log(`[APP] Comprobando accesibilidad a la db ${TENANTS_DB_NAME} del host ${TENANTS_DB_HOST} ...`); - const client = await poolTenants.connect(); - const { rows } = await client.query('SELECT NOW() AS ahora'); - console.log(`\n[APP] Conexión con ${TENANTS_DB_NAME} OK. Hora DB:`, rows[0].ahora); - client.release(); - } catch (error) { - console.error('[APP] Error al conectar con la base de datos al iniciar:', error.message); - console.error('[APP] Revisar credenciales, accesos de red y firewall.'); - } -} - - - -// ============================================= -// Registro de usuario (DB principal) -// ============================================= - - - //============================================= // ---------- Authentik (API & OIDC) ---------- //============================================= +// const { client, getAuthUrl, handleCallback, endSessionUrl } = await initOIDCFromEnv(); +await initOIDCFromEnv(); +const oidc = getOIDC(); -// =========================== -// GET /auth/users/register -// =========================== - -// =========================== -// POST /auth/login -// =========================== - -app.get("/auth/login", (req, res) => { - const { verifier, challenge } = genPKCE(); - const state = base64url(crypto.randomBytes(24)); - req.session.pkce_verifier = verifier; - req.session.oidc_state = state; - const url = authorizeUrl({ state, challenge }); - res.redirect(302, url); +app.get('/auth/debug/session', (req, res) => { + res.json({ sid: req.sessionID, user: req.session?.user ?? null }); }); +// =========================== +// GET /auth/login +// =========================== +/* +app.get('/auth/login', async (req, res) => { + const code_verifier = generators.codeVerifier(); + const code_challenge = generators.codeChallenge(code_verifier); + const state = generators.state(); + const nonce = generators.nonce(); + + const returnTo = typeof req.query.returnTo === 'string' ? req.query.returnTo : '/'; + + req.session.oidc = { code_verifier, state, nonce, returnTo }; + await new Promise(r => req.session.save(r)); + + const authUrl = getAuthUrl({ state, nonce, code_challenge }); + console.log('[OIDC] Redirect auth URL:', authUrl); + return res.redirect(authUrl); +}); +*/ + +app.get('/auth/login', async (req, res, next) => { + try { + const state = generators.state(); + const nonce = generators.nonce(); + const code_verifier = generators.codeVerifier(); + const code_challenge = generators.codeChallenge(code_verifier); + + // Guardamos artefactos para el callback + req.session.oidc = { state, nonce, code_verifier }; + + // Usamos la API del paquete @suitecoffee/oidc + const authUrl = oidc.getAuthUrl({ state, nonce, code_challenge }); + res.redirect(authUrl); + } catch (err) { + next(err); + } +}); + // =========================== // GET /auth/callback // =========================== +/* +app.get('/auth/callback', async (req, res) => { + const { oidc } = req.session || {}; + const code_verifier = oidc?.code_verifier; + const stateStored = oidc?.state; + const nonceStored = oidc?.nonce; + const returnTo = oidc?.returnTo || '/'; + if (!code_verifier || !stateStored) { + console.warn('[OIDC] Falta code_verifier/state en sesión; reiniciando login'); + return res.redirect(303, '/auth/login'); + } + + // helper para quitar guiones + const noDash = (v) => (typeof v === 'string' + ? v.replace(/-/g, '') + : String(v ?? '').replace(/-/g, '')); + + try { + const tokenSet = await handleCallback(req, { + code_verifier, + state: stateStored, + nonce: nonceStored + }); + + // Limpiar datos transitorios OIDC + req.session.oidc = undefined; + + // Claims del IdP + const claims = tokenSet.claims(); + const email = String(claims.email || '').toLowerCase(); + const sub = claims.sub; + + // 1) Asegurar usuario en CORE y tenant (si es primer login) + const { user, memberships, current_tenant } = + await ensureUserAndTenantOnFirstLogin(claims); + + // Normalizaciones sin guiones + const userUidNoDash = noDash(user.user_id); + const currentTenantNoDash = noDash(current_tenant); + + // Normalizar memberships + derivar schemaName cuando falte + const prefix = process.env.TENANT_SCHEMA_PREFIX || 'empresa_'; + const normalizedMemberships = (memberships || []).map(m => { + const tenantUidNoDash = noDash(m.tenant_id); + const schemaName = m.schema_name || `${prefix}${tenantUidNoDash}`; + return { + tenant_id: m.tenant_id, + schema_name: schemaName, + role: m.role, + // duplicados camelCase y sin guiones + tenantId: m.tenant_id, + schemaName, + tenant_uid_nodash: tenantUidNoDash, + tenantUidNoDash, + }; + }); + + // Membership activo (por current_tenant o primero) + const active = normalizedMemberships.find(m => String(m.tenant_id) === String(current_tenant)) + || normalizedMemberships[0] + || null; + + // 2) Regenerar sesión y guardar identidad + memberships/tenant actual + req.session.regenerate(err => { + if (err) { + console.warn('[OIDC] error al regenerar sesión:', err); + return res.redirect(303, '/auth/login'); + } + + req.session.user = { + sub, + email, + user_id: user.user_id, + name: user.name, + + // ids sin guiones (para comparar con schema_name, slugs, etc.) + user_uid_nodash: userUidNoDash, + userUidNoDash, + + // tenant activo (snake + camel + sin guiones) + current_tenant, // UUID + currentTenant: current_tenant, // UUID + current_tenant_nodash: currentTenantNoDash, + currentTenantNoDash: currentTenantNoDash, + + // esquema activo (muchos middlewares esperan esto) + active_schema: active?.schema_name || null, + activeSchema: active?.schemaName || null, + + // membresías normalizadas + memberships: normalizedMemberships, + + // id_token para logout federado + id_token: tokenSet.id_token, + }; + + req.session.save(() => { + const dest = returnTo.startsWith('/') ? returnTo : '/'; + return res.redirect(303, dest); + }); + }); + } catch (err) { + console.warn('[OIDC] callback error:', err?.message || err); + req.session.oidc = undefined; + req.session.save(() => res.redirect(303, '/auth/login')); + } +}); +*/ + +app.get('/auth/callback', async (req, res, next) => { + try { + const ctx = req.session.oidc ?? {}; + const { state, nonce, code_verifier } = ctx; + + if (!state || !nonce || !code_verifier) { + return res.status(400).json({ error: 'missing OIDC PKCE artifacts in session' }); + } + + // Intercambio del code por tokens (usa client.callbackParams internamente) + const tokenSet = await oidc.handleCallback(req, { state, nonce, code_verifier }); + + // Carga de claims complementarios (userinfo) + const userinfo = await oidc.client.userinfo(tokenSet.access_token); + + const claims = { + sub: userinfo.sub, + email: userinfo.email, + name: userinfo.name ?? userinfo.preferred_username ?? null, + }; + + // Bootstrap en BD CORE/TENANTS (IDs 32-hex, sin guiones) + const result = await ensureUserAndTenantOnFirstLogin(claims); + + // Guardar sesión para la App (sin hash/noHash) + req.session.user = { + sub: result.user.sub, + email: result.user.email, + name: result.user.name, + user_id: result.user.user_id, // 32-hex + default_tenant: result.user.default_tenant, // 32-hex + memberships: result.memberships.map(m => ({ + tenant_id: m.tenant_id, // 32-hex + role: m.role, + })), + }; + + // limpiar artefactos OIDC + delete req.session.oidc; + + // Redirige a la App + res.redirect(`${APP_BASE_URL}/inicio`); + } catch (err) { + next(err); + } +}); + + +// ============================== +// POST /auth/logout +// ============================== +/* +app.post('/auth/logout', (req, res) => { + const idToken = req.session?.user?.id_token; + const postLogout = `${APP_BASE_URL}${LOGGED_OUT_PATH}`; + + req.session.destroy(() => { + const url = endSessionUrl({ id_token_hint: idToken, post_logout_redirect_uri: postLogout }); + return url ? res.redirect(url) : res.redirect(postLogout); + }); +}); +*/ + +app.post('/auth/logout', (req, res, next) => { + req.session.destroy((e) => { + if (e) return next(e); + res.clearCookie(SESSION_NAME, { path: '/' }); + res.status(204).end(); + }); +}); // ----------------------------------------------------------------------------- // Healthcheck // ----------------------------------------------------------------------------- app.get('/health', (_req, res) => { - res.status(200).json({ status: 'ok'}), - console.log(`[AUTH] Saludable`) + res.status(200).json({ status: 'ok' }); }); - - -// ============================================= -// 404 + Manejo de errores -// ============================================= -app.use((req, res) => res.status(404).json({ error: 'Error 404, No se encontró la página', path: req.originalUrl })); - -app.use((err, _req, res, _next) => { - console.error('[AUTH] ', err); - if (res.headersSent) return; - res.status(500).json({ error: '¡Oh! A ocurrido un error en el servidor auth.', detail: err.stack || String(err) }); -}); - - - - // ----------------------------------------------------------------------------- // Arranque // ----------------------------------------------------------------------------- -app.listen(PORT, () => { +app.listen(PORT, async () => { console.log(`[AUTH] Servicio de autenticación escuchando en http://localhost:${PORT}`); - verificarConexionCore(); - verificarConexionTenants(); -}); \ No newline at end of file + await verificarConexionCore(); + await verificarConexionTenants(); + await verificarConexionRedisAuthentik(); +}); diff --git a/services/auth/src/pages/index.html.bak b/services/auth/src/pages/index.html.bak deleted file mode 100644 index 33ae362..0000000 --- a/services/auth/src/pages/index.html.bak +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - SuiteCoffee - Autenticación - - - - - -
-

Iniciar Sesión

- - - - - -
- - -
- -
-
- -
- - -
- -
- -
-
- - - - - - \ No newline at end of file diff --git a/services/auth/src/pages/pages.js b/services/auth/src/pages/pages.js deleted file mode 100644 index bbf228c..0000000 --- a/services/auth/src/pages/pages.js +++ /dev/null @@ -1,20 +0,0 @@ -// services/manso/src/api/v1/routes/routes.js - -import { Router } from 'express'; - -const router = Router(); - -// ---------------------------------------------------------- -// Rutas de UI -// ---------------------------------------------------------- - -/*router.get('/', (req, res) => { - res.locals.pageTitle = "Inicio"; // Título de pestaña - res.locals.pageId = "home"; // Sidebar contextual - res.render("dashboard"); // Archivo .ejs a renderizar - // res.json({ ok: true, route: '/inicio' }); // Debug json -});*/ - - - -export default router; \ No newline at end of file diff --git a/services/auth/src/registration/bootstrap.mjs b/services/auth/src/registration/bootstrap.mjs new file mode 100644 index 0000000..830eb11 --- /dev/null +++ b/services/auth/src/registration/bootstrap.mjs @@ -0,0 +1,147 @@ +// services/auth/src/registration/bootstrap.mjs +import { poolCore, poolTenants } from '@suitecoffee/db'; + +/** + * ensureUserAndTenantOnFirstLogin + * Crea/actualiza el usuario en CORE y, si no tiene membresías, + * provisiona su primer tenant (IDs 32-hex SIN guiones). + * + * @param {object} claims OIDC claims { sub, email, name? } + * @returns {Promise<{ user, memberships, current_tenant }>} + */ +export async function ensureUserAndTenantOnFirstLogin(claims) { + const { sub, email, name = null } = claims ?? {}; + if (!sub || !email) { + throw new Error('ensureUserAndTenantOnFirstLogin: faltan claims requeridos (sub, email)'); + } + + // 1) Upsert del usuario por sub + const core = await poolCore.connect(); + try { + await core.query('BEGIN'); + + // Existe? + const ures = await core.query( + `SELECT user_id, sub, email, name, default_tenant + FROM sc_users + WHERE sub = $1`, + [sub] + ); + + let userRow; + if (ures.rowCount === 0) { + // crea usando defaults de la BD (uuid_nodash()) + const ins = await core.query( + `INSERT INTO sc_users (sub, email, name) + VALUES ($1, $2, $3) + RETURNING user_id, sub, email, name, default_tenant`, + [sub, email, name] + ); + userRow = ins.rows[0]; + } else { + userRow = ures.rows[0]; + // actualización mínima de datos cambiantes + if (userRow.email !== email || userRow.name !== name) { + const upd = await core.query( + `UPDATE sc_users + SET email = $2, + name = $3 + WHERE sub = $1 + RETURNING user_id, sub, email, name, default_tenant`, + [sub, email, name] + ); + userRow = upd.rows[0]; + } + } + + // 2) ¿Tiene membresías? + const mres = await core.query( + `SELECT m.user_id, m.tenant_id, m.role, t.schema_name, t.state, t.created_at + FROM sc_memberships m + JOIN sc_tenants t ON t.tenant_id = m.tenant_id + WHERE m.user_id = $1 + ORDER BY t.created_at ASC`, + [userRow.user_id] + ); + + // Si ya tiene, salimos dejando todo igual + if (mres.rowCount > 0) { + await core.query('COMMIT'); + return { + user: { + user_id: userRow.user_id, sub: userRow.sub, email: userRow.email, + name: userRow.name, default_tenant: userRow.default_tenant + }, + memberships: mres.rows, + current_tenant: userRow.default_tenant ?? mres.rows[0].tenant_id + }; + } + + // 3) Crear primer tenant en CORE (IDs sin guiones) + // - Generamos tenant_id y derivamos schema_name/owner_role en una sola sentencia + const tins = await core.query( + `WITH g AS (SELECT uuid_nodash() AS tid) + INSERT INTO sc_tenants (tenant_id, schema_name, owner_role, state) + SELECT g.tid, 'empresa_' || g.tid, 'owner_' || g.tid, 'provisioning' + FROM g + RETURNING tenant_id, schema_name, owner_role, state`, + ); + const tenant = tins.rows[0]; + + // 4) Ejecutar provisión física en DB TENANTS + // - Crea el esquema empresa_ y objetos del tenant + await poolTenants.query( + `SELECT public.f_crear_empresa($1, $2)`, + [tenant.tenant_id, 'empresa_'] + ); + + // 5) Marcar tenant listo y setear default_tenant del usuario + await core.query( + `UPDATE sc_tenants SET state = 'ready' WHERE tenant_id = $1`, + [tenant.tenant_id] + ); + + await core.query( + `INSERT INTO sc_memberships (user_id, tenant_id, role) + VALUES ($1, $2, 'owner') + ON CONFLICT (user_id, tenant_id) DO NOTHING`, + [userRow.user_id, tenant.tenant_id] + ); + + const updUser = await core.query( + `UPDATE sc_users + SET default_tenant = $2 + WHERE user_id = $1 + RETURNING user_id, sub, email, name, default_tenant`, + [userRow.user_id, tenant.tenant_id] + ); + + userRow = updUser.rows[0]; + + // 6) Cargar membresías finales + const mres2 = await core.query( + `SELECT m.user_id, m.tenant_id, m.role, t.schema_name, t.state, t.created_at + FROM sc_memberships m + JOIN sc_tenants t ON t.tenant_id = m.tenant_id + WHERE m.user_id = $1 + ORDER BY t.created_at ASC`, + [userRow.user_id] + ); + + await core.query('COMMIT'); + + return { + user: { + user_id: userRow.user_id, sub: userRow.sub, email: userRow.email, + name: userRow.name, default_tenant: userRow.default_tenant + }, + memberships: mres2.rows, + current_tenant: userRow.default_tenant + }; + } catch (err) { + await core.query('ROLLBACK'); + throw err; + } finally { + core.release(); + } +} diff --git a/services/plugins/package-lock.json b/services/plugins/package-lock.json index 8051c8f..42edfe4 100644 --- a/services/plugins/package-lock.json +++ b/services/plugins/package-lock.json @@ -9,6 +9,9 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@suitecoffee/db": "file:../../packages/core/db", + "@suitecoffee/redis": "file:../../packages/core/redis", + "@suitecoffee/scripts": "file:../../packages/core/scripts", "bcrypt": "^6.0.0", "chalk": "^5.6.0", "connect-redis": "^9.0.0", @@ -36,6 +39,27 @@ "nodemon": "^3.1.10" } }, + "../../packages/core/db": { + "name": "@suitecoffee/db", + "version": "1.0.0", + "peerDependencies": { + "pg": "^8.16.3" + } + }, + "../../packages/core/redis": { + "name": "@suitecoffee/redis", + "version": "1.0.0", + "peerDependencies": { + "pg": "^8.16.3" + } + }, + "../../packages/core/scripts": { + "name": "@suitecoffee/scripts", + "version": "1.0.0", + "peerDependencies": { + "pg": "^8.16.3" + } + }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", @@ -109,6 +133,18 @@ "@redis/client": "^5.8.3" } }, + "node_modules/@suitecoffee/db": { + "resolved": "../../packages/core/db", + "link": true + }, + "node_modules/@suitecoffee/redis": { + "resolved": "../../packages/core/redis", + "link": true + }, + "node_modules/@suitecoffee/scripts": { + "resolved": "../../packages/core/scripts", + "link": true + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", diff --git a/services/plugins/package.json b/services/plugins/package.json index 700f3d9..faaf193 100644 --- a/services/plugins/package.json +++ b/services/plugins/package.json @@ -15,6 +15,12 @@ "nodemon": "^3.1.10" }, "dependencies": { + + "@suitecoffee/db": "file:../../packages/core/db", + "@suitecoffee/redis": "file:../../packages/core/redis", + "@suitecoffee/scripts": "file:../../packages/core/scripts", + "@suitecoffee/middlewares": "file:../../packages/core/middlewares", + "bcrypt": "^6.0.0", "chalk": "^5.6.0", "connect-redis": "^9.0.0", @@ -39,9 +45,8 @@ }, "imports": { "#v1Router": "./src/api/v1/routes/routes.js", - "#pages": "./src/pages/pages.js", - "#db": "./src/db/poolSingleton.js" + "#pages": "./src/pages/pages.js" }, "keywords": [], "description": "" -} +} \ No newline at end of file diff --git a/services/plugins/src/db/poolSingleton.js b/services/plugins/src/db/poolSingleton.js deleted file mode 100644 index 721722f..0000000 --- a/services/plugins/src/db/poolSingleton.js +++ /dev/null @@ -1,82 +0,0 @@ -// Coneción Singleton a base de datos. - -import { Pool } from 'pg'; - -class DatabaseCore { - constructor() { - - if (DatabaseCore.instance) { - return Database.instance; - } - - const config = { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - port: process.env.DB_LOCAL_PORT ? Number(process.env.DB_LOCAL_PORT) : undefined, - ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined, - }; - - this.connection = new Pool(config); - - DatabaseCore.instance = this; - } - async query(sql, params) { - return this.connection.query(sql,params); - } - - async connect() { /* Definida solo para evitar errores */ - return this.connection.connect(); - } - async getClient() { - return this.connection.connect(); - } - - async release() { - await this.connection.end(); - } -} -class DatabaseTenants { - constructor() { - - if (DatabaseTenants.instance) { - return Database.instance; - } - - const config = { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - port: process.env.DB_LOCAL_PORT ? Number(process.env.DB_LOCAL_PORT) : undefined, - ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined, - }; - - this.connection = new Pool(config); - - DatabaseTenants.instance = this; - } - async query(sql, params) { - return this.connection.query(sql,params); - } - - async connect() { /* Definida solo para evitar errores */ - return this.connection.connect(); - } - async getClient() { - return this.connection.connect(); - } - - async release() { - await this.connection.end(); - } -} - -// const db = new Database(); -// db.query('SELECT * FROM users'); - -const poolCore = new DatabaseCore(); -const poolTenants = new DatabaseTenants(); -export default {poolCore, poolTenants}; -export { poolCore, poolTenants }; \ No newline at end of file diff --git a/services/plugins/src/index.mjs b/services/plugins/src/index.mjs index 365a882..65ccaa2 100644 --- a/services/plugins/src/index.mjs +++ b/services/plugins/src/index.mjs @@ -9,7 +9,9 @@ import 'dotenv/config'; // Variables de Entorno import express from 'express'; import expressLayouts from 'express-ejs-layouts'; -import { poolCore, poolTenants } from '#db'; // dbCore y dbTenants +import { poolCore, poolTenants, verificarConexionCore, verificarConexionTenants } from '@suitecoffee/db'; // dbCore y dbTenants desde paquete +import { redisAuthentik, verificarConexionRedisAuthentik} from '@suitecoffee/redis'; +import { checkRequiredEnvVars } from '@suitecoffee/scripts'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -23,35 +25,17 @@ const __dirname = path.dirname(__filename); // Validación de entorno mínimo (ajusta nombres si difieren) // ----------------------------------------------------------------------------- -// Función para verificar que ciertas variables de entorno estén definida -function checkRequiredEnvVars(...requiredKeys) { - const missingKeys = requiredKeys.filter((key) => !process.env[key]); // Filtramos las que NO existen en process.env - if (missingKeys.length > 0) { // Si falta alguna, mostramos una advertencia - console.warn( - `[PLUGIN] No se encontraron las siguientes variables de entorno: \n\n-> ${missingKeys.join('\n-> ')}`+ - `\n` - ); - } -} - checkRequiredEnvVars( - 'PORT', - 'CORE_DB_HOST', 'CORE_DB_PORT', 'CORE_DB_NAME', - 'TENANTS_DB_HOST', 'TENANTS_DB_PORT', 'TENANTS_DB_NAME' + 'PORT' ); + // ---------------------------------------------------------- // Variables del sistema // ---------------------------------------------------------- const PORT = process.env.PORT; -const CORE_DB_HOST = process.env.CORE_DB_HOST; -const CORE_DB_PORT = process.env.CORE_DB_PORT; -const CORE_DB_NAME = process.env.CORE_DB_NAME; -const TENANTS_DB_HOST = process.env.TENANTS_DB_HOST; -const TENANTS_DB_PORT = process.env.TENANTS_DB_PORT; -const TENANTS_DB_NAME = process.env.TENANTS_DB_NAME; // ---------------------------------------------------------- // App + Motor de vistas EJS @@ -72,38 +56,6 @@ app.use(expressLayouts); // Carga los layouts que usara el renderizado app.use(cookieParser(process.env.SESSION_SECRET)); -// ---------------------------------------------------------- -// Verificación de conexión -// ---------------------------------------------------------- - -async function verificarConexionCore() { - try { - console.log(`[PLUGINS] Comprobando accesibilidad a la db ${CORE_DB_NAME} del host ${CORE_DB_HOST} ...`); - const client = await poolCore.connect(); - const { rows } = await client.query('SELECT NOW() AS ahora'); - console.log(`\n[PLUGINS] Conexión con ${CORE_DB_NAME} OK. Hora DB:`, rows[0].ahora); - client.release(); - } catch (error) { - console.error('[PLUGINS] Error al conectar con la base de datos al iniciar:', error.message); - console.error('[PLUGINS] Revisar credenciales, accesos de red y firewall.'); - } -} -async function verificarConexionTenants() { - try { - console.log(`[PLUGINS] Comprobando accesibilidad a la db ${TENANTS_DB_NAME} del host ${TENANTS_DB_HOST} ...`); - const client = await poolTenants.connect(); - const { rows } = await client.query('SELECT NOW() AS ahora'); - console.log(`\n[PLUGINS] Conexión con ${TENANTS_DB_NAME} OK. Hora DB:`, rows[0].ahora); - client.release(); - } catch (error) { - console.error('[PLUGINS] Error al conectar con la base de datos al iniciar:', error.message); - console.error('[PLUGINS] Revisar credenciales, accesos de red y firewall.'); - } -} - - - - // ---------------------------------------------------------- @@ -123,10 +75,11 @@ app.use((req, res, next) => { // Inicio del servidor // ---------------------------------------------------------- -app.listen(PORT, () => { +app.listen(PORT, async () => { console.log(`[PLUGINS] http://localhost:${PORT}`); - verificarConexionCore(); - verificarConexionTenants(); + await verificarConexionCore(); + await verificarConexionTenants(); + await verificarConexionRedisAuthentik(); }); @@ -135,6 +88,6 @@ app.listen(PORT, () => { // Healthcheck // ----------------------------------------------------------------------------- app.get('/health', (_req, res) => { - res.status(200).json({ status: 'ok'}), - console.log(`[PLUGINS] Saludable`) + res.status(200).json({ status: 'ok'}) + // console.log(`[PLUGINS] Saludable`) }); \ No newline at end of file