import express from 'express'; import cors from 'cors'; import { openDb, 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', async (req, res) => { try { const state = { pacientes: await getAllPacientes(), areas: await getAllAreas(), camas: await getAllCamas(), internaciones: (await getAllInternaciones()).map(i => ({ ...i, activa: !!i.activa })), evoluciones: (await getAllEvoluciones()).map(e => ({ ...e, signosVitales: typeof e.signosVitales === 'string' ? JSON.parse(e.signosVitales) : e.signosVitales, examenFisico: typeof e.examenFisico === 'string' ? JSON.parse(e.examenFisico) : e.examenFisico })), laboratorios: (await getAllLaboratorios()).map(l => ({ ...l, resultados: typeof l.resultados === 'string' ? JSON.parse(l.resultados) : l.resultados })), acidosBase: await getAllAcidosBase(), cultivos: await getAllCultivos(), estudiosComplementarios: await getAllEstudiosComplementarios(), interconsultas: (await getAllInterconsultas()).map(ic => ({ ...ic, realizada: !!ic.realizada })), atb: await getAllAtb(), indicaciones: await getAllIndicaciones(), movimientosIndicaciones: await 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', async (req, res) => { try { const state = req.body; const db = await openDb(); await db.run('DELETE FROM evoluciones'); await db.run('DELETE FROM acidosbase'); await db.run('DELETE FROM cultivos'); await db.run('DELETE FROM laboratorios'); await db.run('DELETE FROM internaciones'); await db.run('DELETE FROM camas'); await db.run('DELETE FROM areas'); await db.run('DELETE FROM pacientes'); await db.run('DELETE FROM estudiosComplementarios'); await db.run('DELETE FROM interconsultas'); await db.run('DELETE FROM atb'); await db.run('DELETE FROM indicaciones'); await db.run('DELETE FROM movimientos_indicaciones'); if (state.pacientes?.length) { for (const p of state.pacientes) { await db.run( 'INSERT INTO pacientes (id, apellido, nombre, dni, fechaNacimiento, sexo, telefono, email, direccion, obraSocial, nacionalidad, medicacionHabitual, antecedentes, alergias, grupoSanguineo, historiaClinica) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [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) { for (const a of state.areas) { await db.run('INSERT INTO areas (id, nombre) VALUES (?, ?)', [a.id, a.nombre]); } } if (state.camas?.length) { for (const c of state.camas) { await db.run('INSERT INTO camas (id, numero, areaId, tipo, estado) VALUES (?, ?, ?, ?, ?)', [c.id, c.numero, c.areaId, c.tipo, c.estado]); } } if (state.internaciones?.length) { for (const i of state.internaciones) { await db.run( 'INSERT OR IGNORE INTO internaciones (id, pacienteId, camaId, areaId, fechaIngresoHospital, fechaIngresoClinica, fechaEgreso, diagnosticoIngreso, motivoConsulta, enfermedadActual, antecedentesEnfermedadActual, diagnosticoEgreso, medicoIngresante, motivoEgreso, activa, apache, derivacion) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [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) { for (const e of state.evoluciones) { await db.run( 'INSERT INTO evoluciones (id, internacionId, fecha, hora, medico, signosVitales, examenFisico, novedades, comentarios, pendientes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [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) { for (const l of state.laboratorios) { await db.run( 'INSERT INTO laboratorios (id, pacienteId, fecha, hora, tipo, resultados, observaciones) VALUES (?, ?, ?, ?, ?, ?, ?)', [l.id, l.pacienteId, l.fecha, l.hora || null, l.tipo, JSON.stringify(l.resultados), l.observaciones || null] ); } } if (state.acidosBase?.length) { for (const a of state.acidosBase) { await db.run( 'INSERT INTO acidosbase (id, pacienteId, fecha, hora, ph, pco2, po2, hco3, be, sato2, lactato, interpretacion, fio2) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [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) { for (const c of state.cultivos) { await db.run( 'INSERT INTO cultivos (id, pacienteId, internacionId, fechaToma, protocolo, tipoMuestra, observaciones, estado, fechaResultado, germen, sensible, resistente) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [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) { for (const e of state.estudiosComplementarios) { await db.run( 'INSERT INTO estudiosComplementarios (id, pacienteId, internacionId, fecha, tipo, resultado) VALUES (?, ?, ?, ?, ?, ?)', [e.id, e.pacienteId, e.internacionId || null, e.fecha, e.tipo, e.resultado] ); } } if (state.interconsultas?.length) { for (const ic of state.interconsultas) { await db.run( 'INSERT INTO interconsultas (id, pacienteId, internacionId, fecha, servicioInterconsultado, motivo, respuestaInterconsulta, respuestaFecha) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [ic.id, ic.pacienteId, ic.internacionId, ic.fecha, ic.servicioInterconsultado, ic.motivo || null, ic.respuestaInterconsulta || null, ic.respuestaFecha || null] ); } } if (state.atb?.length) { for (const atb of state.atb) { await db.run( 'INSERT INTO atb (id, pacienteId, internacionId, antibiotico, fechaInicio, fechaFinalizacion) VALUES (?, ?, ?, ?, ?, ?)', [atb.id, atb.pacienteId, atb.internacionId, atb.antibiotico, atb.fechaInicio, atb.fechaFinalizacion || null] ); } } if (state.indicaciones?.length) { for (const ind of state.indicaciones) { await db.run( 'INSERT INTO indicaciones (id, internacionId, tipo, droga, dosis, frecuenciaHoras, via, tipoPlan, tipoPlan2, cantidadMl, cantidadMl2, tiempoHoras, estado, medicoCrea, fechaCrea, tipoInsulina, unidadesDesayuno, unidadesAlmuerzo, unidadesNoche, indicacionNoFco) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [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) { for (const mov of state.movimientosIndicaciones) { await db.run( 'INSERT INTO movimientos_indicaciones (id, indicacionId, internacionId, tipo, fecha, profesional, indicacionPrevia, indicacionNueva) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [mov.id, mov.indicacionId, mov.internacionId, mov.tipo, mov.fecha, mov.profesional, mov.indicacionPrevia || null, mov.indicacionNueva || null] ); } } await db.close(); res.json({ ok: true }); } catch (err) { console.error(err); res.status(500).json({ error: 'failed to save state' }); } }); app.listen(PORT, () => { console.log(`API server listening on http://localhost:${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', async (req, res) => { try { const { dni, password } = req.body; if (!dni || !password) { return res.status(400).json({ error: 'DNI y contraseña requeridos' }); } const usuario = await getUsuarioByDni(dni); if (!usuario) { return res.status(401).json({ error: 'Usuario no encontrado' }); } const validPassword = await 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', async (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 = await verifyPassword(dni, oldPassword); if (!validPassword) { return res.status(401).json({ error: 'Contraseña actual incorrecta' }); } const newHash = await hashPassword(newPassword); await 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', async (req, res) => { try { const { dni, newEmail } = req.body; if (!dni || !newEmail) { return res.status(400).json({ error: 'DNI y email son requeridos' }); } await 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', async (req, res) => { try { const areas = await getAllAreas(); res.json(areas); } catch (err) { console.error(err); res.status(500).json({ error: 'Error al obtener áreas' }); } }); app.get('/api/usuarios', async (req, res) => { try { const usuarios = await 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', async (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 = await getUsuarioByDni(dni); if (existente) { return res.status(400).json({ error: 'Ya existe un usuario con ese DNI' }); } const passwordHash = await 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] }; await 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', async (req, res) => { try { const { id } = req.params; const { apellido, nombre, fechaNacimiento, email, rol, matriculaProfesional, areaId, password } = req.body; const usuario = await 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 = await hashPassword(password); } await 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', async (req, res) => { try { const { id } = req.params; await 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', async (req, res) => { let retries = 3; while (retries > 0) { try { const { id } = req.params; const { estado } = req.body; const db = await openDb(); await db.run('UPDATE camaS SET estado = ? WHERE id = ?', [estado, id]); await db.close(); return res.json({ ok: true }); } catch (err) { console.error('Error updating cama (retry', retries, '):', err.message); retries--; if (retries === 0) { return res.status(500).json({ error: err.message }); } await new Promise(r => setTimeout(r, 200)); } } }); // Helper para actualizar por DNI async function updateUsuarioByDni(dni, datos) { const { getUsuarioByDni, updateUsuario } = await import('./db.js'); const usuario = await getUsuarioByDni(dni); if (usuario) { await updateUsuario(usuario.id, datos); } }