Salud_UT/backend/src/server.js
2025-12-16 19:33:09 -05:00

1150 lines
33 KiB
JavaScript

// 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}`);
});