From dfe25e627ebd3537b1a607637b1218514f46d73c Mon Sep 17 00:00:00 2001 From: snlavaise Date: Tue, 18 Aug 2026 04:35:06 +0000 Subject: [PATCH] =?UTF-8?q?Optimizaciones=20de=20rendimiento:=20consultas?= =?UTF-8?q?=20paralelas=20en=20API,=20cach=C3=A9=20de=20Regex=20en=20parse?= =?UTF-8?q?r=20de=20laboratorio=20y=20pre-c=C3=A1lculo=20de=20claves=20en?= =?UTF-8?q?=20sortCamas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api-mongodb.js | 102 +++++++++++++++++++++++++-------- src/lib/laboratoryParser.ts | 109 ++++++++++++++++++------------------ src/lib/utils.ts | 81 ++++++++++++--------------- 3 files changed, 168 insertions(+), 124 deletions(-) diff --git a/server/api-mongodb.js b/server/api-mongodb.js index 94828fc..35e6e6a 100644 --- a/server/api-mongodb.js +++ b/server/api-mongodb.js @@ -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 [ + 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 + })); + + 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: await getAllPacientes(), + pacientes, areas: areasList, grupos: areasList, - camas: await getAllCamas(), - internaciones: await getAllInternaciones(), - 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 })), - 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), + camas, + internaciones, + evoluciones, + laboratorios, + otrosLaboratorios, + glucemias, + acidosBase, + cultivos, + tiposCultivo, + gruposDeterminacionesLab, + estudiosComplementarios, + interconsultas, + atb, + indicaciones, + movimientosIndicaciones, + pendientes, + usuarios, vistaActual: 'dashboard', currentInternacionId: null }; diff --git a/src/lib/laboratoryParser.ts b/src/lib/laboratoryParser.ts index 64ea197..7245de3 100644 --- a/src/lib/laboratoryParser.ts +++ b/src/lib/laboratoryParser.ts @@ -160,16 +160,19 @@ export const CORE_PARAMETERS_MAPPING = [ { claves: ['aptt', 'kptt'], nombre: 'KPTT', unidad: 'seg', esPrincipal: true }, ]; +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' +]); + export function esParametroCore(param: string): boolean { - const coreList = [ - '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()); + return CORE_LIST_SET.has(param.toLowerCase().trim()); } +const regexCache = new Map(); + 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); } - const escaped = c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const regex = new RegExp(`(?:^|[^a-z0-9_])${escaped}(?:$|[^a-z0-9_])`, 'i'); + let regex = regexCache.get(c); + if (!regex) { + const escaped = c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + 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 | 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,56 +209,43 @@ 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 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; - }; + 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(); + const parts = after.split(' '); + for (const p of parts) { + const v = parseFloat(p); + if (!isNaN(v) && v > 0 && v < 1000) return v; + } + 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(); + const matchedParamNames = new Set(); 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, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 93cb7e4..f193220 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -53,55 +53,44 @@ export function isCamaFueraDeGrupo( } export function sortCamas(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 || []; + + 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 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) { 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; } + } + + const salaDir = isDesc ? -sala : sala; + return { weight, salaDir, cama }; }; - return [...camas].sort((a, b) => { - const pa = parseBedNumber(a.numero); - const pb = parseBedNumber(b.numero); - - const getOrden = (sala: number) => { - const grupo = Math.floor(sala / 100); - const esPar = sala % 2 === 0; - - 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 (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 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" {