From 540fc8339c1a19b80b9835d0910387bdd6b8012d Mon Sep 17 00:00:00 2001 From: nicolasarana <90768149+nicolasarana@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:03:04 -0300 Subject: [PATCH 1/4] HUDS : Armar api o controladores para situaciones activas y antecedentes --- initialize.ts | 1 + modules/huds/hudsSituaciones.controller.ts | 133 ++++++++++++++++++ modules/huds/hudsSituaciones.routes.ts | 44 ++++++ modules/huds/index.ts | 1 + .../schemas/carnet-perinatal.schema.ts | 16 +++ 5 files changed, 195 insertions(+) create mode 100644 modules/huds/hudsSituaciones.controller.ts create mode 100644 modules/huds/hudsSituaciones.routes.ts diff --git a/initialize.ts b/initialize.ts index 63bd7a141a..4fcd888b75 100644 --- a/initialize.ts +++ b/initialize.ts @@ -106,6 +106,7 @@ export function initAPI(app: Express) { app.use('/api/modules/gestor-usuarios', require('./modules/gestor-usuarios').PerfilesRouter); app.use('/api/modules/registro-novedades', require('./modules/registro-novedades').NovedadesRouter); app.use('/api/modules/huds', require('./modules/huds').HudsAccesoRouter); + app.use('/api/modules/huds', require('./modules/huds').HudsSituacionesRouter); app.use('/api/modules/webhook', require('./modules/webhook').WebhookRouter); app.use('/api/modules/webhook', require('./modules/webhook/webhooklog').WebhookLogRouter); app.use('/api/modules/seguimiento-paciente', require('./modules/seguimiento-paciente').SeguimientoPacienteRouter); diff --git a/modules/huds/hudsSituaciones.controller.ts b/modules/huds/hudsSituaciones.controller.ts new file mode 100644 index 0000000000..9311ee37c7 --- /dev/null +++ b/modules/huds/hudsSituaciones.controller.ts @@ -0,0 +1,133 @@ +import { PacienteCtr } from '../../core-v2/mpi/paciente/paciente.routes'; +import { SnomedCtr } from '../../core/term/controller/snomed.controller'; +import { Prestacion } from '../rup/schemas/prestacion'; +import { CarnetPerinatal } from '../perinatal/schemas/carnet-perinatal.schema'; +import { buscarEnHuds } from '../rup/controllers/rup'; +import * as moment from 'moment'; + +// SNOMED ECL para antecedentes familiares +const ECL_ANTECEDENTES_FAMILIARES = '<< 57177007'; + +function registrosPorSemanticTag(registros, semanticTag) { + const result = []; + for (const registro of registros) { + if (registro.concepto && registro.concepto.semanticTag === semanticTag) { + result.push(registro); + } + if (registro.registros && registro.registros.length > 0) { + result.push(...registrosPorSemanticTag(registro.registros, semanticTag)); + } + } + return result; +} + +export async function situacionesActivas(pacienteID) { + const paciente = await PacienteCtr.findById(pacienteID); + if (!paciente) { + return null; + } + + const result: any[] = []; + + const prestaciones: any[] = await Prestacion.find({ + 'paciente.id': { $in: paciente.vinculos }, + 'estadoActual.tipo': 'validada' + }); + + for (const prestacion of prestaciones) { + const registros = registrosPorSemanticTag(prestacion.ejecucion.registros || [], 'trastorno'); + for (const registro of registros) { + result.push({ + tipo: 'trastorno', + concepto: registro.concepto, + fecha: prestacion.ejecucion.fecha, + profesional: prestacion.solicitud.profesional, + organizacion: prestacion.ejecucion.organizacion, + idPrestacion: prestacion._id, + tipoPrestacion: prestacion.solicitud.tipoPrestacion + }); + } + } + + // Usar vinculos para cubrir pacientes con múltiples identificadores ANDES + const carnet: any = await CarnetPerinatal.findOne({ + 'paciente.id': { $in: paciente.vinculos }, + fechaFinEmbarazo: null + }); + + if (carnet && carnet.embarazoEnCurso) { + const controles = (carnet.controles || []).sort( + (a, b) => moment(b.fechaControl).valueOf() - moment(a.fechaControl).valueOf() + ); + const ultimoControl = controles.length > 0 ? controles[0] : null; + + if (ultimoControl && moment(ultimoControl.fechaControl).isAfter(moment().subtract(9, 'months'))) { + result.push({ + tipo: 'embarazo', + situacion: 'embarazo en curso', + fechaUltimoControl: ultimoControl.fechaControl, + profesional: ultimoControl.profesional, + idPrestacion: ultimoControl.idPrestacion, + organizacion: ultimoControl.organizacion + }); + } + } + + return result; +} + +/** + * Antecedentes personales: trastornos registrados en prestaciones validadas del paciente. + * Filtra por semanticTag === 'trastorno' en memoria (sin llamada a Snowstorm). + * Incluye registros anidados recursivamente. + */ +export async function antecedentesPersonales(pacienteID) { + const paciente = await PacienteCtr.findById(pacienteID); + if (!paciente) { + return null; + } + + const prestaciones: any[] = await Prestacion.find({ + 'paciente.id': { $in: paciente.vinculos }, + 'estadoActual.tipo': 'validada' + }); + + const result: any[] = []; + + for (const prestacion of prestaciones) { + const registros = registrosPorSemanticTag(prestacion.ejecucion.registros || [], 'trastorno'); + for (const registro of registros) { + result.push({ + concepto: registro.concepto, + fecha: prestacion.ejecucion.fecha, + profesional: prestacion.solicitud.profesional, + organizacion: prestacion.ejecucion.organizacion, + idPrestacion: prestacion._id, + tipoPrestacion: prestacion.solicitud.tipoPrestacion + }); + } + } + + return result; +} + +export async function antecedentesFamiliares(pacienteID) { + const paciente = await PacienteCtr.findById(pacienteID); + if (!paciente) { + return null; + } + + const expression = '<< 57177007'; + const conceptos = await SnomedCtr.getConceptByExpression(expression); + + if (!conceptos || conceptos.length === 0) { + return []; + } + + const prestaciones: any[] = await Prestacion.find({ + 'paciente.id': { $in: paciente.vinculos }, + 'estadoActual.tipo': 'validada' + }); + + return buscarEnHuds(prestaciones, conceptos); +} diff --git a/modules/huds/hudsSituaciones.routes.ts b/modules/huds/hudsSituaciones.routes.ts new file mode 100644 index 0000000000..af0aaf4dc0 --- /dev/null +++ b/modules/huds/hudsSituaciones.routes.ts @@ -0,0 +1,44 @@ +import * as express from 'express'; +import { Types } from 'mongoose'; +import { Auth } from '../../auth/auth.class'; +import { asyncHandler } from '@andes/api-tool'; +import { situacionesActivas, antecedentesPersonales, antecedentesFamiliares } from './hudsSituaciones.controller'; + +const router = express.Router(); + +router.use(Auth.authenticate()); + +router.get('/:idPaciente/situacionesActivas', asyncHandler(async (req: any, res) => { + if (!Types.ObjectId.isValid(req.params.idPaciente)) { + return res.status(404).send('Paciente no encontrado'); + } + const result = await situacionesActivas(req.params.idPaciente); + if (!result) { + return res.status(404).send('Paciente no encontrado'); + } + res.json(result); +})); + +router.get('/:idPaciente/antecedentesPersonales', asyncHandler(async (req: any, res) => { + if (!Types.ObjectId.isValid(req.params.idPaciente)) { + return res.status(404).send('Paciente no encontrado'); + } + const result = await antecedentesPersonales(req.params.idPaciente); + if (!result) { + return res.status(404).send('Paciente no encontrado'); + } + res.json(result); +})); + +router.get('/:idPaciente/AntecedentesFamiliares', asyncHandler(async (req: any, res) => { + if (!Types.ObjectId.isValid(req.params.idPaciente)) { + return res.status(404).send('Paciente no encontrado'); + } + const result = await antecedentesFamiliares(req.params.idPaciente); + if (!result) { + return res.status(404).send('Paciente no encontrado'); + } + res.json(result); +})); + +export const HudsSituacionesRouter = router; diff --git a/modules/huds/index.ts b/modules/huds/index.ts index 68f7974de2..25b272d577 100644 --- a/modules/huds/index.ts +++ b/modules/huds/index.ts @@ -1,2 +1,3 @@ export { HudsAccesosCtr } from './hudsAccesos.controller'; export { HudsAccesoRouter } from './hudsAccesos.routes'; +export { HudsSituacionesRouter } from './hudsSituaciones.routes'; diff --git a/modules/perinatal/schemas/carnet-perinatal.schema.ts b/modules/perinatal/schemas/carnet-perinatal.schema.ts index 988b53594c..92559c085b 100644 --- a/modules/perinatal/schemas/carnet-perinatal.schema.ts +++ b/modules/perinatal/schemas/carnet-perinatal.schema.ts @@ -1,6 +1,7 @@ import { Schema, Types, model } from 'mongoose'; import { AuditPlugin } from '@andes/mongoose-plugin-audit'; import { PacienteSubSchema } from '../../../core-v2/mpi/paciente/paciente.schema'; +import * as moment from 'moment'; export const CarnetPerinatalSchema = new Schema({ fecha: Date, @@ -38,6 +39,21 @@ export const CarnetPerinatalSchema = new Schema({ nota: String }); +CarnetPerinatalSchema.virtual('embarazoEnCurso').get(function () { + if (this.fechaFinEmbarazo) { + return false; + } + + const fechaRef = this.fechaUltimoControl || this.fecha; + + if (!fechaRef) { + return false; + } + + const diezMeses = moment().subtract(10, 'months').toDate(); + return moment(fechaRef).isAfter(diezMeses); +}); + CarnetPerinatalSchema.plugin(AuditPlugin); export const CarnetPerinatal = model('carnet-perinatal', CarnetPerinatalSchema, 'carnet-perinatal'); From 2dd96c050cab82e2ab6b2b722a4edcd14f1b1213 Mon Sep 17 00:00:00 2001 From: nicolasarana <90768149+nicolasarana@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:53:22 -0300 Subject: [PATCH 2/4] fix - de conceptos harcodeadas a buscar en la base de datos --- modules/huds/hudsSituaciones.controller.ts | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/modules/huds/hudsSituaciones.controller.ts b/modules/huds/hudsSituaciones.controller.ts index 9311ee37c7..1f74a8520e 100644 --- a/modules/huds/hudsSituaciones.controller.ts +++ b/modules/huds/hudsSituaciones.controller.ts @@ -5,8 +5,7 @@ import { CarnetPerinatal } from '../perinatal/schemas/carnet-perinatal.schema'; import { buscarEnHuds } from '../rup/controllers/rup'; import * as moment from 'moment'; -// SNOMED ECL para antecedentes familiares -const ECL_ANTECEDENTES_FAMILIARES = '<< 57177007'; +import { ECLQueries } from '../../core/tm/schemas/eclqueries.schema'; function registrosPorSemanticTag(registros, semanticTag) { const result = []; @@ -87,28 +86,28 @@ export async function antecedentesPersonales(pacienteID) { return null; } + const eclQuery = await ECLQueries.findOne({ key: 'antecedentes_personales' }); + const expression = eclQuery ? eclQuery.valor : '<< 312850006'; + const conceptos = await SnomedCtr.getConceptByExpression(expression); + + if (!conceptos || conceptos.length === 0) { + return []; + } + const prestaciones: any[] = await Prestacion.find({ 'paciente.id': { $in: paciente.vinculos }, 'estadoActual.tipo': 'validada' }); - const result: any[] = []; - - for (const prestacion of prestaciones) { - const registros = registrosPorSemanticTag(prestacion.ejecucion.registros || [], 'trastorno'); - for (const registro of registros) { - result.push({ - concepto: registro.concepto, - fecha: prestacion.ejecucion.fecha, - profesional: prestacion.solicitud.profesional, - organizacion: prestacion.ejecucion.organizacion, - idPrestacion: prestacion._id, - tipoPrestacion: prestacion.solicitud.tipoPrestacion - }); - } - } - - return result; + const results = buscarEnHuds(prestaciones, conceptos); + return results.map(item => ({ + concepto: item.registro.concepto, + fecha: item.fecha, + profesional: item.profesional, + organizacion: item.organizacion, + idPrestacion: item.idPrestacion, + tipoPrestacion: item.tipoPrestacion + })); } export async function antecedentesFamiliares(pacienteID) { @@ -117,7 +116,8 @@ export async function antecedentesFamiliares(pacienteID) { return null; } - const expression = '<< 57177007'; + const eclQuery = await ECLQueries.findOne({ key: 'antecedentes_familiares' }); + const expression = eclQuery ? eclQuery.valor : '<< 57177007'; const conceptos = await SnomedCtr.getConceptByExpression(expression); if (!conceptos || conceptos.length === 0) { From 2251772358d8f9a86b3447aab9efad9c4391c612 Mon Sep 17 00:00:00 2001 From: MarianoCampetella Date: Mon, 7 Sep 2026 15:08:19 -0300 Subject: [PATCH 3/4] correcciones 07/09 --- modules/huds/hudsSituaciones.controller.ts | 32 ++++++++-------------- modules/huds/hudsSituaciones.routes.ts | 2 +- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/modules/huds/hudsSituaciones.controller.ts b/modules/huds/hudsSituaciones.controller.ts index 1f74a8520e..6e6a3a2db1 100644 --- a/modules/huds/hudsSituaciones.controller.ts +++ b/modules/huds/hudsSituaciones.controller.ts @@ -36,15 +36,17 @@ export async function situacionesActivas(pacienteID) { for (const prestacion of prestaciones) { const registros = registrosPorSemanticTag(prestacion.ejecucion.registros || [], 'trastorno'); for (const registro of registros) { - result.push({ - tipo: 'trastorno', - concepto: registro.concepto, - fecha: prestacion.ejecucion.fecha, - profesional: prestacion.solicitud.profesional, - organizacion: prestacion.ejecucion.organizacion, - idPrestacion: prestacion._id, - tipoPrestacion: prestacion.solicitud.tipoPrestacion - }); + if (registro.valor?.estado === 'activo') { + result.push({ + tipo: 'trastorno', + concepto: registro.concepto, + fecha: prestacion.ejecucion.fecha, + profesional: prestacion.solicitud.profesional, + organizacion: prestacion.ejecucion.organizacion, + idPrestacion: prestacion._id, + tipoPrestacion: prestacion.solicitud.tipoPrestacion + }); + } } } @@ -89,7 +91,6 @@ export async function antecedentesPersonales(pacienteID) { const eclQuery = await ECLQueries.findOne({ key: 'antecedentes_personales' }); const expression = eclQuery ? eclQuery.valor : '<< 312850006'; const conceptos = await SnomedCtr.getConceptByExpression(expression); - if (!conceptos || conceptos.length === 0) { return []; } @@ -99,15 +100,7 @@ export async function antecedentesPersonales(pacienteID) { 'estadoActual.tipo': 'validada' }); - const results = buscarEnHuds(prestaciones, conceptos); - return results.map(item => ({ - concepto: item.registro.concepto, - fecha: item.fecha, - profesional: item.profesional, - organizacion: item.organizacion, - idPrestacion: item.idPrestacion, - tipoPrestacion: item.tipoPrestacion - })); + return buscarEnHuds(prestaciones, conceptos); } export async function antecedentesFamiliares(pacienteID) { @@ -119,7 +112,6 @@ export async function antecedentesFamiliares(pacienteID) { const eclQuery = await ECLQueries.findOne({ key: 'antecedentes_familiares' }); const expression = eclQuery ? eclQuery.valor : '<< 57177007'; const conceptos = await SnomedCtr.getConceptByExpression(expression); - if (!conceptos || conceptos.length === 0) { return []; } diff --git a/modules/huds/hudsSituaciones.routes.ts b/modules/huds/hudsSituaciones.routes.ts index af0aaf4dc0..0c0f298aca 100644 --- a/modules/huds/hudsSituaciones.routes.ts +++ b/modules/huds/hudsSituaciones.routes.ts @@ -30,7 +30,7 @@ router.get('/:idPaciente/antecedentesPersonales', asyncHandler(async (req: any, res.json(result); })); -router.get('/:idPaciente/AntecedentesFamiliares', asyncHandler(async (req: any, res) => { +router.get('/:idPaciente/antecedentesFamiliares', asyncHandler(async (req: any, res) => { if (!Types.ObjectId.isValid(req.params.idPaciente)) { return res.status(404).send('Paciente no encontrado'); } From f74a75ed45550692de36c19cf28a80ecb4871eef Mon Sep 17 00:00:00 2001 From: MarianoCampetella Date: Mon, 21 Sep 2026 09:36:08 -0300 Subject: [PATCH 4/4] Resumen del paciente: desarrollo front --- modules/huds/hudsSituaciones.controller.ts | 83 ++++++++++++++++++---- modules/huds/hudsSituaciones.routes.ts | 2 +- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/modules/huds/hudsSituaciones.controller.ts b/modules/huds/hudsSituaciones.controller.ts index 6e6a3a2db1..e1e01e7368 100644 --- a/modules/huds/hudsSituaciones.controller.ts +++ b/modules/huds/hudsSituaciones.controller.ts @@ -3,6 +3,7 @@ import { SnomedCtr } from '../../core/term/controller/snomed.controller'; import { Prestacion } from '../rup/schemas/prestacion'; import { CarnetPerinatal } from '../perinatal/schemas/carnet-perinatal.schema'; import { buscarEnHuds } from '../rup/controllers/rup'; +import { Auth } from '../../auth/auth.class'; import * as moment from 'moment'; import { ECLQueries } from '../../core/tm/schemas/eclqueries.schema'; @@ -20,36 +21,93 @@ function registrosPorSemanticTag(registros, semanticTag) { return result; } -export async function situacionesActivas(pacienteID) { +export async function situacionesActivas(pacienteID, req: any) { const paciente = await PacienteCtr.findById(pacienteID); if (!paciente) { return null; } - const result: any[] = []; - - const prestaciones: any[] = await Prestacion.find({ + const query: any = { 'paciente.id': { $in: paciente.vinculos }, 'estadoActual.tipo': 'validada' - }); + }; + + // Mismo criterio que /modules/rup/prestaciones cuando el usuario tiene huds:soloEfectorActual + if (Auth.check(req, 'huds:soloEfectorActual')) { + query['ejecucion.organizacion.id'] = Auth.getOrganization(req); + } + + const prestaciones: any[] = await Prestacion.find(query); + + // Replica el filtro de privacidad aplicado en /modules/rup/prestaciones + const profesional = Auth.getProfesional(req); + const profesionalId = profesional && profesional.id && profesional.id.toString(); + for (const prestacion of prestaciones) { + let profId = false; + if (prestacion.solicitud.profesional && prestacion.solicitud.profesional.id) { + profId = prestacion.solicitud.profesional.id.toString(); + } + const registros = prestacion.ejecucion.registros; + if (registros) { + for (let j = 0; j < registros.length; j++) { + const privacy = registros[j].privacy || { scope: 'public' }; + if (privacy.scope !== 'public' && profesionalId !== profId) { + switch (privacy.scope) { + case 'private': + registros.splice(j, 1); + j--; + break; + case 'termOnly': + registros[j].valor = 'El contenido de este registro sólo puede ser visualizado por el profesional que lo registró.'; + registros[j].registros = []; + break; + } + } + } + } + } + // Agrupamos por concepto.conceptId, igual que getConceptosByPaciente en el front + const grupos = new Map(); for (const prestacion of prestaciones) { const registros = registrosPorSemanticTag(prestacion.ejecucion.registros || [], 'trastorno'); for (const registro of registros) { - if (registro.valor?.estado === 'activo') { - result.push({ + const conceptId = registro.concepto.conceptId; + if (!grupos.has(conceptId)) { + grupos.set(conceptId, { tipo: 'trastorno', concepto: registro.concepto, - fecha: prestacion.ejecucion.fecha, - profesional: prestacion.solicitud.profesional, - organizacion: prestacion.ejecucion.organizacion, - idPrestacion: prestacion._id, - tipoPrestacion: prestacion.solicitud.tipoPrestacion + evoluciones: [] }); } + grupos.get(conceptId).evoluciones.push({ + idPrestacion: prestacion._id, + idRegistro: registro.id, + tipoPrestacion: prestacion.solicitud.tipoPrestacion?.term, + fechaCarga: prestacion.ejecucion.fecha, + profesional: registro.createdBy?.nombreCompleto, + organizacion: prestacion.ejecucion.organizacion?.nombre, + fechaInicio: registro.valor?.fechaInicio ?? null, + estado: registro.valor?.estado ?? '', + evolucion: registro.valor?.evolucion ?? '', + idRegistroOrigen: registro.valor?.idRegistroOrigen ?? null, + valor: registro.valor + }); + } + } + + const result: any[] = []; + for (const grupo of grupos.values()) { + grupo.evoluciones.sort((a, b) => moment(b.fechaCarga).valueOf() - moment(a.fechaCarga).valueOf()); + // Solo dejamos las situaciones cuya última evolución está activa + if (grupo.evoluciones[0].estado === 'activo') { + result.push(grupo); } } + // Ordenamos los grupos por la evolución más reciente (descendente) + result.sort((a, b) => moment(b.evoluciones[0].fechaCarga).valueOf() - moment(a.evoluciones[0].fechaCarga).valueOf()); + // Usar vinculos para cubrir pacientes con múltiples identificadores ANDES const carnet: any = await CarnetPerinatal.findOne({ 'paciente.id': { $in: paciente.vinculos }, @@ -120,6 +178,5 @@ export async function antecedentesFamiliares(pacienteID) { 'paciente.id': { $in: paciente.vinculos }, 'estadoActual.tipo': 'validada' }); - return buscarEnHuds(prestaciones, conceptos); } diff --git a/modules/huds/hudsSituaciones.routes.ts b/modules/huds/hudsSituaciones.routes.ts index 0c0f298aca..06bdb9be1d 100644 --- a/modules/huds/hudsSituaciones.routes.ts +++ b/modules/huds/hudsSituaciones.routes.ts @@ -12,7 +12,7 @@ router.get('/:idPaciente/situacionesActivas', asyncHandler(async (req: any, res) if (!Types.ObjectId.isValid(req.params.idPaciente)) { return res.status(404).send('Paciente no encontrado'); } - const result = await situacionesActivas(req.params.idPaciente); + const result = await situacionesActivas(req.params.idPaciente, req); if (!result) { return res.status(404).send('Paciente no encontrado'); }