// app/src/index.js import chalk from 'chalk'; // Colores! import favicon from 'serve-favicon'; // Favicon import express from 'express'; import expressLayouts from 'express-ejs-layouts'; import cors from 'cors'; import { Pool } from 'pg'; // Rutas import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Variables de Entorno import dotenv from 'dotenv'; // Cargar .env según entorno if (process.env.NODE_ENV === 'development') { dotenv.config({ path: path.resolve(__dirname, '../.env.development') }); } else if (process.env.NODE_ENV === 'test') { dotenv.config({ path: path.resolve(__dirname, '../.env.test') }); } else if (process.env.NODE_ENV === 'production') { dotenv.config({ path: path.resolve(__dirname, '../.env.production') }); } else { dotenv.config(); // .env por defecto } // ---------------------------------------------------------- // App // ---------------------------------------------------------- const app = express(); app.set('trust proxy', true); app.use(cors()); app.use(express.json()); app.use(express.json({ limit: '1mb' })); app.use(express.static(path.join(__dirname, 'pages'))); // ---------------------------------------------------------- // Motor de vistas EJS // ---------------------------------------------------------- app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); app.use(expressLayouts); app.set("layout", "layouts/main"); // Archivos estáticos app.use(express.static(path.join(__dirname, "public"))); app.use('/favicon', express.static(path.join(__dirname, 'public', 'favicon'), { maxAge: '1y' })); app.use(favicon(path.join(__dirname, 'public', 'favicon', 'favicon.ico'), { maxAge: '1y' })); const url = v => !v ? "" : (v.startsWith("http") ? v : `/img/productos/${v}`); // ---------------------------------------------------------- // Configuración de conexión PostgreSQL // ---------------------------------------------------------- const dbConfig = { 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, }; const pool = new Pool(dbConfig); // ---------------------------------------------------------- // 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', 'vw_compras' ]; const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/; // Identificadores SQL -> comillas dobles y escape correcto const q = (s) => `"${String(s).replace(/"/g, '""')}"`; function ensureTable(name) { const t = String(name || '').toLowerCase(); if (!ALLOWED_TABLES.includes(t)) throw new Error('Tabla no permitida'); return t; } async function getClient() { const client = await pool.connect(); return client; } // ---------------------------------------------------------- // 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(); }); // ---------------------------------------------------------- // Rutas de UI // ---------------------------------------------------------- app.get("/", (req, res) => { res.locals.pageTitle = "Dashboard"; res.locals.pageId = "home"; // para el sidebar contextual res.render("dashboard"); }); app.get("/dashboard", (req, res) => { res.locals.pageTitle = "Dashboard"; res.locals.pageId = "dashboard"; // <- importante res.render("dashboard"); }); // app.get('/', (req, res) => { // res.sendFile(path.join(__dirname, 'pages', 'dashboard.html')); // }); app.get("/comandas", (req, res) => { res.locals.pageTitle = "Comandas"; res.locals.pageId = "comandas"; // <- importante para el sidebar contextual res.render("comandas"); }); // app.get('/comandas', (req, res) => { // res.sendFile(path.join(__dirname, 'pages', 'comandas.html')); // }); app.get("/estadoComandas", (req, res) => { res.locals.pageTitle = "Estado de Comandas"; res.locals.pageId = "estadoComandas"; res.render("estadoComandas"); }); // app.get('/estadoComandas', (req, res) => { // res.sendFile(path.join(__dirname, 'pages', 'estadoComandas.html')); // }); app.get("/productos", (req, res) => { res.locals.pageTitle = "Productos"; res.locals.pageId = "productos"; res.render("productos"); }); app.get('/usuarios', (req, res) => { res.locals.pageTitle = 'Usuarios'; res.locals.pageId = 'usuarios'; res.render('usuarios'); }); app.get('/reportes', (req, res) => { res.locals.pageTitle = 'Reportes'; res.locals.pageId = 'reportes'; res.render('reportes'); }); app.get('/compras', (req, res) => { res.locals.pageTitle = 'Compras'; res.locals.pageId = 'compras'; res.render('compras'); }); // ---------------------------------------------------------- // 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 pool.query( `SELECT * FROM public.f_comandas_resumen($1, $2)`, [estado, limit] ); res.json(rows); } catch (e) { next(e); } }); // app.get('/api/comandas', async (req, res, next) => { // try { // const estado = (req.query.estado || '').trim(); // const limit = Math.min(parseInt(req.query.limit || '200', 10), 1000); // const params = []; // let where = ''; // if (estado) { params.push(estado); where = `WHERE c.estado = $${params.length}`; } // params.push(limit); // const sql = ` // 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) AS items, // COALESCE(i.total, 0) AS total // 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 // ${where} // ORDER BY c.id_comanda DESC // LIMIT $${params.length} // `; // const client = await pool.connect(); // try { // const { rows } = await client.query(sql, params); // res.json(rows); // } finally { client.release(); } // } 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) => pool.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) ); // app.get('/api/comandas/:id/detalle', async (req, res, next) => { // try { // const id = parseInt(req.params.id, 10); // if (!Number.isInteger(id) || id <= 0) { // return res.status(400).json({ error: 'id inválido' }); // } // const sql = ` // 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 // `; // const { rows } = await pool.query(sql, [id]); // res.json(rows); // } catch (e) { next(e); } // }); // app.get('/api/comandas/:id/detalle', async (req, res, next) => { // try { // const id = parseInt(req.params.id, 10); // if (!id) return res.status(400).json({ error: 'id inválido' }); // const sql = ` // SELECT 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 // WHERE d.id_comanda = $1 // ORDER BY d.id_det_comanda // `; // const { rows } = await pool.query(sql, [id]); // res.json(rows); // } catch (e) { next(e); } // }); // 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 pool.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 pool.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); } }); // // Cambiar estado (abrir/cerrar) // app.post('/api/comandas/:id/estado', async (req, res, next) => { // try { // const id = parseInt(req.params.id, 10); // let { estado } = req.body || {}; // if (!id) return res.status(400).json({ error: 'id inválido' }); // const allowed = new Set(['abierta','cerrada','pagada','anulada']); // if (!allowed.has(estado)) return res.status(400).json({ error: 'estado inválido' }); // const { rows } = await pool.query( // `UPDATE comandas SET estado = $2 WHERE id_comanda = $1 RETURNING *`, // [id, estado] // ); // if (!rows.length) return res.status(404).json({ error: 'comanda no encontrada' }); // res.json({ updated: rows[0] }); // } catch (e) { next(e); } // }); // GET producto + receta app.get('/api/rpc/get_producto/:id', async (req, res) => { const id = Number(req.params.id); const { rows } = await pool.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 pool.query(q, params); res.json(rows[0] || {}); } catch(e) { console.error(e); res.status(500).json({ error: 'save_producto failed' }); } }); // app.post('/api/rpc/save_producto', async (req, res) => { // const { id_producto=null, nombre, img_producto=null, precio=0, activo=true, id_categoria=null, receta=[] } = req.body || {}; // const q = 'SELECT * FROM public.save_producto($1,$2,$3,$4,$5,$6,$7::jsonb)'; // const params = [id_producto, nombre, img_producto, precio, activo, id_categoria, JSON.stringify(receta||[])]; // const { rows } = await pool.query(q, params); // res.json(rows[0] || {}); // }); // GET MP + proveedores app.get('/api/rpc/get_materia/:id', async (req, res) => { const id = Number(req.params.id); try { const { rows } = await pool.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 pool.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 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" } 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 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 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 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 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 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 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 pool.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 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' } 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 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 }); } }); // app.post('/api/rpc/report_asistencia', async (req,res)=>{ // try{ // const {desde, hasta} = req.body||{}; // const sql = 'SELECT * FROM public.report_asistencia($1::date,$2::date)'; // const {rows} = await pool.query(sql,[desde, hasta]); // res.json(rows); // } catch (e) { // console.error(e); // res.status(500).json({ error: 'report_tickets failed' + e }); // } // }); // app.post('/api/rpc/report_tickets', async (req, res) => { // try { // const { year } = req.body || {}; // const sql = 'SELECT public.report_tickets_year($1::int) AS data'; // const { rows } = await pool.query(sql, [year]); // res.json(rows[0]?.data || {}); // } catch (e) { // console.error(e); // res.status(500).json({ error: 'report_tickets failed' + e }); // } // }); // 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 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 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 pool.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 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 } app.post('/api/rpc/report_gastos', 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 }); } }); // (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 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 }); } }); // ---------------------------------------------------------- // Verificación de conexión // ---------------------------------------------------------- async function verificarConexion() { try { const client = await pool.connect(); const res = await client.query('SELECT NOW() AS hora'); console.log(`\nConexión con la base de datos ${chalk.green(process.env.DB_NAME)} fue exitosa.`); console.log('Fecha y hora actual de la base de datos:', res.rows[0].hora); client.release(); } catch (error) { console.error('Error al conectar con la base de datos al iniciar:', error.message); console.error('Revisar credenciales y accesos de red.'); } } // ---------------------------------------------------------- // Inicio del servidor // ---------------------------------------------------------- const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; app.listen(PORT, () => { console.log(`Servidor de aplicación escuchando en ${chalk.yellow(`http://localhost:${PORT}`)}`); console.log(chalk.grey(`Comprobando accesibilidad a la db ${chalk.white(process.env.DB_NAME)} del host ${chalk.white(process.env.DB_HOST)} ...`)); verificarConexion(); }); // Healthcheck app.get('/health', async (_req, res) => { res.status(200).json({ status: 'ok' }); });