Optimizaciones de rendimiento: consultas paralelas en API, caché de Regex en parser de laboratorio y pre-cálculo de claves en sortCamas

This commit is contained in:
2026-08-18 04:35:06 +00:00
parent 770a4ea426
commit dfe25e627e
3 changed files with 168 additions and 124 deletions
+78 -24
View File
@@ -79,33 +79,87 @@ function generateUUID() {
// ========== STATE ENDPOINT (initial load) ========== // ========== STATE ENDPOINT (initial load) ==========
app.get('/api/state', async (req, res) => { app.get('/api/state', async (req, res) => {
try { try {
const areasList = await getAllAreas(); const [
const usuariosList = await getAllUsuarios(); pacientes,
const state = { areasList,
pacientes: await getAllPacientes(), camas,
areas: areasList, internaciones,
grupos: areasList, rawEvoluciones,
camas: await getAllCamas(), rawLaboratorios,
internaciones: await getAllInternaciones(), otrosLaboratorios,
evoluciones: (await getAllEvoluciones()).map(e => ({ glucemias,
acidosBase,
cultivos,
tiposCultivo,
gruposDeterminacionesLab,
estudiosComplementarios,
rawInterconsultas,
atb,
indicaciones,
movimientosIndicaciones,
pendientes,
usuariosList
] = await Promise.all([
getAllPacientes(),
getAllAreas(),
getAllCamas(),
getAllInternaciones(),
getAllEvoluciones(),
getAllLaboratorios(),
getAllOtrosLaboratorios(),
getAllGlucemias(),
getAllAcidosBase(),
getAllCultivos(),
getAllTiposCultivo(),
getAllGruposLaboratorio(),
getAllEstudiosComplementarios(),
getAllInterconsultas(),
getAllAtb(),
getAllIndicaciones(),
getAllMovimientosIndicaciones(),
getAllPendientes(),
getAllUsuarios()
]);
const evoluciones = rawEvoluciones.map(e => ({
...e, ...e,
signosVitales: typeof e.signosVitales === 'string' ? JSON.parse(e.signosVitales) : e.signosVitales, signosVitales: typeof e.signosVitales === 'string' ? JSON.parse(e.signosVitales) : e.signosVitales,
examenFisico: typeof e.examenFisico === 'string' ? JSON.parse(e.examenFisico) : e.examenFisico 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 })),
otrosLaboratorios: await getAllOtrosLaboratorios(), const laboratorios = rawLaboratorios.map(l => ({
glucemias: await getAllGlucemias(), ...l,
acidosBase: await getAllAcidosBase(), resultados: typeof l.resultados === 'string' ? JSON.parse(l.resultados) : l.resultados
cultivos: await getAllCultivos(), }));
tiposCultivo: await getAllTiposCultivo(),
gruposDeterminacionesLab: await getAllGruposLaboratorio(), const interconsultas = rawInterconsultas.map(ic => ({
estudiosComplementarios: await getAllEstudiosComplementarios(), ...ic,
interconsultas: (await getAllInterconsultas()).map(ic => ({ ...ic, realizada: !!ic.realizada })), realizada: !!ic.realizada
atb: await getAllAtb(), }));
indicaciones: await getAllIndicaciones(),
movimientosIndicaciones: await getAllMovimientosIndicaciones(), const usuarios = usuariosList.map(({ passwordHash, ...u }) => u);
pendientes: await getAllPendientes(),
usuarios: usuariosList.map(({ passwordHash, ...u }) => u), const state = {
pacientes,
areas: areasList,
grupos: areasList,
camas,
internaciones,
evoluciones,
laboratorios,
otrosLaboratorios,
glucemias,
acidosBase,
cultivos,
tiposCultivo,
gruposDeterminacionesLab,
estudiosComplementarios,
interconsultas,
atb,
indicaciones,
movimientosIndicaciones,
pendientes,
usuarios,
vistaActual: 'dashboard', vistaActual: 'dashboard',
currentInternacionId: null currentInternacionId: null
}; };
+41 -40
View File
@@ -160,16 +160,19 @@ export const CORE_PARAMETERS_MAPPING = [
{ claves: ['aptt', 'kptt'], nombre: 'KPTT', unidad: 'seg', esPrincipal: true }, { claves: ['aptt', 'kptt'], nombre: 'KPTT', unidad: 'seg', esPrincipal: true },
]; ];
export function esParametroCore(param: string): boolean { const CORE_LIST_SET = new Set([
const coreList = [
'hematocrito', 'hto', 'hemoglobina', 'hb', 'leucocitos', 'gb', 'plaquetas', 'plaq', 'hematocrito', 'hto', 'hemoglobina', 'hb', 'leucocitos', 'gb', 'plaquetas', 'plaq',
'glucemia', 'glucosa', 'urea', 'creatinina', 'creat', 'sodio', 'na', 'potasio', 'k', 'k+', 'glucemia', 'glucosa', 'urea', 'creatinina', 'creat', 'sodio', 'na', 'potasio', 'k', 'k+',
'cloro', 'cl', 'cl-', 'bilirrubina total', 'bt', 'bilirrubina directa', 'bd', 'got', 'ast', 'gpt', 'alt', 'cloro', 'cl', 'cl-', 'bilirrubina total', 'bt', 'bilirrubina directa', 'bd', 'got', 'ast', 'gpt', 'alt',
'tiempo de protrombina', 'tp', 't.p.', 't.p', 'rin', 'inr', 'aptt', 'kptt', 'proteínas totales', 'proteinas totales' 'tiempo de protrombina', 'tp', 't.p.', 't.p', 'rin', 'inr', 'aptt', 'kptt', 'proteínas totales', 'proteinas totales'
]; ]);
return coreList.includes(param.toLowerCase().trim());
export function esParametroCore(param: string): boolean {
return CORE_LIST_SET.has(param.toLowerCase().trim());
} }
const regexCache = new Map<string, RegExp>();
export function matchClaveTexto(lineaLower: string, clave: string): boolean { export function matchClaveTexto(lineaLower: string, clave: string): boolean {
const c = clave.toLowerCase().trim(); const c = clave.toLowerCase().trim();
if (!c) return false; if (!c) return false;
@@ -182,13 +185,21 @@ export function matchClaveTexto(lineaLower: string, clave: string): boolean {
if (c.length > 4) { if (c.length > 4) {
return lineaLower.includes(c); return lineaLower.includes(c);
} }
let regex = regexCache.get(c);
if (!regex) {
const escaped = c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escaped = c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(?:^|[^a-z0-9_])${escaped}(?:$|[^a-z0-9_])`, 'i'); regex = new RegExp(`(?:^|[^a-z0-9_])${escaped}(?:$|[^a-z0-9_])`, 'i');
regexCache.set(c, regex);
}
return regex.test(lineaLower); return regex.test(lineaLower);
} }
export function parseAcidoBaseTexto(texto: string, fecha?: string, pacienteId?: string): Omit<AcidoBase, 'id'> | null { export function parseAcidoBaseTexto(texto: string, fecha?: string, pacienteId?: string): Omit<AcidoBase, 'id'> | null {
const lineas = texto.split('\n'); const lineas = texto.split('\n');
const cleanLineas = lineas.map(linea =>
linea.replace(/show_chart|list_alt|\t+/g, ' ').replace(/,/g, '.').replace(/\s+/g, ' ').trim().toLowerCase()
);
let ph: number | undefined; let ph: number | undefined;
let pco2: number | undefined; let pco2: number | undefined;
let po2: number | undefined; let po2: number | undefined;
@@ -198,9 +209,7 @@ export function parseAcidoBaseTexto(texto: string, fecha?: string, pacienteId?:
let lactato: number | undefined; let lactato: number | undefined;
let fio2: number | undefined; let fio2: number | undefined;
for (const linea of lineas) { const getValue = (cleanLinea: string, param: string): number | undefined => {
const cleanLinea = linea.replace(/show_chart|list_alt|\t+/g, ' ').replace(/,/g, '.').replace(/\s+/g, ' ').trim().toLowerCase();
const getValue = (param: string): number | undefined => {
const idx = cleanLinea.indexOf(param); const idx = cleanLinea.indexOf(param);
if (idx === -1) return undefined; if (idx === -1) return undefined;
const after = cleanLinea.slice(idx + param.length).trim(); const after = cleanLinea.slice(idx + param.length).trim();
@@ -212,42 +221,31 @@ export function parseAcidoBaseTexto(texto: string, fecha?: string, pacienteId?:
return undefined; return undefined;
}; };
for (const cleanLinea of cleanLineas) {
if ((cleanLinea.includes('estado') && cleanLinea.includes('ácido')) || cleanLinea.includes('base') || cleanLinea.includes('gases en sangre')) { if ((cleanLinea.includes('estado') && cleanLinea.includes('ácido')) || cleanLinea.includes('base') || cleanLinea.includes('gases en sangre')) {
ph = getValue('ph') || (cleanLinea.match(/ph\s+(\d+\.?\d*)/)?.[1] ? parseFloat(cleanLinea.match(/ph\s+(\d+\.?\d*)/)![1]) : undefined); const phMatch = cleanLinea.match(/ph\s+(\d+\.?\d*)/);
pco2 = getValue('pco2') || getValue('pco₂'); ph = getValue(cleanLinea, 'ph') || (phMatch ? parseFloat(phMatch[1]) : undefined);
po2 = getValue('po2') || getValue('po₂'); pco2 = getValue(cleanLinea, 'pco2') || getValue(cleanLinea, 'pco₂');
hco3 = getValue('hco3'); po2 = getValue(cleanLinea, 'po2') || getValue(cleanLinea, 'po₂');
be = getValue('exceso de base') || getValue('base excess') || getValue('exceso'); hco3 = getValue(cleanLinea, 'hco3');
sato2 = getValue('saturación') || getValue('sato2') || getValue('sat'); be = getValue(cleanLinea, 'exceso de base') || getValue(cleanLinea, 'base excess') || getValue(cleanLinea, 'exceso');
lactato = getValue('lactato'); sato2 = getValue(cleanLinea, 'saturación') || getValue(cleanLinea, 'sato2') || getValue(cleanLinea, 'sat');
fio2 = getValue('fio2'); lactato = getValue(cleanLinea, 'lactato');
fio2 = getValue(cleanLinea, 'fio2');
break; break;
} }
} }
if (!ph) { if (!ph) {
for (const linea of lineas) { for (const cleanLinea of cleanLineas) {
const cleanLinea = linea.replace(/show_chart|list_alt|\t+/g, ' ').replace(/,/g, '.').replace(/\s+/g, ' ').trim().toLowerCase(); if (cleanLinea.includes('ph') && !cleanLinea.includes('pco2') && !cleanLinea.includes('pco')) { ph = getValue(cleanLinea, 'ph'); }
const getValue = (param: string): number | undefined => { else if (cleanLinea.includes('pco2') || cleanLinea.includes('pco₂')) { pco2 = getValue(cleanLinea, 'pco2'); }
const idx = cleanLinea.indexOf(param); else if (cleanLinea.includes('po2') || cleanLinea.includes('po₂')) { po2 = getValue(cleanLinea, 'po2'); }
if (idx === -1) return undefined; else if (cleanLinea.includes('hco3')) { hco3 = getValue(cleanLinea, 'hco3'); }
const after = cleanLinea.slice(idx + param.length).trim(); else if (cleanLinea.includes('exceso') || cleanLinea.includes('base excess')) { be = getValue(cleanLinea, 'exceso') || getValue(cleanLinea, 'base'); }
const parts = after.split(' '); else if (cleanLinea.includes('saturación') || cleanLinea.includes('sato2')) { sato2 = getValue(cleanLinea, 'saturación') || getValue(cleanLinea, 'sat'); }
for (const p of parts) { else if (cleanLinea.includes('lactato')) { lactato = getValue(cleanLinea, 'lactato'); }
const v = parseFloat(p); else if (cleanLinea.includes('fio2')) { fio2 = getValue(cleanLinea, 'fio2'); }
if (!isNaN(v) && v > 0 && v < 1000) return v;
}
return undefined;
};
if (cleanLinea.includes('ph') && !cleanLinea.includes('pco2') && !cleanLinea.includes('pco')) { ph = getValue('ph'); }
else if (cleanLinea.includes('pco2') || cleanLinea.includes('pco₂')) { pco2 = getValue('pco2'); }
else if (cleanLinea.includes('po2') || cleanLinea.includes('po₂')) { po2 = getValue('po2'); }
else if (cleanLinea.includes('hco3')) { hco3 = getValue('hco3'); }
else if (cleanLinea.includes('exceso') || cleanLinea.includes('base excess')) { be = getValue('exceso') || getValue('base'); }
else if (cleanLinea.includes('saturación') || cleanLinea.includes('sato2')) { sato2 = getValue('saturación') || getValue('sat'); }
else if (cleanLinea.includes('lactato')) { lactato = getValue('lactato'); }
else if (cleanLinea.includes('fio2')) { fio2 = getValue('fio2'); }
} }
} }
@@ -279,6 +277,7 @@ export function parseLaboratorioTextoCompleto(
const resultados: ResultadoLaboratorio[] = []; const resultados: ResultadoLaboratorio[] = [];
const reconocidos: RecognizedDetermination[] = []; const reconocidos: RecognizedDetermination[] = [];
const processedLines = new Set<number>(); const processedLines = new Set<number>();
const matchedParamNames = new Set<string>();
const lineas = texto.split('\n'); const lineas = texto.split('\n');
// 1. Process Core Parameters // 1. Process Core Parameters
@@ -288,7 +287,7 @@ export function parseLaboratorioTextoCompleto(
if (!lineaLower) continue; if (!lineaLower) continue;
for (const core of CORE_PARAMETERS_MAPPING) { for (const core of CORE_PARAMETERS_MAPPING) {
if (resultados.some(r => r.parametro === core.nombre)) continue; if (matchedParamNames.has(core.nombre)) continue;
let matchedClave = false; let matchedClave = false;
let matchedClaveString = ''; let matchedClaveString = '';
@@ -337,6 +336,7 @@ export function parseLaboratorioTextoCompleto(
estado: calcularEstadoLaboratorioExtendido(core.nombre, String(valorFinal)) estado: calcularEstadoLaboratorioExtendido(core.nombre, String(valorFinal))
}); });
matchedParamNames.add(core.nombre);
processedLines.add(lIdx); processedLines.add(lIdx);
break; break;
} }
@@ -350,7 +350,7 @@ export function parseLaboratorioTextoCompleto(
for (const grupo of activeGroups) { for (const grupo of activeGroups) {
for (const det of (grupo.determinaciones || [])) { for (const det of (grupo.determinaciones || [])) {
if (resultados.some(r => r.parametro === det.nombre)) continue; if (matchedParamNames.has(det.nombre)) continue;
for (let lIdx = 0; lIdx < lineas.length; lIdx++) { for (let lIdx = 0; lIdx < lineas.length; lIdx++) {
const linea = lineas[lIdx]; const linea = lineas[lIdx];
@@ -409,6 +409,7 @@ export function parseLaboratorioTextoCompleto(
reconocidos.push(recItem); reconocidos.push(recItem);
processedLines.add(lIdx); processedLines.add(lIdx);
matchedParamNames.add(det.nombre);
resultados.push({ resultados.push({
parametro: det.nombre, parametro: det.nombre,
+28 -39
View File
@@ -53,55 +53,44 @@ export function isCamaFueraDeGrupo(
} }
export function sortCamas<T extends { numero: string }>(camas: T[]): T[] { export function sortCamas<T extends { numero: string }>(camas: T[]): T[] {
const parseBedNumber = (numero: string) => { if (!camas || camas.length <= 1) return camas || [];
const match = (numero || '').match(/^(\d{3})-(\d+)$/);
if (!match) return { sala: 0, cama: 0 };
return {
sala: parseInt(match[1], 10),
cama: parseInt(match[2], 10)
};
};
return [...camas].sort((a, b) => { const parseBedKey = (numero: string) => {
const pa = parseBedNumber(a.numero); const clean = (numero || '').trim();
const pb = parseBedNumber(b.numero); const match = clean.match(/^(\d{3})-(\d+)$/);
if (!match) return { weight: 10, salaDir: 0, cama: 0 };
const getOrden = (sala: number) => { const sala = parseInt(match[1], 10);
const cama = parseInt(match[2], 10);
const grupo = Math.floor(sala / 100); const grupo = Math.floor(sala / 100);
const esPar = sala % 2 === 0; const esPar = sala % 2 === 0;
let weight = 10;
let isDesc = false;
if (grupo === 2) { if (grupo === 2) {
if (!esPar) return { weight: 1, order: 'desc', sala }; // 2XX impares, mayor a menor if (!esPar) { weight = 1; isDesc = true; }
return { weight: 2, order: 'asc', sala }; // 2XX pares, menor a mayor else { weight = 2; isDesc = false; }
} else if (grupo === 3) {
if (esPar) { weight = 3; isDesc = false; }
else { weight = 4; isDesc = true; }
} else if (grupo === 4) {
if (!esPar) { weight = 5; isDesc = true; }
else { weight = 6; isDesc = false; }
} }
if (grupo === 3) {
if (esPar) return { weight: 3, order: 'asc', sala }; // 3XX pares, menor a mayor const salaDir = isDesc ? -sala : sala;
return { weight: 4, order: 'desc', sala }; // 3XX impares, mayor a menor return { weight, salaDir, cama };
}
if (grupo === 4) {
if (!esPar) return { weight: 5, order: 'desc', sala }; // 4XX impares, mayor a menor
return { weight: 6, order: 'asc', sala }; // 4XX pares, menor a mayor
}
return { weight: 10, order: 'asc', sala };
}; };
const oa = getOrden(pa.sala); const mapped = camas.map(item => ({ item, key: parseBedKey(item.numero) }));
const ob = getOrden(pb.sala); mapped.sort((a, b) => {
if (a.key.weight !== b.key.weight) return a.key.weight - b.key.weight;
if (oa.weight !== ob.weight) { if (a.key.salaDir !== b.key.salaDir) return a.key.salaDir - b.key.salaDir;
return oa.weight - ob.weight; return a.key.cama - b.key.cama;
}
if (oa.sala !== ob.sala) {
if (oa.order === 'desc') {
return ob.sala - oa.sala;
} else {
return oa.sala - ob.sala;
}
}
return pa.cama - pb.cama;
}); });
return mapped.map(m => m.item);
} }
export function computeSector(numero: string): "En Área" | "Fuera de Área" { export function computeSector(numero: string): "En Área" | "Fuera de Área" {