Fix: solucionar error SQLITE_BUSY y mejorar backend

- 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
This commit is contained in:
2026-04-24 02:06:21 -03:00
parent d30dbab8f0
commit e887d66411
8 changed files with 405 additions and 327 deletions
+3
View File
@@ -3,6 +3,9 @@ dist
server/node_modules
server/*.log
*.db
*.db-shm
*.db-wal
*.db-journal
.DS_Store
.env
.env.local
+15
View File
@@ -36,6 +36,7 @@
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -5509,6 +5510,20 @@
"bcrypt": "bin/bcrypt"
}
},
"node_modules/better-sqlite3": {
"version": "12.9.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz",
"integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+1
View File
@@ -38,6 +38,7 @@
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
Binary file not shown.
+100 -136
View File
@@ -1,83 +1,79 @@
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import Database from 'better-sqlite3';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import bcrypt from 'bcryptjs';
import { existsSync, mkdirSync } from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DB_PATH = __dirname + '/data/hospital.db';
export async function openDb() {
const db = await open({
filename: DB_PATH,
driver: sqlite3.Database,
});
// Ensure data directory exists
if (!existsSync(__dirname + '/data')) {
mkdirSync(__dirname + '/data', { recursive: true });
}
await db.exec(`
CREATE TABLE IF NOT EXISTS kv (
key TEXT PRIMARY KEY,
value TEXT
);
`);
// Create persistent database connection with WAL mode and proper settings
const db = new Database(DB_PATH);
await db.exec(`
CREATE TABLE IF NOT EXISTS usuarios (
id TEXT PRIMARY KEY,
apellido TEXT NOT NULL,
nombre TEXT NOT NULL,
dni TEXT UNIQUE NOT NULL,
fechaNacimiento TEXT NOT NULL,
email TEXT NOT NULL,
rol TEXT NOT NULL CHECK(rol IN ('admin', 'medico', 'enfermero')),
matriculaProfesional TEXT,
passwordHash TEXT NOT NULL,
areaId TEXT,
fechaCreacion TEXT NOT NULL
);
`);
// Enable WAL mode for better concurrency
db.pragma('journal_mode = WAL');
// Set busy timeout to 5000ms (5 seconds)
db.pragma('busy_timeout = 5000');
// Additional performance and concurrency settings
db.pragma('synchronous = NORMAL');
db.pragma('cache_size = 1000');
db.pragma('temp_store = MEMORY');
db.pragma('mmap_size = 268435456'); // 256MB
export function getDb() {
return db;
}
export async function getValue(key) {
const db = await openDb();
const row = await db.get('SELECT value FROM kv WHERE key = ?', key);
await db.close();
function rowToObj(row) {
if (!row) return row;
const obj = {};
for (const key in row) {
const val = row[key];
try {
obj[key] = typeof val === 'string' && (val.startsWith('[') || val.startsWith('{')) ? JSON.parse(val) : val;
} catch {
obj[key] = val;
}
}
return obj;
}
export function getValue(key) {
const row = db.prepare('SELECT value FROM kv WHERE key = ?').get(key);
return row?.value ?? null;
}
export async function setValue(key, value) {
const db = await openDb();
await db.run('INSERT INTO kv (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value', key, value);
await db.close();
export function setValue(key, value) {
const stmt = db.prepare('INSERT INTO kv (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value');
stmt.run(key, value);
}
export async function getUsuarioByDni(dni) {
const db = await openDb();
const row = await db.get('SELECT * FROM usuarios WHERE dni = ?', dni);
await db.close();
export function getUsuarioByDni(dni) {
const row = db.prepare('SELECT * FROM usuarios WHERE dni = ?').get(dni);
return row || null;
}
export async function getUsuarioById(id) {
const db = await openDb();
const row = await db.get('SELECT * FROM usuarios WHERE id = ?', id);
await db.close();
export function getUsuarioById(id) {
const row = db.prepare('SELECT * FROM usuarios WHERE id = ?').get(id);
return row || null;
}
export async function getAllUsuarios() {
const db = await openDb();
const rows = await db.all('SELECT * FROM usuarios ORDER BY apellido, nombre');
await db.close();
export function getAllUsuarios() {
const rows = db.prepare('SELECT * FROM usuarios ORDER BY apellido, nombre').all();
return rows;
}
export async function createUsuario(usuario) {
const db = await openDb();
await db.run(
`INSERT INTO usuarios (id, apellido, nombre, dni, fechaNacimiento, email, rol, matriculaProfesional, passwordHash, areaId, fechaCreacion)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
export function createUsuario(usuario) {
const stmt = db.prepare(`INSERT INTO usuarios (id, apellido, nombre, dni, fechaNacimiento, email, rol, matriculaProfesional, passwordHash, areaId, fechaCreacion)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
stmt.run(
usuario.id,
usuario.apellido,
usuario.nombre,
@@ -90,11 +86,9 @@ export async function createUsuario(usuario) {
usuario.areaId || null,
usuario.fechaCreacion
);
await db.close();
}
export async function updateUsuario(id, datos) {
const db = await openDb();
export function updateUsuario(id, datos) {
const fields = [];
const values = [];
@@ -109,53 +103,40 @@ export async function updateUsuario(id, datos) {
if (fields.length > 0) {
values.push(id);
await db.run(`UPDATE usuarios SET ${fields.join(', ')} WHERE id = ?`, values);
db.prepare(`UPDATE usuarios SET ${fields.join(', ')} WHERE id = ?`).run(values);
}
await db.close();
}
export async function deleteUsuario(id) {
const db = await openDb();
await db.run('DELETE FROM usuarios WHERE id = ?', id);
await db.close();
export function deleteUsuario(id) {
db.prepare('DELETE FROM usuarios WHERE id = ?').run(id);
}
export async function verifyPassword(dni, password) {
const db = await openDb();
const row = await db.get('SELECT passwordHash FROM usuarios WHERE dni = ?', dni);
await db.close();
export function verifyPassword(dni, password) {
const row = db.prepare('SELECT passwordHash FROM usuarios WHERE dni = ?').get(dni);
if (!row) return false;
return bcrypt.compareSync(password, row.passwordHash);
}
export async function hashPassword(password) {
export function hashPassword(password) {
return bcrypt.hashSync(password, 10);
}
export async function getAllPacientes() {
const db = await openDb();
const rows = await db.all('SELECT * FROM pacientes');
await db.close();
export function getAllPacientes() {
const rows = db.prepare('SELECT * FROM pacientes').all();
return rows;
}
export async function getAllAreas() {
const db = await openDb();
const rows = await db.all('SELECT * FROM areas');
await db.close();
export function getAllAreas() {
const rows = db.prepare('SELECT * FROM areas').all();
return rows;
}
export async function getAllCamas() {
const db = await openDb();
const rows = await db.all('SELECT * FROM camas');
await db.close();
export function getAllCamas() {
const rows = db.prepare('SELECT * FROM camas').all();
return rows;
}
export async function updateCama(id, updates) {
const db = await openDb();
export function updateCama(id, updates) {
const fields = [];
const values = [];
if (updates.estado !== undefined) {
@@ -172,77 +153,60 @@ export async function updateCama(id, updates) {
}
if (fields.length > 0) {
values.push(id);
await db.run(`UPDATE camas SET ${fields.join(', ')} WHERE id = ?`, values);
db.prepare(`UPDATE camas SET ${fields.join(', ')} WHERE id = ?`).run(values);
}
await db.close();
}
export async function getAllInternaciones() {
const db = await openDb();
const rows = await db.all('SELECT * FROM internaciones');
await db.close();
export function getAllInternaciones() {
const rows = db.prepare('SELECT * FROM internaciones').all();
return rows.map(i => ({ ...i, activa: !!i.activa }));
}
export function getAllEvoluciones() {
const rows = db.prepare('SELECT * FROM evoluciones').all();
return rows.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
}));
}
export function getAllLaboratorios() {
const rows = db.prepare('SELECT * FROM laboratorios').all();
return rows.map(l => ({ ...l, resultados: typeof l.resultados === 'string' ? JSON.parse(l.resultados) : l.resultados }));
}
export function getAllAcidosBase() {
const rows = db.prepare('SELECT * FROM acidosbase').all();
return rows;
}
export async function getAllEvoluciones() {
const db = await openDb();
const rows = await db.all('SELECT * FROM evoluciones');
await db.close();
export function getAllCultivos() {
const rows = db.prepare('SELECT * FROM cultivos').all();
return rows;
}
export async function getAllLaboratorios() {
const db = await openDb();
const rows = await db.all('SELECT * FROM laboratorios');
await db.close();
export function getAllEstudiosComplementarios() {
const rows = db.prepare('SELECT * FROM estudiosComplementarios').all();
return rows;
}
export async function getAllAcidosBase() {
const db = await openDb();
const rows = await db.all('SELECT * FROM acidosbase');
await db.close();
export function getAllInterconsultas() {
const rows = db.prepare('SELECT * FROM interconsultas').all();
return rows.map(ic => ({ ...ic, realizada: !!ic.realizada }));
}
export function getAllAtb() {
const rows = db.prepare('SELECT * FROM atb').all();
return rows;
}
export async function getAllCultivos() {
const db = await openDb();
const rows = await db.all('SELECT * FROM cultivos');
await db.close();
export function getAllIndicaciones() {
const rows = db.prepare('SELECT * FROM indicaciones').all();
return rows;
}
export async function getAllEstudiosComplementarios() {
const db = await openDb();
const rows = await db.all('SELECT * FROM estudiosComplementarios');
await db.close();
return rows;
}
export async function getAllInterconsultas() {
const db = await openDb();
const rows = await db.all('SELECT * FROM interconsultas');
await db.close();
return rows;
}
export async function getAllAtb() {
const db = await openDb();
const rows = await db.all('SELECT * FROM atb');
await db.close();
return rows;
}
export async function getAllIndicaciones() {
const db = await openDb();
const rows = await db.all('SELECT * FROM indicaciones');
await db.close();
return rows;
}
export async function getAllMovimientosIndicaciones() {
const db = await openDb();
const rows = await db.all('SELECT * FROM movimientos_indicaciones');
await db.close();
export function getAllMovimientosIndicaciones() {
const rows = db.prepare('SELECT * FROM movimientos_indicaciones').all();
return rows;
}
+206 -166
View File
@@ -1,6 +1,6 @@
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';
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());
@@ -9,26 +9,22 @@ 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) => {
app.get('/api/state', (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(),
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
};
@@ -39,137 +35,119 @@ app.get('/api/state', async (req, res) => {
}
});
app.put('/api/state', async (req, res) => {
app.put('/api/state', (req, res) => {
try {
const state = req.body;
const db = await openDb();
const db = getDb();
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');
// 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) {
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.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) {
for (const a of state.areas) {
await db.run('INSERT INTO areas (id, nombre) VALUES (?, ?)', [a.id, a.nombre]);
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) {
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.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) {
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.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) {
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.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) {
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.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) {
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.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) {
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.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) {
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.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) {
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.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) {
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.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) {
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.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) {
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]
);
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);
}
}
}
})();
await db.close();
res.json({ ok: true });
} catch (err) {
console.error(err);
@@ -177,8 +155,79 @@ app.put('/api/state', async (req, res) => {
}
});
// 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(`API server listening on http://localhost:${PORT}`);
console.log(`Server running on port ${PORT}`);
});
// === AUTENTICACIÓN ===
@@ -191,19 +240,19 @@ function generateUUID() {
});
}
app.post('/api/auth/login', async (req, res) => {
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 = await getUsuarioByDni(dni);
const usuario = getUsuarioByDni(dni);
if (!usuario) {
return res.status(401).json({ error: 'Usuario no encontrado' });
}
const validPassword = await verifyPassword(dni, password);
const validPassword = verifyPassword(dni, password);
if (!validPassword) {
return res.status(401).json({ error: 'Contraseña incorrecta' });
}
@@ -216,20 +265,20 @@ app.post('/api/auth/login', async (req, res) => {
}
});
app.post('/api/auth/change-password', async (req, res) => {
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 = await verifyPassword(dni, oldPassword);
const validPassword = 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 });
const newHash = hashPassword(newPassword);
updateUsuarioByDni(dni, { passwordHash: newHash });
res.json({ ok: true });
} catch (err) {
console.error(err);
@@ -237,14 +286,14 @@ app.post('/api/auth/change-password', async (req, res) => {
}
});
app.put('/api/auth/update-email', async (req, res) => {
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' });
}
await updateUsuarioByDni(dni, { email: newEmail });
updateUsuarioByDni(dni, { email: newEmail });
res.json({ ok: true });
} catch (err) {
console.error(err);
@@ -254,9 +303,9 @@ app.put('/api/auth/update-email', async (req, res) => {
// === GESTIÓN DE USUARIOS (ADMIN) ===
app.get('/api/areas', async (req, res) => {
app.get('/api/areas', (req, res) => {
try {
const areas = await getAllAreas();
const areas = getAllAreas();
res.json(areas);
} catch (err) {
console.error(err);
@@ -264,9 +313,9 @@ app.get('/api/areas', async (req, res) => {
}
});
app.get('/api/usuarios', async (req, res) => {
app.get('/api/usuarios', (req, res) => {
try {
const usuarios = await getAllUsuarios();
const usuarios = getAllUsuarios();
const usuariosSinPassword = usuarios.map(({ passwordHash, ...u }) => u);
res.json(usuariosSinPassword);
} catch (err) {
@@ -275,7 +324,7 @@ app.get('/api/usuarios', async (req, res) => {
}
});
app.post('/api/usuarios', async (req, res) => {
app.post('/api/usuarios', (req, res) => {
try {
const { apellido, nombre, dni, fechaNacimiento, email, rol, matriculaProfesional, password, areaId } = req.body;
@@ -283,12 +332,12 @@ app.post('/api/usuarios', async (req, res) => {
return res.status(400).json({ error: 'Todos los campos obligatorios deben estar presentes' });
}
const existente = await getUsuarioByDni(dni);
const existente = getUsuarioByDni(dni);
if (existente) {
return res.status(400).json({ error: 'Ya existe un usuario con ese DNI' });
}
const passwordHash = await hashPassword(password);
const passwordHash = hashPassword(password);
const usuario = {
id: generateUUID(),
apellido,
@@ -303,7 +352,7 @@ app.post('/api/usuarios', async (req, res) => {
fechaCreacion: new Date().toISOString().split('T')[0]
};
await createUsuario(usuario);
createUsuario(usuario);
res.json({ ...usuario, passwordHash: undefined });
} catch (err) {
console.error(err);
@@ -311,12 +360,12 @@ app.post('/api/usuarios', async (req, res) => {
}
});
app.put('/api/usuarios/:id', async (req, res) => {
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 = await getUsuarioById(id);
const usuario = getUsuarioById(id);
if (!usuario) {
return res.status(404).json({ error: 'Usuario no encontrado' });
}
@@ -330,10 +379,10 @@ app.put('/api/usuarios/:id', async (req, res) => {
if (matriculaProfesional !== undefined) datos.matriculaProfesional = matriculaProfesional;
if (areaId !== undefined) datos.areaId = areaId;
if (password) {
datos.passwordHash = await hashPassword(password);
datos.passwordHash = hashPassword(password);
}
await updateUsuario(id, datos);
updateUsuario(id, datos);
res.json({ ok: true });
} catch (err) {
console.error(err);
@@ -341,10 +390,10 @@ app.put('/api/usuarios/:id', async (req, res) => {
}
});
app.delete('/api/usuarios/:id', async (req, res) => {
app.delete('/api/usuarios/:id', (req, res) => {
try {
const { id } = req.params;
await deleteUsuario(id);
deleteUsuario(id);
res.json({ ok: true });
} catch (err) {
console.error(err);
@@ -352,32 +401,23 @@ app.delete('/api/usuarios/:id', async (req, res) => {
}
});
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));
}
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
async function updateUsuarioByDni(dni, datos) {
const { getUsuarioByDni, updateUsuario } = await import('./db.js');
const usuario = await getUsuarioByDni(dni);
function updateUsuarioByDni(dni, datos) {
const usuario = getUsuarioByDni(dni);
if (usuario) {
await updateUsuario(usuario.id, datos);
updateUsuario(usuario.id, datos);
}
}
+27
View File
@@ -16,6 +16,14 @@ if (!existsSync(join(__dirname, 'data'))) {
const db = new Database(DB_PATH);
// Configure WAL mode and performance settings
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');
db.pragma('synchronous = NORMAL');
db.pragma('cache_size = 1000');
db.pragma('temp_store = MEMORY');
db.pragma('mmap_size = 268435456');
try {
db.exec("ALTER TABLE internaciones ADD COLUMN areaId TEXT;");
} catch (e) {
@@ -23,6 +31,20 @@ try {
}
db.exec(`
CREATE TABLE IF NOT EXISTS usuarios (
id TEXT PRIMARY KEY,
apellido TEXT NOT NULL,
nombre TEXT NOT NULL,
dni TEXT NOT NULL UNIQUE,
fechaNacimiento TEXT,
email TEXT,
rol TEXT,
matriculaProfesional TEXT,
passwordHash TEXT,
areaId TEXT,
fechaCreacion TEXT
);
CREATE TABLE IF NOT EXISTS pacientes (
id TEXT PRIMARY KEY,
apellido TEXT NOT NULL,
@@ -139,6 +161,11 @@ CREATE TABLE IF NOT EXISTS areas (
tipo TEXT,
resultado TEXT
);
CREATE TABLE IF NOT EXISTS kv (
key TEXT PRIMARY KEY,
value TEXT
);
`);
try {
+52 -24
View File
@@ -111,33 +111,61 @@ export function useHospitalStore() {
return () => { mounted = false; };
}, []);
// Persist state to backend when it changes (debounced)
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
// Persistence: event-driven + periodic save
const [saveQueue, setSaveQueue] = useState<string[]>([]);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
if (isLoaded && !initialLoadComplete) {
setInitialLoadComplete(true);
}
}, [isLoaded, initialLoadComplete]);
// Track which parts changed
const [changedKeys, setChangedKeys] = useState<Set<string>>(new Set());
useEffect(() => {
if (!initialLoadComplete) return;
const t = setTimeout(() => {
(async () => {
try {
const { currentUser, isAuthenticated, ...stateToSave } = state;
await fetch(`${API_BASE}/state`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(stateToSave),
});
} catch (err) {
// ignore save errors for now
// Mark parts as changed
const markChanged = (key: string) => {
setChangedKeys(prev => {
const next = new Set(prev);
next.add(key);
return next;
});
};
// Save only changed parts
const saveChanges = useCallback(async () => {
if (isSaving || changedKeys.size === 0) return;
setIsSaving(true);
try {
const keysToSave = Array.from(changedKeys);
const partialState: any = {};
keysToSave.forEach(key => {
if (key in state) {
partialState[key] = state[key];
}
})();
}, 300);
return () => clearTimeout(t);
}, [state, initialLoadComplete]);
});
await fetch(`${API_BASE}/state/partial`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(partialState),
});
setChangedKeys(new Set());
} catch (err) {
console.error('Save error:', err);
} finally {
setIsSaving(false);
}
}, [state, changedKeys, isSaving]);
// Periodic save every 5 seconds if there are changes
useEffect(() => {
if (!isLoaded) return;
const t = setInterval(() => {
saveChanges();
}, 5000);
return () => clearInterval(t);
}, [isLoaded, saveChanges]);
// Also save on特定 events (optional)
const queueSave = (keys: string[]) => {
keys.forEach(markChanged);
};
// Utility functions
const getCamaAreaId = useCallback((camaId: string): string | null => {