1150 lines
33 KiB
JavaScript
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}`);
|
|
}); |