// server.js require('dotenv').config(); const express = require('express'); const cors = require('cors'); const { Pool } = require('pg'); const ExcelJS = require('exceljs'); const PDFDocument = require('pdfkit'); const fsPromises = require('fs').promises; const { crearLibroAutorizacion } = require('./plantilla_autorizacion'); const path = require('path'); const os = require('os'); const { execFile } = require('child_process'); const multer = require('multer'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const archiver = require('archiver'); // --- CONFIGURACIÓN INICIAL --- const JWT_SECRET = process.env.JWT_SECRET || 'secreto-salud-ut-2025'; const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h'; // Multer: guardamos el archivo en memoria const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024, // 50 MB }, }); // Path de LibreOffice desde .env const sofficePath = process.env.SOFFICE_PATH || 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'; // Configuración de la Base de Datos const pool = new Pool({ host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'postgres', port: Number(process.env.DB_PORT) || 5432, password: process.env.DB_PASSWORD || '1234', database: process.env.DB_NAME || 'saludut', }); const app = express(); const PORT = Number(process.env.PORT) || 3000; app.use(cors()); // --- 1. DEFINICIÓN DE MIDDLEWARES (ANTES DE USARLOS) --- // Es crucial definir los middlewares aquí para que puedan ser usados por las rutas que se definan más abajo. // Esto resuelve el error: "Cannot access 'verificarToken' before initialization". // Middleware para verificar token JWT const verificarToken = (req, res, next) => { const token = req.headers['authorization']?.replace('Bearer ', ''); if (!token) { return res.status(401).json({ error: 'Token no proporcionado' }); } try { const decoded = jwt.verify(token, JWT_SECRET); req.usuario = decoded; next(); } catch (error) { return res.status(401).json({ error: 'Token inválido o expirado' }); } }; // Middleware para verificar rol específico const verificarRol = (rolPermitido) => { return (req, res, next) => { if (req.usuario.nombre_rol !== rolPermitido) { return res.status(403).json({ error: 'No tienes permisos para esta acción' }); } next(); }; }; // Middleware para verificar si es administrador const esAdministrador = verificarRol('administrador'); // Middleware para verificar si puede generar autorizaciones const puedeGenerarAutorizaciones = (req, res, next) => { if (req.usuario.nombre_rol !== 'administrador' && req.usuario.nombre_rol !== 'administrativo_sede') { return res.status(403).json({ error: 'No tienes permisos para generar autorizaciones' }); } next(); }; // Middleware para verificar acceso por sede const verificarAccesoSede = async (req, res, next) => { // Si es administrador, tiene acceso a todo if (req.usuario.nombre_rol === 'administrador') { return next(); } // Si es administrativo, verificar que tenga acceso a la sede del paciente const { interno } = req.query; if (!interno) { return res.status(400).json({ error: 'Se requiere el parámetro interno' }); } try { const sql = ` SELECT 1 FROM usuario_sede us JOIN ingreso i ON us.codigo_establecimiento = i.codigo_establecimiento WHERE us.id_usuario = $1 AND i.interno = $2 `; const { rows } = await pool.query(sql, [req.usuario.id_usuario, interno]); if (rows.length === 0) { return res.status(403).json({ error: 'No tienes permisos para acceder a este interno' }); } next(); } catch (error) { console.error('Error verificando acceso a sede:', error); return res.status(500).json({ error: 'Error en el servidor' }); } }; // --- 2. DEFINICIÓN DE RUTAS --- // RUTA ESPECIAL PARA SUBIDA DE ARCHIVOS // ¡IMPORTANTE! Esta ruta usa 'multer' y DEBE estar definida ANTES de los middlewares `express.json()` y `express.urlencoded()`. // Esto resuelve el error: "SyntaxError: Unexpected token '-', "------WebK"... is not valid JSON". app.post( '/api/cargar-excel-pacientes', verificarToken, esAdministrador, upload.single('archivo'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No se recibió archivo Excel' }); } // 1) Guardar el Excel como pacientes.xlsx (mismo nombre que espera Python) const backendDir = __dirname; const excelPath = path.join(backendDir, 'pacientes.xlsx'); await fsPromises.writeFile(excelPath, req.file.buffer); // 2) Ejecutar pacientes.py para generar los .sql const scriptPath = path.join(backendDir, 'pacientes.py'); await new Promise((resolve, reject) => { execFile( process.env.PYTHON_PATH || 'python', // o 'python3' [scriptPath], { cwd: backendDir }, (error, stdout, stderr) => { console.log('pacientes.py stdout:\n', stdout); console.error('pacientes.py stderr:\n', stderr); if (error) return reject(error); resolve(); } ); }); // 3) Leer los .sql generados y ejecutarlos en una transacción const files = ['establecimiento.sql', 'paciente.sql', 'ingreso.sql']; console.log('Ejecutando scripts SQL generados...'); await pool.query('BEGIN'); for (const fname of files) { const fullPath = path.join(backendDir, fname); const sqlText = await fsPromises.readFile(fullPath, 'utf8'); const lines = sqlText.split('\n'); for (const line of lines) { const stmt = line.trim(); if (!stmt || stmt.startsWith('--')) continue; // saltar comentarios / líneas vacías await pool.query(stmt); } } await pool.query('COMMIT'); console.log('Transacción COMMIT OK'); // 4) Stats de pacientes (si existe la columna activo) let activos = null; let antiguos = null; try { const activosRes = await pool.query( 'SELECT COUNT(*)::int AS c FROM paciente WHERE activo = true' ); const antiguosRes = await pool.query( 'SELECT COUNT(*)::int AS c FROM paciente WHERE activo = false' ); activos = activosRes.rows[0].c; antiguos = antiguosRes.rows[0].c; } catch (e) { console.warn( 'No se pudieron obtener stats de activos/antiguos (¿columna activo existe?):', e.message ); } // 5) Respuesta al frontend return res.json({ ok: true, mensaje: 'Excel procesado y BD actualizada', activos, antiguos, }); } catch (err) { console.error('Error en /api/cargar-excel-pacientes:', err); try { await pool.query('ROLLBACK'); } catch (e) { console.error('Error haciendo rollback:', e); } return res.status(500).json({ error: 'Error procesando el Excel / ejecutando scripts SQL', }); } } ); // MIDDLEWARES GLOBALES (para el resto de las rutas que sí usan JSON) app.use( express.json({ limit: '50mb', }) ); app.use( express.urlencoded({ extended: true, limit: '50mb', }) ); // Función auxiliar para generar PDFs async function generarPdfAutorizacionYObtenerPath(numero_autorizacion) { const sql = ` SELECT a.numero_autorizacion, a.fecha_autorizacion, a.observacion, p.*, ips.nombre_ips, ips.nit, ips.direccion, ips.municipio, ips.departamento, aut.nombre AS nombre_autorizante, aut.cargo AS cargo_autorizante, aut.telefono AS tel_autorizante, e.nombre_establecimiento, e.epc_departamento FROM autorizacion a JOIN paciente p ON a.interno = p.interno JOIN ips ON a.id_ips = ips.id_ips JOIN autorizante aut ON a.numero_documento_autorizante = aut.numero_documento JOIN ingreso i ON a.interno = i.interno JOIN establecimiento e ON i.codigo_establecimiento = e.codigo_establecimiento WHERE a.numero_autorizacion = $1 LIMIT 1; `; const { rows } = await pool.query(sql, [numero_autorizacion]); if (rows.length === 0) { const err = new Error('Autorización no encontrada'); err.code = 'NOT_FOUND'; throw err; } const a = rows[0]; // 1) Crear el Excel con tu plantilla const libro = await crearLibroAutorizacion(a); // 2) Guardar XLSX y PDF en temporales const tmpDir = path.join(os.tmpdir(), 'salud_ut_autorizaciones'); await fsPromises.mkdir(tmpDir, { recursive: true }); const xlsxPath = path.join(tmpDir, `autorizacion_${numero_autorizacion}.xlsx`); const pdfPath = path.join(tmpDir, `autorizacion_${numero_autorizacion}.pdf`); await libro.xlsx.writeFile(xlsxPath); // 3) Convertir XLSX -> PDF await new Promise((resolve, reject) => { execFile( sofficePath, [ '--headless', '--convert-to', 'pdf', '--outdir', tmpDir, xlsxPath, ], (error, stdout, stderr) => { console.log('soffice stdout:', stdout); console.log('soffice stderr:', stderr); if (error) return reject(error); resolve(); } ); }); // Borramos el XLSX, dejamos el PDF try { await fsPromises.unlink(xlsxPath); } catch (e) { console.warn('No se pudo borrar XLSX temporal:', e.message); } return pdfPath; // devolvemos ruta del PDF } // RESTO DE RUTAS DE LA API app.get('/api/pacientes', verificarToken, async (req, res) => { const { numero_documento, interno, nombre } = req.query; if (!numero_documento && !interno && !nombre) { return res.status(400).json({ error: 'Debe enviar al menos uno: numero_documento, interno o nombre', }); } let sql = ` SELECT p.interno, p.tipo_documento, p.numero_documento, p.primer_apellido, p.segundo_apellido, p.primer_nombre, p.segundo_nombre, p.fecha_nacimiento, p.edad, p.sexo, i.codigo_establecimiento, e.nombre_establecimiento, i.estado, i.fecha_ingreso, i.tiempo_reclusion FROM paciente p LEFT JOIN ingreso i ON p.interno = i.interno LEFT JOIN establecimiento e ON i.codigo_establecimiento = e.codigo_establecimiento WHERE 1 = 1 `; const params = []; if (numero_documento) { params.push(`%${numero_documento}%`); sql += ` AND p.numero_documento ILIKE $${params.length}`; } if (interno) { params.push(`%${interno}%`); sql += ` AND p.interno ILIKE $${params.length}`; } if (nombre) { params.push(`%${nombre}%`); sql += ` AND ( (p.primer_nombre || ' ' || COALESCE(p.segundo_nombre, '') || ' ' || p.primer_apellido || ' ' || COALESCE(p.segundo_apellido, '') ) ILIKE $${params.length} ) `; } sql += ' ORDER BY p.interno LIMIT 50;'; try { const { rows } = await pool.query(sql, params); res.json(rows); } catch (err) { console.error('Error en consulta /api/pacientes:', err.message); res.status(500).json({ error: 'Error consultando la base de datos' }); } }); /** * GET /api/ips-por-interno?interno=1007362 * Busca el departamento del establecimiento del interno * y devuelve las IPS de ese departamento (o Bogotá). */ app.get('/api/ips-por-interno', verificarToken, async (req, res) => { const { interno } = req.query; if (!interno) { return res.status(400).json({ error: 'Falta parámetro interno' }); } try { // 1) Obtener el departamento y municipio del establecimiento donde está el interno const qDepto = ` SELECT e.epc_departamento, e.epc_ciudad FROM ingreso i JOIN establecimiento e ON i.codigo_establecimiento = e.codigo_establecimiento WHERE i.interno = $1 LIMIT 1; `; const deptoRes = await pool.query(qDepto, [interno]); if (deptoRes.rows.length === 0) { return res.json([]); // interno sin ingreso o sin establecimiento } const departamento = (deptoRes.rows[0].epc_departamento || '').trim(); const municipioRaw = (deptoRes.rows[0].epc_ciudad || '').trim(); const municipioUpper = municipioRaw.toUpperCase(); // Detectar Bogotá aunque venga como "BOGOTA D.C.", "BOGOTA DISTRITO CAPITAL", etc. const esBogota = municipioUpper.includes('BOGOTA') || departamento.toUpperCase().includes('BOGOTA'); let qIps; let params; if (esBogota) { // Para Bogotá buscamos por "BOGOTA" en municipio o departamento qIps = ` SELECT id_ips, nombre_ips, direccion, telefono, departamento, municipio FROM ips WHERE municipio ILIKE $1 OR departamento ILIKE $1 ORDER BY nombre_ips; `; params = ['%BOGOTA%']; } else { // Resto del país: por departamento qIps = ` SELECT id_ips, nombre_ips, direccion, telefono, departamento, municipio FROM ips WHERE departamento ILIKE $1 ORDER BY nombre_ips; `; params = [`%${departamento}%`]; } // (Opcional) log para depurar si algo raro pasa console.log('[IPS-por-interno] interno', interno, 'depto:', departamento, 'muni:', municipioRaw, 'esBogota?', esBogota); const { rows } = await pool.query(qIps, params); res.json(rows); } catch (err) { console.error('Error en /api/ips-por-interno:', err.message); res.status(500).json({ error: 'Error consultando IPS' }); } }); /** * GET /api/autorizantes * Lista las personas que pueden autorizar (activos). */ app.get('/api/autorizantes', async (_req, res) => { try { const sql = ` SELECT numero_documento, tipo_documento, nombre, telefono, cargo, activo FROM autorizante WHERE activo = true ORDER BY nombre; `; const { rows } = await pool.query(sql); res.json(rows); } catch (err) { console.error('Error en /api/autorizantes:', err.message); res.status(500).json({ error: 'Error consultando autorizantes' }); } }); /** * POST /api/autorizaciones * Body JSON: * { * "interno": "1007362", * "id_ips": 15, * "numero_documento_autorizante": 12345678, * "observacion": "Consulta oftalmología", * "fecha_autorizacion": "2025-11-25" // opcional * } */ app.post('/api/autorizaciones', verificarToken, puedeGenerarAutorizaciones, async (req, res) => { const { interno, id_ips, numero_documento_autorizante, observacion, fecha_autorizacion, } = req.body; if (!interno || !id_ips || !numero_documento_autorizante) { return res.status(400).json({ error: 'interno, id_ips y numero_documento_autorizante son obligatorios', }); } // ... aquí va tu lógica de permisos por rol / sede ... try { const sql = ` INSERT INTO autorizacion (interno, id_ips, numero_documento_autorizante, fecha_autorizacion, observacion) VALUES ($1, $2, $3, COALESCE($4::date, current_date), $5) RETURNING numero_autorizacion, fecha_autorizacion; `; const params = [ interno, id_ips, numero_documento_autorizante, fecha_autorizacion || null, observacion || null, ]; const { rows } = await pool.query(sql, params); res.status(201).json(rows[0]); } catch (err) { console.error('Error en /api/autorizaciones:', err.message); res.status(500).json({ error: 'Error guardando autorización' }); } }); /** * GET /api/autorizaciones?interno=1007362 * Devuelve todas las autorizaciones del interno. */ app.get('/api/autorizaciones', async (req, res) => { const { interno } = req.query; if (!interno) { return res.status(400).json({ error: 'Falta parámetro interno' }); } try { const sql = ` SELECT a.numero_autorizacion, a.fecha_autorizacion, a.observacion, ips.nombre_ips, ips.municipio, ips.departamento, aut.nombre AS nombre_autorizante FROM autorizacion a JOIN ips ON a.id_ips = ips.id_ips JOIN autorizante aut ON a.numero_documento_autorizante = aut.numero_documento WHERE a.interno = $1 ORDER BY a.fecha_autorizacion DESC, a.numero_autorizacion DESC; `; const { rows } = await pool.query(sql, [interno]); res.json(rows); } catch (err) { console.error('Error en /api/autorizaciones:', err.message); res.status(500).json({ error: 'Error consultando autorizaciones' }); } }); // Endpoint para generar el Excel de autorizaciones app.get('/api/generar-excel-autorizaciones', async (req, res) => { try { // Crear el workbook y la hoja de trabajo const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet('Autorizaciones'); // Agregar encabezados worksheet.columns = [ { header: 'Número de Autorización', key: 'numero_autorizacion', width: 20 }, { header: 'Interno', key: 'interno', width: 15 }, { header: 'Nombre del paciente', key: 'nombre_paciente', width: 30 }, { header: 'IPS', key: 'nombre_ips', width: 30 }, { header: 'Fecha de autorización', key: 'fecha_autorizacion', width: 20 }, { header: 'Observación', key: 'observacion', width: 50 }, { header: 'Autoriza', key: 'autoriza', width: 20 }, ]; // Obtener autorizaciones de la base de datos const sql = ` SELECT a.numero_autorizacion, p.interno, p.primer_nombre || ' ' || p.segundo_nombre || ' ' || p.primer_apellido || ' ' || p.segundo_apellido AS nombre_paciente, i.nombre_ips, a.fecha_autorizacion, a.observacion, aut.nombre AS autoriza FROM autorizacion a JOIN paciente p ON a.interno = p.interno JOIN ips i ON a.id_ips = i.id_ips JOIN autorizante aut ON a.numero_documento_autorizante = aut.numero_documento `; const { rows } = await pool.query(sql); // Agregar las filas a la hoja de trabajo rows.forEach(row => { worksheet.addRow({ numero_autorizacion: row.numero_autorizacion, interno: row.interno, nombre_paciente: row.nombre_paciente, nombre_ips: row.nombre_ips, fecha_autorizacion: row.fecha_autorizacion, observacion: row.observacion, autoriza: row.autoriza, }); }); // Configurar la respuesta para que sea descargada como archivo Excel res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', 'attachment; filename=autorizaciones.xlsx'); // Escribir el archivo en la respuesta await workbook.xlsx.write(res); res.end(); } catch (err) { console.error('Error al generar el Excel:', err.message); res.status(500).json({ error: 'Error al generar el archivo Excel' }); } }); /** * GET /api/generar-pdf-autorizacion?numero_autorizacion=2025-000123 */ app.get('/api/generar-pdf-autorizacion', verificarToken, esAdministrador, async (req, res) => { const { numero_autorizacion } = req.query; if (!numero_autorizacion) { return res .status(400) .json({ error: 'Falta parámetro numero_autorizacion' }); } try { const pdfPath = await generarPdfAutorizacionYObtenerPath(numero_autorizacion); const pdfBuf = await fsPromises.readFile(pdfPath); res.setHeader('Content-Type', 'application/pdf'); res.setHeader( 'Content-Disposition', `attachment; filename="autorizacion_${numero_autorizacion}.pdf"` ); res.end(pdfBuf); // Limpieza try { await fsPromises.unlink(pdfPath); } catch (e) { console.warn('No se pudo borrar PDF temporal:', e.message); } } catch (err) { console.error('Error en /api/generar-pdf-autorizacion:', err); if (err.code === 'NOT_FOUND') { return res.status(404).json({ error: 'Autorización no encontrada' }); } res.status(500).json({ error: 'Error generando el PDF' }); } }); // =========================== // ENDPOINTS DE AUTENTICACIÓN // =========================== /** * POST /api/auth/login * Body: { username, password } */ app.post('/api/auth/login', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Usuario y contraseña son requeridos' }); } try { const sql = ` SELECT u.*, r.nombre_rol FROM usuario u JOIN rol r ON u.id_rol = r.id_rol WHERE u.username = $1 AND u.activo = true `; const { rows } = await pool.query(sql, [username]); if (rows.length === 0) { return res.status(401).json({ error: 'Credenciales inválidas' }); } const usuario = rows[0]; const passwordValida = await bcrypt.compare(password, usuario.password_hash); if (!passwordValida) { return res.status(401).json({ error: 'Credenciales inválidas' }); } // Actualizar último login await pool.query( 'UPDATE usuario SET ultimo_login = current_timestamp WHERE id_usuario = $1', [usuario.id_usuario] ); // Obtener sedes si es administrativo de sede let sedes = []; if (usuario.nombre_rol === 'administrativo_sede') { const sedesQuery = ` SELECT us.codigo_establecimiento, e.nombre_establecimiento FROM usuario_sede us JOIN establecimiento e ON us.codigo_establecimiento = e.codigo_establecimiento WHERE us.id_usuario = $1 `; const sedesResult = await pool.query(sedesQuery, [usuario.id_usuario]); sedes = sedesResult.rows; } const token = jwt.sign( { id_usuario: usuario.id_usuario, username: usuario.username, nombre_completo: usuario.nombre_completo, nombre_rol: usuario.nombre_rol, id_rol: usuario.id_rol }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } ); res.json({ token, usuario: { id_usuario: usuario.id_usuario, username: usuario.username, nombre_completo: usuario.nombre_completo, nombre_rol: usuario.nombre_rol, sedes } }); } catch (error) { console.error('Error en login:', error); res.status(500).json({ error: 'Error en el servidor' }); } }); /** * POST /api/auth/register * Body: { username, email, password, nombre_completo, id_rol, sedes? } * Solo ADMIN */ app.post('/api/auth/register', verificarToken, esAdministrador, async (req, res) => { const { username, email, password, nombre_completo, id_rol, sedes } = req.body; console.log('Solicitud de registro:', req.body); if (!username || !email || !password || !nombre_completo || !id_rol) { return res.status(400).json({ error: 'Todos los campos son requeridos' }); } try { // Validar contraseña con la función de Postgres const sqlValidar = 'SELECT validar_contrasena($1) as valida'; const { rows } = await pool.query(sqlValidar, [password]); if (!rows[0].valida) { return res.status(400).json({ error: 'La contraseña debe tener al menos 8 caracteres y contener letras y números' }); } // Verificar duplicados const checkSql = 'SELECT id_usuario FROM usuario WHERE username = $1 OR email = $2'; const checkResult = await pool.query(checkSql, [username, email]); if (checkResult.rows.length > 0) { return res.status(400).json({ error: 'El usuario o email ya están registrados' }); } // Encriptar contraseña const passwordHash = await bcrypt.hash(password, 10); // Insertar usuario const insertSql = ` INSERT INTO usuario (username, email, password_hash, nombre_completo, id_rol) VALUES ($1, $2, $3, $4, $5) RETURNING id_usuario, username, email, nombre_completo, id_rol, activo, fecha_creacion, ultimo_login `; const { rows: newUsuario } = await pool.query(insertSql, [ username, email, passwordHash, nombre_completo, id_rol ]); const usuarioCreado = newUsuario[0]; // Si es administrativo_sede, asignar sedes if (id_rol == 2 && Array.isArray(sedes) && sedes.length > 0) { for (const sede of sedes) { await pool.query( 'INSERT INTO usuario_sede (id_usuario, codigo_establecimiento) VALUES ($1, $2)', [usuarioCreado.id_usuario, sede] ); } } res.status(201).json({ mensaje: 'Usuario creado exitosamente', usuario: usuarioCreado }); } catch (error) { console.error('Error en /api/auth/register:', error); res.status(500).json({ error: 'Error en el servidor' }); } }); /** * GET /api/auth/verify */ app.get('/api/auth/verify', verificarToken, async (req, res) => { try { const sql = ` SELECT u.id_usuario, u.username, u.nombre_completo, r.nombre_rol FROM usuario u JOIN rol r ON u.id_rol = r.id_rol WHERE u.id_usuario = $1 AND u.activo = true `; const { rows } = await pool.query(sql, [req.usuario.id_usuario]); if (rows.length === 0) { return res.status(401).json({ error: 'Usuario no encontrado' }); } let sedes = []; if (rows[0].nombre_rol === 'administrativo_sede') { const sedesQuery = ` SELECT us.codigo_establecimiento, e.nombre_establecimiento FROM usuario_sede us JOIN establecimiento e ON us.codigo_establecimiento = e.codigo_establecimiento WHERE us.id_usuario = $1 `; const sedesResult = await pool.query(sedesQuery, [req.usuario.id_usuario]); sedes = sedesResult.rows; } res.json({ usuario: { ...rows[0], sedes } }); } catch (error) { console.error('Error en verify:', error); res.status(500).json({ error: 'Error en el servidor' }); } }); /** * GET /api/usuarios * Solo administrador */ app.get('/api/usuarios', verificarToken, esAdministrador, async (_req, res) => { try { const sql = ` SELECT u.id_usuario, u.username, u.email, u.nombre_completo, u.activo, u.fecha_creacion, u.ultimo_login, r.nombre_rol FROM usuario u JOIN rol r ON u.id_rol = r.id_rol ORDER BY u.fecha_creacion ASC `; const { rows } = await pool.query(sql); res.json(rows); } catch (error) { console.error('Error listando usuarios:', error); res.status(500).json({ error: 'Error en el servidor' }); } }); /** * PATCH /api/usuarios/:id/estado * { activo: boolean } * Solo administrador */ app.patch('/api/usuarios/:id/estado', verificarToken, esAdministrador, async (req, res) => { const { id } = req.params; const { activo } = req.body; if (typeof activo !== 'boolean') { return res.status(400).json({ error: 'El campo "activo" debe ser booleano' }); } try { const sql = ` UPDATE usuario SET activo = $1 WHERE id_usuario = $2 RETURNING id_usuario, username, email, nombre_completo, activo `; const { rows } = await pool.query(sql, [activo, id]); if (rows.length === 0) { return res.status(404).json({ error: 'Usuario no encontrado' }); } res.json(rows[0]); } catch (error) { console.error('Error cambiando estado de usuario:', error); res.status(500).json({ error: 'Error en el servidor' }); } }); /** * GET /api/roles * Lista los roles disponibles (solo administrador) */ app.get('/api/roles', verificarToken, esAdministrador, async (req, res) => { try { const sql = ` SELECT id_rol, nombre_rol, descripcion FROM rol ORDER BY id_rol `; const { rows } = await pool.query(sql); res.json(rows); } catch (error) { console.error('Error listando roles:', error); res.status(500).json({ error: 'Error en el servidor' }); } }); /** * GET /api/autorizaciones-por-fecha * Query params: fecha_inicio, fecha_fin * Solo para administradores */ app.get('/api/autorizaciones-por-fecha', verificarToken, esAdministrador, async (req, res) => { const { fecha_inicio, fecha_fin } = req.query; if (!fecha_inicio || !fecha_fin) { return res.status(400).json({ error: 'fecha_inicio y fecha_fin son requeridos' }); } try { const sql = ` SELECT a.numero_autorizacion, a.fecha_autorizacion, a.observacion, p.interno, p.primer_nombre || ' ' || COALESCE(p.segundo_nombre, '') || ' ' || p.primer_apellido || ' ' || COALESCE(p.segundo_apellido, '') AS nombre_paciente, ips.nombre_ips, ips.municipio, ips.departamento, aut.nombre AS nombre_autorizante, e.nombre_establecimiento FROM autorizacion a JOIN paciente p ON a.interno = p.interno JOIN ips ON a.id_ips = ips.id_ips JOIN autorizante aut ON a.numero_documento_autorizante = aut.numero_documento JOIN ingreso i ON a.interno = i.interno JOIN establecimiento e ON i.codigo_establecimiento = e.codigo_establecimiento WHERE a.fecha_autorizacion BETWEEN $1 AND $2 ORDER BY a.fecha_autorizacion DESC, a.numero_autorizacion DESC `; const { rows } = await pool.query(sql, [fecha_inicio, fecha_fin]); res.json(rows); } catch (error) { console.error('Error en autorizaciones por fecha:', error); res.status(500).json({ error: 'Error en el servidor' }); } }); /** * GET /api/autorizaciones-por-fecha/zip * Query params: fecha_inicio, fecha_fin * Devuelve un ZIP con todos los PDFs de las autorizaciones de ese rango * Solo para administradores */ app.get('/api/autorizaciones-por-fecha/zip', verificarToken, esAdministrador, async (req, res) => { const { fecha_inicio, fecha_fin } = req.query; if (!fecha_inicio || !fecha_fin) { return res.status(400).json({ error: 'fecha_inicio y fecha_fin son requeridos' }); } try { // 1) Conseguir los números de autorización del rango const sqlNumeros = ` SELECT a.numero_autorizacion FROM autorizacion a WHERE a.fecha_autorizacion BETWEEN $1 AND $2 ORDER BY a.fecha_autorizacion DESC, a.numero_autorizacion DESC `; const { rows } = await pool.query(sqlNumeros, [fecha_inicio, fecha_fin]); if (rows.length === 0) { return res.status(404).json({ error: 'No hay autorizaciones en ese rango de fechas.' }); } // 2) Preparar carpeta temporal const tmpDir = path.join(os.tmpdir(), 'salud_ut_autorizaciones_zip'); await fsPromises.mkdir(tmpDir, { recursive: true }); // 3) Preparar respuesta como ZIP en streaming const nombreZip = `autorizaciones_${fecha_inicio}_${fecha_fin}.zip`; res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Disposition', `attachment; filename="${nombreZip}"`); const archive = archiver('zip', { zlib: { level: 9 } }); archive.on('error', (err) => { console.error('Error creando ZIP:', err); if (!res.headersSent) { res.status(500).end('Error creando el ZIP'); } else { res.end(); } }); archive.pipe(res); // 4) SQL para obtener los datos completos de UNA autorización const sqlDetalle = ` SELECT a.numero_autorizacion, a.fecha_autorizacion, a.observacion, p.*, ips.nombre_ips, ips.nit, ips.direccion, ips.municipio, ips.departamento, aut.nombre AS nombre_autorizante, aut.cargo AS cargo_autorizante, aut.telefono AS tel_autorizante, e.nombre_establecimiento, e.epc_departamento FROM autorizacion a JOIN paciente p ON a.interno = p.interno JOIN ips ON a.id_ips = ips.id_ips JOIN autorizante aut ON a.numero_documento_autorizante = aut.numero_documento JOIN ingreso i ON a.interno = i.interno JOIN establecimiento e ON i.codigo_establecimiento = e.codigo_establecimiento WHERE a.numero_autorizacion = $1 LIMIT 1; `; // 5) Para cada autorización: crear XLSX, convertir a PDF y meterlo al ZIP for (const row of rows) { const numero = row.numero_autorizacion; const det = await pool.query(sqlDetalle, [numero]); if (det.rows.length === 0) { continue; } const datosAut = det.rows[0]; // Crear Excel con la plantilla que ya tienes const libro = await crearLibroAutorizacion(datosAut); const xlsxPath = path.join(tmpDir, `aut_${numero}.xlsx`); const pdfPath = path.join(tmpDir, `aut_${numero}.pdf`); await libro.xlsx.writeFile(xlsxPath); // Convertir a PDF con LibreOffice await new Promise((resolve, reject) => { execFile( sofficePath, [ '--headless', '--convert-to', 'pdf', '--outdir', tmpDir, xlsxPath, ], (error, stdout, stderr) => { console.log('soffice stdout:', stdout); console.log('soffice stderr:', stderr); if (error) { return reject(error); } resolve(); } ); }); // Añadir PDF al ZIP archive.file(pdfPath, { name: `autorizacion_${numero}.pdf` }); // Borrar el XLSX (el PDF lo podrías borrar luego si quieres) try { await fsPromises.unlink(xlsxPath); } catch (e) { console.warn('No se pudo borrar XLSX temporal:', e.message); } } // 6) Cerrar ZIP (esto dispara la descarga) archive.finalize(); // (Opcional) podrías limpiar la carpeta tmp en un job aparte } catch (error) { console.error('Error en /api/autorizaciones-por-fecha/zip:', error); if (!res.headersSent) { res.status(500).json({ error: 'Error generando el ZIP de autorizaciones' }); } } }); // --- 3. INICIO DEL SERVIDOR --- app.listen(PORT, () => { console.log(`API escuchando en http://localhost:${PORT}`); });