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) ==========
app.get('/api/state', async (req, res) => {
try {
const areasList = await getAllAreas();
const usuariosList = await getAllUsuarios();
const state = {
pacientes: await getAllPacientes(),
areas: areasList,
grupos: areasList,
camas: await getAllCamas(),
internaciones: await getAllInternaciones(),
evoluciones: (await getAllEvoluciones()).map(e => ({
const [
pacientes,
areasList,
camas,
internaciones,
rawEvoluciones,
rawLaboratorios,
otrosLaboratorios,
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,
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 })),
otrosLaboratorios: await getAllOtrosLaboratorios(),
glucemias: await getAllGlucemias(),
acidosBase: await getAllAcidosBase(),
cultivos: await getAllCultivos(),
tiposCultivo: await getAllTiposCultivo(),
gruposDeterminacionesLab: await getAllGruposLaboratorio(),
estudiosComplementarios: await getAllEstudiosComplementarios(),
interconsultas: (await getAllInterconsultas()).map(ic => ({ ...ic, realizada: !!ic.realizada })),
atb: await getAllAtb(),
indicaciones: await getAllIndicaciones(),
movimientosIndicaciones: await getAllMovimientosIndicaciones(),
pendientes: await getAllPendientes(),
usuarios: usuariosList.map(({ passwordHash, ...u }) => u),
}));
const laboratorios = rawLaboratorios.map(l => ({
...l,
resultados: typeof l.resultados === 'string' ? JSON.parse(l.resultados) : l.resultados
}));
const interconsultas = rawInterconsultas.map(ic => ({
...ic,
realizada: !!ic.realizada
}));
const 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',
currentInternacionId: null
};
+41 -40
View File
@@ -160,16 +160,19 @@ export const CORE_PARAMETERS_MAPPING = [
{ claves: ['aptt', 'kptt'], nombre: 'KPTT', unidad: 'seg', esPrincipal: true },
];
export function esParametroCore(param: string): boolean {
const coreList = [
const CORE_LIST_SET = new Set([
'hematocrito', 'hto', 'hemoglobina', 'hb', 'leucocitos', 'gb', 'plaquetas', 'plaq',
'glucemia', 'glucosa', 'urea', 'creatinina', 'creat', 'sodio', 'na', 'potasio', 'k', 'k+',
'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'
];
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 {
const c = clave.toLowerCase().trim();
if (!c) return false;
@@ -182,13 +185,21 @@ export function matchClaveTexto(lineaLower: string, clave: string): boolean {
if (c.length > 4) {
return lineaLower.includes(c);
}
let regex = regexCache.get(c);
if (!regex) {
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);
}
export function parseAcidoBaseTexto(texto: string, fecha?: string, pacienteId?: string): Omit<AcidoBase, 'id'> | null {
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 pco2: number | undefined;
let po2: number | undefined;
@@ -198,9 +209,7 @@ export function parseAcidoBaseTexto(texto: string, fecha?: string, pacienteId?:
let lactato: number | undefined;
let fio2: number | undefined;
for (const linea of lineas) {
const cleanLinea = linea.replace(/show_chart|list_alt|\t+/g, ' ').replace(/,/g, '.').replace(/\s+/g, ' ').trim().toLowerCase();
const getValue = (param: string): number | undefined => {
const getValue = (cleanLinea: string, param: string): number | undefined => {
const idx = cleanLinea.indexOf(param);
if (idx === -1) return undefined;
const after = cleanLinea.slice(idx + param.length).trim();
@@ -212,42 +221,31 @@ export function parseAcidoBaseTexto(texto: string, fecha?: string, pacienteId?:
return undefined;
};
for (const cleanLinea of cleanLineas) {
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);
pco2 = getValue('pco2') || getValue('pco₂');
po2 = getValue('po2') || getValue('po₂');
hco3 = getValue('hco3');
be = getValue('exceso de base') || getValue('base excess') || getValue('exceso');
sato2 = getValue('saturación') || getValue('sato2') || getValue('sat');
lactato = getValue('lactato');
fio2 = getValue('fio2');
const phMatch = cleanLinea.match(/ph\s+(\d+\.?\d*)/);
ph = getValue(cleanLinea, 'ph') || (phMatch ? parseFloat(phMatch[1]) : undefined);
pco2 = getValue(cleanLinea, 'pco2') || getValue(cleanLinea, 'pco₂');
po2 = getValue(cleanLinea, 'po2') || getValue(cleanLinea, 'po₂');
hco3 = getValue(cleanLinea, 'hco3');
be = getValue(cleanLinea, 'exceso de base') || getValue(cleanLinea, 'base excess') || getValue(cleanLinea, 'exceso');
sato2 = getValue(cleanLinea, 'saturación') || getValue(cleanLinea, 'sato2') || getValue(cleanLinea, 'sat');
lactato = getValue(cleanLinea, 'lactato');
fio2 = getValue(cleanLinea, 'fio2');
break;
}
}
if (!ph) {
for (const linea of lineas) {
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);
if (idx === -1) return undefined;
const after = cleanLinea.slice(idx + param.length).trim();
const parts = after.split(' ');
for (const p of parts) {
const v = parseFloat(p);
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'); }
for (const cleanLinea of cleanLineas) {
if (cleanLinea.includes('ph') && !cleanLinea.includes('pco2') && !cleanLinea.includes('pco')) { ph = getValue(cleanLinea, 'ph'); }
else if (cleanLinea.includes('pco2') || cleanLinea.includes('pco₂')) { pco2 = getValue(cleanLinea, 'pco2'); }
else if (cleanLinea.includes('po2') || cleanLinea.includes('po₂')) { po2 = getValue(cleanLinea, 'po2'); }
else if (cleanLinea.includes('hco3')) { hco3 = getValue(cleanLinea, 'hco3'); }
else if (cleanLinea.includes('exceso') || cleanLinea.includes('base excess')) { be = getValue(cleanLinea, 'exceso') || getValue(cleanLinea, 'base'); }
else if (cleanLinea.includes('saturación') || cleanLinea.includes('sato2')) { sato2 = getValue(cleanLinea, 'saturación') || getValue(cleanLinea, 'sat'); }
else if (cleanLinea.includes('lactato')) { lactato = getValue(cleanLinea, 'lactato'); }
else if (cleanLinea.includes('fio2')) { fio2 = getValue(cleanLinea, 'fio2'); }
}
}
@@ -279,6 +277,7 @@ export function parseLaboratorioTextoCompleto(
const resultados: ResultadoLaboratorio[] = [];
const reconocidos: RecognizedDetermination[] = [];
const processedLines = new Set<number>();
const matchedParamNames = new Set<string>();
const lineas = texto.split('\n');
// 1. Process Core Parameters
@@ -288,7 +287,7 @@ export function parseLaboratorioTextoCompleto(
if (!lineaLower) continue;
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 matchedClaveString = '';
@@ -337,6 +336,7 @@ export function parseLaboratorioTextoCompleto(
estado: calcularEstadoLaboratorioExtendido(core.nombre, String(valorFinal))
});
matchedParamNames.add(core.nombre);
processedLines.add(lIdx);
break;
}
@@ -350,7 +350,7 @@ export function parseLaboratorioTextoCompleto(
for (const grupo of activeGroups) {
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++) {
const linea = lineas[lIdx];
@@ -409,6 +409,7 @@ export function parseLaboratorioTextoCompleto(
reconocidos.push(recItem);
processedLines.add(lIdx);
matchedParamNames.add(det.nombre);
resultados.push({
parametro: det.nombre,
+28 -39
View File
@@ -53,55 +53,44 @@ export function isCamaFueraDeGrupo(
}
export function sortCamas<T extends { numero: string }>(camas: T[]): T[] {
const parseBedNumber = (numero: string) => {
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)
};
};
if (!camas || camas.length <= 1) return camas || [];
return [...camas].sort((a, b) => {
const pa = parseBedNumber(a.numero);
const pb = parseBedNumber(b.numero);
const parseBedKey = (numero: string) => {
const clean = (numero || '').trim();
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 esPar = sala % 2 === 0;
let weight = 10;
let isDesc = false;
if (grupo === 2) {
if (!esPar) return { weight: 1, order: 'desc', sala }; // 2XX impares, mayor a menor
return { weight: 2, order: 'asc', sala }; // 2XX pares, menor a mayor
if (!esPar) { weight = 1; isDesc = true; }
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
return { weight: 4, order: 'desc', sala }; // 3XX impares, mayor a menor
}
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 salaDir = isDesc ? -sala : sala;
return { weight, salaDir, cama };
};
const oa = getOrden(pa.sala);
const ob = getOrden(pb.sala);
if (oa.weight !== ob.weight) {
return oa.weight - ob.weight;
}
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;
const mapped = camas.map(item => ({ item, key: parseBedKey(item.numero) }));
mapped.sort((a, b) => {
if (a.key.weight !== b.key.weight) return a.key.weight - b.key.weight;
if (a.key.salaDir !== b.key.salaDir) return a.key.salaDir - b.key.salaDir;
return a.key.cama - b.key.cama;
});
return mapped.map(m => m.item);
}
export function computeSector(numero: string): "En Área" | "Fuera de Área" {