- Migrar de sqlite3 a better-sqlite3 con conexión persistente - Configurar WAL mode, busy_timeout y PRAGMAs optimizados - Implementar transacciones para operaciones bulk - Corregir error initialLoadComplete en useHospitalStore.ts - Agregar tablas usuarios y kv faltantes - Actualizar .gitignore para archivos WAL de SQLite
424 lines
18 KiB
JavaScript
424 lines
18 KiB
JavaScript
import express from 'express';
|
|
import cors from 'cors';
|
|
import { getDb, getValue, setValue, getUsuarioByDni, getUsuarioById, getAllUsuarios, createUsuario, updateUsuario, deleteUsuario, verifyPassword, hashPassword, getAllPacientes, getAllAreas, getAllCamas, getAllInternaciones, getAllEvoluciones, getAllLaboratorios, getAllAcidosBase, getAllCultivos, getAllEstudiosComplementarios, getAllInterconsultas, getAllAtb, getAllIndicaciones, getAllMovimientosIndicaciones } from './db.js';
|
|
|
|
const app = express();
|
|
app.use(cors());
|
|
app.use(express.json({ limit: '5mb' }));
|
|
|
|
const PORT = process.env.PORT || 4001;
|
|
const STORAGE_KEY = 'hospital-data-v1';
|
|
|
|
app.get('/api/state', (req, res) => {
|
|
try {
|
|
const state = {
|
|
pacientes: getAllPacientes(),
|
|
areas: getAllAreas(),
|
|
camas: getAllCamas(),
|
|
internaciones: getAllInternaciones(),
|
|
evoluciones: getAllEvoluciones(),
|
|
laboratorios: getAllLaboratorios(),
|
|
acidosBase: getAllAcidosBase(),
|
|
cultivos: getAllCultivos(),
|
|
estudiosComplementarios: getAllEstudiosComplementarios(),
|
|
interconsultas: getAllInterconsultas(),
|
|
atb: getAllAtb(),
|
|
indicaciones: getAllIndicaciones(),
|
|
movimientosIndicaciones: getAllMovimientosIndicaciones(),
|
|
vistaActual: 'dashboard',
|
|
currentInternacionId: null
|
|
};
|
|
res.json(state);
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'failed to read state' });
|
|
}
|
|
});
|
|
|
|
app.put('/api/state', (req, res) => {
|
|
try {
|
|
const state = req.body;
|
|
const db = getDb();
|
|
|
|
// Use transaction for atomicity and better performance
|
|
const result = db.transaction(() => {
|
|
db.prepare('DELETE FROM evoluciones').run();
|
|
db.prepare('DELETE FROM acidosbase').run();
|
|
db.prepare('DELETE FROM cultivos').run();
|
|
db.prepare('DELETE FROM laboratorios').run();
|
|
db.prepare('DELETE FROM internaciones').run();
|
|
db.prepare('DELETE FROM camas').run();
|
|
db.prepare('DELETE FROM areas').run();
|
|
db.prepare('DELETE FROM pacientes').run();
|
|
db.prepare('DELETE FROM estudiosComplementarios').run();
|
|
db.prepare('DELETE FROM interconsultas').run();
|
|
db.prepare('DELETE FROM atb').run();
|
|
db.prepare('DELETE FROM indicaciones').run();
|
|
db.prepare('DELETE FROM movimientos_indicaciones').run();
|
|
|
|
if (state.pacientes?.length) {
|
|
const stmt = db.prepare('INSERT INTO pacientes (id, apellido, nombre, dni, fechaNacimiento, sexo, telefono, email, direccion, obraSocial, nacionalidad, medicacionHabitual, antecedentes, alergias, grupoSanguineo, historiaClinica) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const p of state.pacientes) {
|
|
stmt.run(p.id, p.apellido, p.nombre, p.dni, p.fechaNacimiento || null, p.sexo || null, p.telefono || null, p.email || null, p.direccion || null, p.obraSocial || null, p.nacionalidad || null, p.medicacionHabitual || null, p.antecedentes || null, p.alergias || null, p.grupoSanguineo || null, p.historiaClinica || null);
|
|
}
|
|
}
|
|
|
|
if (state.areas?.length) {
|
|
const stmt = db.prepare('INSERT INTO areas (id, nombre) VALUES (?, ?)');
|
|
for (const a of state.areas) {
|
|
stmt.run(a.id, a.nombre);
|
|
}
|
|
}
|
|
|
|
if (state.camas?.length) {
|
|
const stmt = db.prepare('INSERT OR IGNORE INTO camas (id, numero, areaId, tipo, estado) VALUES (?, ?, ?, ?, ?)');
|
|
for (const c of state.camas) {
|
|
stmt.run(c.id, c.numero, c.areaId, c.tipo, c.estado);
|
|
}
|
|
}
|
|
|
|
if (state.internaciones?.length) {
|
|
const stmt = db.prepare('INSERT OR IGNORE INTO internaciones (id, pacienteId, camaId, areaId, fechaIngresoHospital, fechaIngresoClinica, fechaEgreso, diagnosticoIngreso, motivoConsulta, enfermedadActual, antecedentesEnfermedadActual, diagnosticoEgreso, medicoIngresante, motivoEgreso, activa, apache, derivacion) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const i of state.internaciones) {
|
|
stmt.run(i.id, i.pacienteId, i.camaId, i.areaId || null, i.fechaIngresoHospital || null, i.fechaIngresoClinica || null, i.fechaEgreso || null, i.diagnosticoIngreso || null, i.motivoConsulta || null, i.enfermedadActual || null, i.antecedentesEnfermedadActual || null, i.diagnosticoEgreso || null, i.medicoIngresante || null, i.motivoEgreso || null, i.activa ? 1 : 0, i.apache || null, i.derivacion || null);
|
|
}
|
|
}
|
|
|
|
if (state.evoluciones?.length) {
|
|
const stmt = db.prepare('INSERT OR IGNORE INTO evoluciones (id, internacionId, fecha, hora, medico, signosVitales, examenFisico, novedades, comentarios, pendientes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const e of state.evoluciones) {
|
|
stmt.run(e.id, e.internacionId, e.fecha, e.hora || '', e.medico || '', JSON.stringify(e.signosVitales || {}), JSON.stringify(e.examenFisico || {}), e.novedades || '', e.comentarios || '', e.pendientes || '');
|
|
}
|
|
}
|
|
|
|
if (state.laboratorios?.length) {
|
|
const stmt = db.prepare('INSERT INTO laboratorios (id, pacienteId, fecha, hora, tipo, resultados, observaciones) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
for (const l of state.laboratorios) {
|
|
stmt.run(l.id, l.pacienteId, l.fecha, l.hora || null, l.tipo, JSON.stringify(l.resultados), l.observaciones || null);
|
|
}
|
|
}
|
|
|
|
if (state.acidosBase?.length) {
|
|
const stmt = db.prepare('INSERT INTO acidosbase (id, pacienteId, fecha, hora, ph, pco2, po2, hco3, be, sato2, lactato, interpretacion, fio2) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const a of state.acidosBase) {
|
|
stmt.run(a.id, a.pacienteId, a.fecha, a.hora, a.ph, a.pco2, a.po2, a.hco3, a.be, a.sato2, a.lactato || null, a.interpretacion || null, a.fio2 || null);
|
|
}
|
|
}
|
|
|
|
if (state.cultivos?.length) {
|
|
const stmt = db.prepare('INSERT INTO cultivos (id, pacienteId, internacionId, fechaToma, protocolo, tipoMuestra, observaciones, estado, fechaResultado, germen, sensible, resistente) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const c of state.cultivos) {
|
|
stmt.run(c.id, c.pacienteId, c.internacionId, c.fechaToma, c.protocolo, c.tipoMuestra, c.observaciones, c.estado, c.fechaResultado, c.germen, c.sensible, c.resistente);
|
|
}
|
|
}
|
|
|
|
if (state.estudiosComplementarios?.length) {
|
|
const stmt = db.prepare('INSERT INTO estudiosComplementarios (id, pacienteId, internacionId, fecha, tipo, resultado) VALUES (?, ?, ?, ?, ?, ?)');
|
|
for (const e of state.estudiosComplementarios) {
|
|
stmt.run(e.id, e.pacienteId, e.internacionId || null, e.fecha, e.tipo, e.resultado);
|
|
}
|
|
}
|
|
|
|
if (state.interconsultas?.length) {
|
|
const stmt = db.prepare('INSERT INTO interconsultas (id, pacienteId, internacionId, fecha, servicioInterconsultado, motivo, respuestaInterconsulta, respuestaFecha) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const ic of state.interconsultas) {
|
|
stmt.run(ic.id, ic.pacienteId, ic.internacionId, ic.fecha, ic.servicioInterconsultado, ic.motivo || null, ic.respuestaInterconsulta || null, ic.respuestaFecha || null);
|
|
}
|
|
}
|
|
|
|
if (state.atb?.length) {
|
|
const stmt = db.prepare('INSERT INTO atb (id, pacienteId, internacionId, antibiotico, fechaInicio, fechaFinalizacion) VALUES (?, ?, ?, ?, ?, ?)');
|
|
for (const atb of state.atb) {
|
|
stmt.run(atb.id, atb.pacienteId, atb.internacionId, atb.antibiotico, atb.fechaInicio, atb.fechaFinalizacion || null);
|
|
}
|
|
}
|
|
|
|
if (state.indicaciones?.length) {
|
|
const stmt = db.prepare('INSERT INTO indicaciones (id, internacionId, tipo, droga, dosis, frecuenciaHoras, via, tipoPlan, tipoPlan2, cantidadMl, cantidadMl2, tiempoHoras, estado, medicoCrea, fechaCrea, tipoInsulina, unidadesDesayuno, unidadesAlmuerzo, unidadesNoche, indicacionNoFco) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const ind of state.indicaciones) {
|
|
stmt.run(ind.id, ind.internacionId, ind.tipo, ind.droga || null, ind.dosis || null, ind.frecuenciaHoras || null, ind.via || null, ind.tipoPlan || null, ind.tipoPlan2 || null, ind.cantidadMl || null, ind.cantidadMl2 || null, ind.tiempoHoras || null, ind.estado, ind.medicoCrea, ind.fechaCrea, ind.tipoInsulina || null, ind.unidadesDesayuno || null, ind.unidadesAlmuerzo || null, ind.unidadesNoche || null, ind.indicacionNoFco || null);
|
|
}
|
|
}
|
|
|
|
if (state.movimientosIndicaciones?.length) {
|
|
const stmt = db.prepare('INSERT INTO movimientos_indicaciones (id, indicacionId, internacionId, tipo, fecha, profesional, indicacionPrevia, indicacionNueva) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const mov of state.movimientosIndicaciones) {
|
|
stmt.run(mov.id, mov.indicacionId, mov.internacionId, mov.tipo, mov.fecha, mov.profesional, mov.indicacionPrevia || null, mov.indicacionNueva || null);
|
|
}
|
|
}
|
|
})();
|
|
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'failed to save state' });
|
|
}
|
|
});
|
|
|
|
// Partial state update (only changed parts)
|
|
app.put('/api/state/partial', (req, res) => {
|
|
try {
|
|
const updates = req.body;
|
|
if (!updates || typeof updates !== 'object') {
|
|
return res.status(400).json({ error: 'Invalid update data' });
|
|
}
|
|
|
|
const db = getDb();
|
|
|
|
// Use transaction for atomicity
|
|
const result = db.transaction(() => {
|
|
// Only update tables that were actually changed
|
|
if (updates.pacientes) {
|
|
db.prepare('DELETE FROM pacientes').run();
|
|
if (updates.pacientes.length > 0) {
|
|
const stmt = db.prepare('INSERT INTO pacientes (id, apellido, nombre, dni, fechaNacimiento, sexo, telefono, email, direccion, obraSocial, nacionalidad, medicacionHabitual, antecedentes, alergias, grupoSanguineo, historiaClinica) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const p of updates.pacientes) {
|
|
stmt.run(p.id, p.apellido, p.nombre, p.dni, p.fechaNacimiento || null, p.sexo || null, p.telefono || null, p.email || null, p.direccion || null, p.obraSocial || null, p.nacionalidad || null, p.medicacionHabitual || null, p.antecedentes || null, p.alergias || null, p.grupoSanguineo || null, p.historiaClinica || null);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updates.areas) {
|
|
db.prepare('DELETE FROM areas').run();
|
|
if (updates.areas.length > 0) {
|
|
const stmt = db.prepare('INSERT INTO areas (id, nombre) VALUES (?, ?)');
|
|
for (const a of updates.areas) {
|
|
stmt.run(a.id, a.nombre);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updates.camas) {
|
|
db.prepare('DELETE FROM camas').run();
|
|
if (updates.camas.length > 0) {
|
|
const stmt = db.prepare('INSERT INTO camas (id, numero, areaId, tipo, estado) VALUES (?, ?, ?, ?, ?)');
|
|
for (const c of updates.camas) {
|
|
stmt.run(c.id, c.numero, c.areaId, c.tipo, c.estado);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updates.internaciones) {
|
|
db.prepare('DELETE FROM internaciones').run();
|
|
if (updates.internaciones.length > 0) {
|
|
const stmt = db.prepare('INSERT OR IGNORE INTO internaciones (id, pacienteId, camaId, areaId, fechaIngresoHospital, fechaIngresoClinica, fechaEgreso, diagnosticoIngreso, motivoConsulta, enfermedadActual, antecedentesEnfermedadActual, diagnosticoEgreso, medicoIngresante, motivoEgreso, activa, apache, derivacion) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const i of updates.internaciones) {
|
|
stmt.run(i.id, i.pacienteId, i.camaId, i.areaId || null, i.fechaIngresoHospital || null, i.fechaIngresoClinica || null, i.fechaEgreso || null, i.diagnosticoIngreso || null, i.motivoConsulta || null, i.enfermedadActual || null, i.antecedentesEnfermedadActual || null, i.diagnosticoEgreso || null, i.medicoIngresante || null, i.motivoEgreso || null, i.activa ? 1 : 0, i.apache || null, i.derivacion || null);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updates.evoluciones) {
|
|
db.prepare('DELETE FROM evoluciones').run();
|
|
if (updates.evoluciones.length > 0) {
|
|
const stmt = db.prepare('INSERT OR IGNORE INTO evoluciones (id, internacionId, fecha, hora, medico, signosVitales, examenFisico, novedades, comentarios, pendientes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
for (const e of updates.evoluciones) {
|
|
stmt.run(e.id, e.internacionId, e.fecha, e.hora || '', e.medico || '', JSON.stringify(e.signosVitales || {}), JSON.stringify(e.examenFisico || {}), e.novedades || '', e.comentarios || '', e.pendientes || '');
|
|
}
|
|
}
|
|
}
|
|
})();
|
|
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`Server running on port ${PORT}`);
|
|
});
|
|
|
|
// === AUTENTICACIÓN ===
|
|
|
|
function generateUUID() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
const r = (Math.random() * 16) | 0;
|
|
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
|
|
app.post('/api/auth/login', (req, res) => {
|
|
try {
|
|
const { dni, password } = req.body;
|
|
if (!dni || !password) {
|
|
return res.status(400).json({ error: 'DNI y contraseña requeridos' });
|
|
}
|
|
|
|
const usuario = getUsuarioByDni(dni);
|
|
if (!usuario) {
|
|
return res.status(401).json({ error: 'Usuario no encontrado' });
|
|
}
|
|
|
|
const validPassword = verifyPassword(dni, password);
|
|
if (!validPassword) {
|
|
return res.status(401).json({ error: 'Contraseña incorrecta' });
|
|
}
|
|
|
|
const { passwordHash, ...userWithoutPassword } = usuario;
|
|
res.json(userWithoutPassword);
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Error en autenticación' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/auth/change-password', (req, res) => {
|
|
try {
|
|
const { dni, oldPassword, newPassword } = req.body;
|
|
if (!dni || !oldPassword || !newPassword) {
|
|
return res.status(400).json({ error: 'Todos los campos son requeridos' });
|
|
}
|
|
|
|
const validPassword = verifyPassword(dni, oldPassword);
|
|
if (!validPassword) {
|
|
return res.status(401).json({ error: 'Contraseña actual incorrecta' });
|
|
}
|
|
|
|
const newHash = hashPassword(newPassword);
|
|
updateUsuarioByDni(dni, { passwordHash: newHash });
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Error al cambiar contraseña' });
|
|
}
|
|
});
|
|
|
|
app.put('/api/auth/update-email', (req, res) => {
|
|
try {
|
|
const { dni, newEmail } = req.body;
|
|
if (!dni || !newEmail) {
|
|
return res.status(400).json({ error: 'DNI y email son requeridos' });
|
|
}
|
|
|
|
updateUsuarioByDni(dni, { email: newEmail });
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Error al actualizar email' });
|
|
}
|
|
});
|
|
|
|
// === GESTIÓN DE USUARIOS (ADMIN) ===
|
|
|
|
app.get('/api/areas', (req, res) => {
|
|
try {
|
|
const areas = getAllAreas();
|
|
res.json(areas);
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Error al obtener áreas' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/usuarios', (req, res) => {
|
|
try {
|
|
const usuarios = getAllUsuarios();
|
|
const usuariosSinPassword = usuarios.map(({ passwordHash, ...u }) => u);
|
|
res.json(usuariosSinPassword);
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Error al obtener usuarios' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/usuarios', (req, res) => {
|
|
try {
|
|
const { apellido, nombre, dni, fechaNacimiento, email, rol, matriculaProfesional, password, areaId } = req.body;
|
|
|
|
if (!apellido || !nombre || !dni || !fechaNacimiento || !email || !rol || !password) {
|
|
return res.status(400).json({ error: 'Todos los campos obligatorios deben estar presentes' });
|
|
}
|
|
|
|
const existente = getUsuarioByDni(dni);
|
|
if (existente) {
|
|
return res.status(400).json({ error: 'Ya existe un usuario con ese DNI' });
|
|
}
|
|
|
|
const passwordHash = hashPassword(password);
|
|
const usuario = {
|
|
id: generateUUID(),
|
|
apellido,
|
|
nombre,
|
|
dni,
|
|
fechaNacimiento,
|
|
email,
|
|
rol,
|
|
matriculaProfesional: matriculaProfesional || null,
|
|
passwordHash,
|
|
areaId: areaId || null,
|
|
fechaCreacion: new Date().toISOString().split('T')[0]
|
|
};
|
|
|
|
createUsuario(usuario);
|
|
res.json({ ...usuario, passwordHash: undefined });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Error al crear usuario' });
|
|
}
|
|
});
|
|
|
|
app.put('/api/usuarios/:id', (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { apellido, nombre, fechaNacimiento, email, rol, matriculaProfesional, areaId, password } = req.body;
|
|
|
|
const usuario = getUsuarioById(id);
|
|
if (!usuario) {
|
|
return res.status(404).json({ error: 'Usuario no encontrado' });
|
|
}
|
|
|
|
const datos = {};
|
|
if (apellido !== undefined) datos.apellido = apellido;
|
|
if (nombre !== undefined) datos.nombre = nombre;
|
|
if (fechaNacimiento !== undefined) datos.fechaNacimiento = fechaNacimiento;
|
|
if (email !== undefined) datos.email = email;
|
|
if (rol !== undefined) datos.rol = rol;
|
|
if (matriculaProfesional !== undefined) datos.matriculaProfesional = matriculaProfesional;
|
|
if (areaId !== undefined) datos.areaId = areaId;
|
|
if (password) {
|
|
datos.passwordHash = hashPassword(password);
|
|
}
|
|
|
|
updateUsuario(id, datos);
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Error al actualizar usuario' });
|
|
}
|
|
});
|
|
|
|
app.delete('/api/usuarios/:id', (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
deleteUsuario(id);
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Error al eliminar usuario' });
|
|
}
|
|
});
|
|
|
|
app.put('/api/camas/:id', (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { estado } = req.body;
|
|
const db = getDb();
|
|
db.prepare('UPDATE camas SET estado = ? WHERE id = ?').run(estado, id);
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error('Error updating cama:', err.message);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Helper para actualizar por DNI
|
|
function updateUsuarioByDni(dni, datos) {
|
|
const usuario = getUsuarioByDni(dni);
|
|
if (usuario) {
|
|
updateUsuario(usuario.id, datos);
|
|
}
|
|
}
|