From a563498bbc36855279ea7d4e65d64128b7ae55b9 Mon Sep 17 00:00:00 2001 From: aldoEMatamala <123381977+aldoEMatamala@users.noreply.github.com> Date: Tue, 12 May 2026 15:51:45 -0300 Subject: [PATCH 1/5] REC - Generar rutas para prescripciones de insumos (#2164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(REC-174): Generar rutas para prescripciones de insumos * REC-230 Agregar en la visualización de la HUDS las prescripciones de insumos --------- Co-authored-by: mcele <19591224+MCele@users.noreply.github.com> --- initialize.ts | 2 + modules/recetas/recetasInsumos/index.ts | 2 + .../recetasInsumos/receta-insumo.events.ts | 54 ++++ .../recetasInsumos/receta-insumo.routes.ts | 48 +++ .../recetasInsumos/receta-insumo.schema.ts | 168 +++++++++++ .../recetasInsumos/recetaInsumosController.ts | 273 ++++++++++++++++++ 6 files changed, 547 insertions(+) create mode 100644 modules/recetas/recetasInsumos/index.ts create mode 100644 modules/recetas/recetasInsumos/receta-insumo.events.ts create mode 100644 modules/recetas/recetasInsumos/receta-insumo.routes.ts create mode 100644 modules/recetas/recetasInsumos/receta-insumo.schema.ts create mode 100644 modules/recetas/recetasInsumos/recetaInsumosController.ts diff --git a/initialize.ts b/initialize.ts index 4fd5e32dae..66a5d6069e 100644 --- a/initialize.ts +++ b/initialize.ts @@ -123,6 +123,8 @@ export function initAPI(app: Express) { app.use('/api/modules', require('./modules/constantes').ConstantesRouter); app.use('/api/modules', require('./modules/recetas').RecetasRouter); app.use('/api/modules', require('./modules/insumos').InsumosRouter); + app.use('/api/modules', require('./modules/recetas/recetasInsumos').RecetaInsumoRouter); + if (configPrivate.hosts.BI_QUERY) { app.use( diff --git a/modules/recetas/recetasInsumos/index.ts b/modules/recetas/recetasInsumos/index.ts new file mode 100644 index 0000000000..6adda677dd --- /dev/null +++ b/modules/recetas/recetasInsumos/index.ts @@ -0,0 +1,2 @@ +import './receta-insumo.events'; +export { RecetaInsumoRouter } from './receta-insumo.routes'; diff --git a/modules/recetas/recetasInsumos/receta-insumo.events.ts b/modules/recetas/recetasInsumos/receta-insumo.events.ts new file mode 100644 index 0000000000..15b66b5a97 --- /dev/null +++ b/modules/recetas/recetasInsumos/receta-insumo.events.ts @@ -0,0 +1,54 @@ +import { EventCore } from '@andes/event-bus'; +import { crearRecetaInsumo } from '../../recetas/recetasInsumos/recetaInsumosController'; +import { getProfesionActualizada } from '../../recetas/recetasController'; +import * as moment from 'moment'; +import { RecetaInsumo } from './receta-insumo.schema'; +import { createLog, informarLog, updateLog, jobsLog } from './../recetaLogs'; + +EventCore.on('prestacion:recetaInsumo:create', async ({ prestacion, registro }) => { + const idRegistro = registro._id; + const profPrestacion = prestacion.solicitud.profesional; + const { profesionGrado, matriculaGrado, especialidades } = await getProfesionActualizada(profPrestacion.id); + const profesional = { + id: profPrestacion.id, + nombre: profPrestacion.nombre, + apellido: profPrestacion.apellido, + documento: profPrestacion.documento, + profesion: profesionGrado, + especialidad: especialidades, + matricula: matriculaGrado + }; + + const organizacion = { + id: prestacion.ejecucion.organizacion.id, + nombre: prestacion.ejecucion.organizacion.nombre + }; + + const dataReceta = { + idPrestacion: prestacion.id, + idRegistro, + fechaRegistro: prestacion.ejecucion.fecha || moment().toDate(), + fechaPrestacion: prestacion.ejecucion.fecha, + paciente: prestacion.paciente, + profesional, + organizacion, + insumo: null, + diagnostico: null, + }; + try { + for (const insumo of registro.valor.insumos) { + const receta: any = await RecetaInsumo.findOne({ + 'insumo.id': insumo.generico.id, + idRegistro + }); + if (!receta) { + dataReceta.insumo = insumo; + await crearRecetaInsumo(dataReceta, prestacion.createdBy); // falta return + } + + } + } catch (err) { + createLog.error('create', { dataReceta, prestacion, profesional }, err, { prestacion, registro }); + return err; + } +}); diff --git a/modules/recetas/recetasInsumos/receta-insumo.routes.ts b/modules/recetas/recetasInsumos/receta-insumo.routes.ts new file mode 100644 index 0000000000..fc56d49980 --- /dev/null +++ b/modules/recetas/recetasInsumos/receta-insumo.routes.ts @@ -0,0 +1,48 @@ +import { MongoQuery, ResourceBase } from '@andes/core'; +import { Auth } from '../../../auth/auth.class'; +import { RecetaInsumo } from './receta-insumo.schema'; +import { asyncHandler, Request, Response } from '@andes/api-tool'; +import { create, buscarRecetasInsumos } from './recetaInsumosController'; + +class RecetaInsumoResource extends ResourceBase { + Model = RecetaInsumo; + resourceName = 'recetaInsumos'; + routesEnable = ['get, post']; + middlewares = [Auth.authenticate()]; + searchFileds = { + paciente: { + field: 'paciente.id', + fn: MongoQuery.equalMatch + }, + documento: { + field: 'paciente.documento', + fn: MongoQuery.equalMatch + } + }; +} + +export const post = async (req, res) => { + const resp = await create(req); + const status = resp?.status || resp?.errors || 200; + res.status(status).json(resp); +}; +export const get = async (req, res) => { + const result = await buscarRecetasInsumos(req); + res.json(result); +}; +export const RecetaInsumoCtr = new RecetaInsumoResource({}); +export const RecetaInsumoRouter = RecetaInsumoCtr.makeRoutes(); + +const authorizeByToken = async (req: Request, res: Response, next) => + Auth.authorizeByToken(req, res, next, [ + 'huds:visualizacionHuds', + 'huds:visualizacionParcialHuds:laboratorio', + 'huds:visualizacionParcialHuds:vacuna', + 'huds:visualizacionParcialHuds:receta', + 'huds:visualizacionParcialHuds:*', + 'recetas:read' + ]); + +RecetaInsumoRouter.use(Auth.authenticate()); +RecetaInsumoRouter.get('/recetasInsumos', authorizeByToken, asyncHandler(get)); +RecetaInsumoRouter.post('/recetasInsumos', authorizeByToken, asyncHandler(post)); diff --git a/modules/recetas/recetasInsumos/receta-insumo.schema.ts b/modules/recetas/recetasInsumos/receta-insumo.schema.ts new file mode 100644 index 0000000000..2d6af10eef --- /dev/null +++ b/modules/recetas/recetasInsumos/receta-insumo.schema.ts @@ -0,0 +1,168 @@ +import { AuditPlugin } from '@andes/mongoose-plugin-audit'; +import * as mongoose from 'mongoose'; +import { ProfesionalSubSchema } from '../../../core/tm/schemas/profesional'; +import { PacienteSubSchema } from '../../../core-v2/mpi/paciente/paciente.schema'; +const insumoSubSchema = new mongoose.Schema({ + id: String, + nombre: String, + codigo: [ + { + fuente: String, + valor: String + } + ], + + tipo: { + type: String, + enum: ['dispositivo', 'nutricion', 'magistral'] + }, + cantidad: Number, + unidades: String, + tratamientoProlongado: Boolean, + tiempoTratamiento: mongoose.SchemaTypes.Mixed, + ordenTratamiento: Number, + especificacion: String +}, { _id: false }); + +const cancelarSchema = new mongoose.Schema({ + idDispensaApp: { + type: String, + required: false + }, + motivo: { + type: String, + required: false + }, + organizacion: { + id: String, + nombre: String + } +}); +const sistemaSchema = { + type: String, + enum: ['sifaho', 'recetar'] +}; +const estadoDispensaSchema = new mongoose.Schema({ + tipo: { + type: String, + enum: ['sin-dispensa', 'dispensada', 'dispensa-parcial'], + required: true, + default: 'sin-dispensa' + }, + idDispensaApp: { + type: String, + required: false + }, + fecha: Date, + sistema: sistemaSchema, + cancelada: { + type: cancelarSchema, + required: false + } +}); +const estadosSchema = new mongoose.Schema({ + tipo: { + type: String, + enum: ['pendiente', 'vigente', 'finalizada', 'vencida', 'suspendida', 'rechazada'], + required: true, + default: 'vigente' + }, + motivo: { + type: String, + required: false + }, + observacion: { + type: String, + required: false + }, + profesional: { + type: ProfesionalSubSchema, + required: false + }, + organizacionExterna: { + id: { + type: String, + required: false + }, + nombre: { + type: String, + required: false + } + } +}); + + +export const recetaInsumoSchema = new mongoose.Schema({ + organizacion: { + id: mongoose.SchemaTypes.ObjectId, + nombre: String + }, + profesional: { + id: mongoose.SchemaTypes.ObjectId, + nombre: String, + apellido: String, + documento: String, + profesion: String, + matricula: Number, + especialidad: String, + }, + fechaRegistro: Date, + fechaPrestacion: Date, + idPrestacion: String, + idRegistro: String, + diagnostico: mongoose.SchemaTypes.Mixed, + insumo: { type: insumoSubSchema, required: true }, + dispensa: [ + { + idDispensaApp: String, + fecha: Date, + insumos: [{ + cantidad: Number, + descripcion: String, + insumo: mongoose.SchemaTypes.Mixed, + cantidadEnvases: Number, + observacion: { + type: String, + required: false + } + }], + organizacion: { + id: String, + nombre: String + }, + } + ], + estados: [estadosSchema], + estadoActual: estadosSchema, + estadosDispensa: [estadoDispensaSchema], + estadoDispensaActual: estadoDispensaSchema, + paciente: PacienteSubSchema, + renovacion: String, + appNotificada: [{ app: sistemaSchema, fecha: Date }], + origenExterno: { + id: String, // id receta creada por sistema que no es Andes + app: sistemaSchema, + fecha: Date + } +}); + +recetaInsumoSchema.pre('save', function (next) { + const recetaInsumo: any = this; + + if (recetaInsumo.estados && recetaInsumo.estados.length > 0) { + recetaInsumo.estadoActual = recetaInsumo.estados[recetaInsumo.estados.length - 1]; + } + if (recetaInsumo.estadosDispensa && recetaInsumo.estadosDispensa.length > 0) { + recetaInsumo.estadoDispensaActual = recetaInsumo.estadosDispensa[recetaInsumo.estadosDispensa.length - 1]; + } + + next(); +}); + +recetaInsumoSchema.plugin(AuditPlugin); + +recetaInsumoSchema.index({ + idPrestacion: 1, +}); + +export const RecetaInsumo = mongoose.model('recetasInsumo', recetaInsumoSchema, 'recetasInsumo'); diff --git a/modules/recetas/recetasInsumos/recetaInsumosController.ts b/modules/recetas/recetasInsumos/recetaInsumosController.ts new file mode 100644 index 0000000000..051e0577c6 --- /dev/null +++ b/modules/recetas/recetasInsumos/recetaInsumosController.ts @@ -0,0 +1,273 @@ +import * as moment from 'moment'; +import { Types } from 'mongoose'; +import { Auth } from '../../../auth/auth.class'; +import { RecetaInsumo } from './receta-insumo.schema'; +import { createLog, informarLog, updateLog } from '../recetaLogs'; +import { ParamsIncorrect, RecetaNotFound } from '../recetas.error'; +import { Paciente } from '../../../core-v2/mpi/paciente/paciente.schema'; +import { Profesional } from '../../../core/tm/schemas/profesional'; +import { getProfesionActualizada } from '../recetasController'; + +export async function buscarRecetasInsumos(req) { + const options: any = {}; + const params = req.params.id ? req.params : req.query; + const pacienteId = params.pacienteId || null; + const documento = params.documento || null; + const sexo = params.sexo || null; + try { + if ((!pacienteId && (!documento || !sexo)) || (pacienteId && !Types.ObjectId.isValid(pacienteId))) { + throw new ParamsIncorrect(); + } + const paramMap = { + id: '_id', + pacienteId: 'paciente.id', + documento: 'paciente.documento', + sexo: 'paciente.sexo', + estado: 'estadoActual.tipo' + }; + Object.keys(paramMap).forEach(key => { + if (params[key]) { + options[paramMap[key]] = key === 'id' ? Types.ObjectId(params[key]) : params[key]; + } + }); + + if (params.estadoDispensa) { + const estadoDispensaArray = params.estadoDispensa.split(','); + options['estadoDispensaActual.tipo'] = { $in: estadoDispensaArray }; + } else { + options['estadoDispensaActual.tipo'] = 'sin-dispensa'; + } + + if (params.fechaInicio || params.fechaFin) { + const fechaInicio = params.fechaInicio ? moment(params.fechaInicio).startOf('day').toDate() : moment().subtract(1, 'years').startOf('day').toDate(); + const fechaFin = params.fechaFin ? moment(params.fechaFin).endOf('day').toDate() : moment().endOf('day').toDate(); + options['fechaRegistro'] = { $gte: fechaInicio, $lte: fechaFin }; + } + if (Object.keys(options).length === 0) { + throw new ParamsIncorrect(); + } + const recetasInsumos: any = await RecetaInsumo.find(options); + return recetasInsumos; + } catch (err) { + await informarLog.error('buscarRecetasInsumos', { params, options }, err, req); + return err; + } +} + +export async function create(req) { + const pacienteRecetar = req.body.paciente; + const profRecetar = req.body.profesional; + const dataRecetaInsumo = { + insumo: req.body.insumo, + idPrestacion: req.body.idPrestacion, + idRegistro: req.body.idRegistro || req.body.idPrestacion, + fechaRegistro: null, + fechaPrestacion: null, + paciente: null, + profesional: null, + organizacion: req.body.organizacion, + diagnostico: null, + origenExterno: null + }; + try { + dataRecetaInsumo.fechaRegistro = dataRecetaInsumo.fechaRegistro ? moment(dataRecetaInsumo.fechaRegistro).toDate() : moment().toDate(); + dataRecetaInsumo.fechaPrestacion = dataRecetaInsumo.fechaPrestacion ? dataRecetaInsumo.fechaPrestacion : dataRecetaInsumo.fechaRegistro; + const insumoIncompleto = !req.body.insumo || (!req.body.insumo.concepto?.conceptId && !req.body.insumo.generico?.id); + dataRecetaInsumo.origenExterno = { + id: req.body.origenExterno?.id || '', + app: req.user.app?.nombre.toLowerCase() || '', + fecha: req.body.origenExterno?.fecha ? new Date(req.body.origenExterno.fecha) : dataRecetaInsumo.fechaRegistro, + }; + if (insumoIncompleto) { + throw new ParamsIncorrect('Faltan datos del insumo'); + } else { + const query: any = { + idRegistro: dataRecetaInsumo.idRegistro + }; + if (dataRecetaInsumo.insumo.generico) { + query['insumo.insumo'] = dataRecetaInsumo.insumo.generico.insumo; + } else { + query['insumo.concepto.conceptId'] = dataRecetaInsumo.insumo.concepto.conceptId; + } + const recetaInsumo = await RecetaInsumo.findOne(query); + if (recetaInsumo) { + throw new ParamsIncorrect('Receta de insumo ya registrada'); + } + } + if (!req.body.idPrestacion || !dataRecetaInsumo.organizacion) { + throw new ParamsIncorrect('Faltan datos de la receta de insumo'); + } + if (!pacienteRecetar || !pacienteRecetar.id) { + throw new ParamsIncorrect('Faltan datos del paciente'); + } else { + const pacienteAndes: any = await Paciente.findById(pacienteRecetar.id); + if (!pacienteAndes) { + throw new ParamsIncorrect('Paciente no encontrado'); + } else { + pacienteAndes.obraSocial = (!pacienteRecetar.obraSocial) ? null : + { + origen: pacienteRecetar.obraSocial.otraOS ? 'RECETAR' : 'PUCO', + nombre: pacienteRecetar.obraSocial.nombre, + financiador: pacienteRecetar.obraSocial.nombre, + codigoPuco: pacienteRecetar.obraSocial.codigoPuco || null, + numeroAfiliado: pacienteRecetar.obraSocial.numeroAfiliado || null + }; + } + dataRecetaInsumo.paciente = pacienteAndes; + } + if (!profRecetar || !profRecetar.id) { + throw new ParamsIncorrect('Faltan datos del profesional'); + } else { + const profAndes = await Profesional.findById(profRecetar.id); + if (!profAndes) { + throw new ParamsIncorrect('Profesional no encontrado'); + } + const { profesionGrado, matriculaGrado, especialidades } = await getProfesionActualizada(profRecetar.id); + dataRecetaInsumo.profesional = { + _id: profAndes._id, + id: profAndes._id, + nombre: profAndes.nombre, + apellido: profAndes.apellido, + documento: profAndes.documento, + profesion: profesionGrado, + especialidad: especialidades, + matricula: matriculaGrado + }; + } + return await crearRecetaInsumo(dataRecetaInsumo, req); + } catch (err) { + createLog.error('create', { dataRecetaInsumo, pacienteRecetar, profRecetar }, err, req); + return err; + } +} + +export async function crearRecetaInsumo(dataRecetaInsumo, req) { + const insumo = dataRecetaInsumo.insumo; + const tratamientoProlongado: Boolean = insumo.tratamientoProlongado && insumo.tiempoTratamiento && insumo.tiempoTratamiento.id !== null; + const cantRecetas = (tratamientoProlongado) ? parseInt(insumo.tiempoTratamiento.id, 10) : 1; + const recetas = []; + let recetaInsumo; + for (let i = 0; i < cantRecetas; i++) { + try { + recetaInsumo = new RecetaInsumo(); + recetaInsumo.idPrestacion = dataRecetaInsumo.idPrestacion; + recetaInsumo.idRegistro = dataRecetaInsumo.idRegistro; + const diag = insumo?.diagnostico; + recetaInsumo.diagnostico = (typeof diag === 'string') ? { descripcion: diag } : diag; + if (insumo.generico) { + recetaInsumo.insumo = { + id: insumo.generico.id, + nombre: insumo.generico.nombre, + codigo: insumo.generico.codigo, + tipo: insumo.generico.tipo, + tratamientoProlongado, + tiempoTratamiento: tratamientoProlongado ? insumo.tiempoTratamiento : null, + ordenTratamiento: i, + cantidad: insumo.cantidad, + especificacion: insumo.especificacion + }; + } else { + recetaInsumo.insumo = { + ...insumo, + tratamientoProlongado, + tiempoTratamiento: tratamientoProlongado ? insumo.tiempoTratamiento : null, + ordenTratamiento: i + }; + } + recetaInsumo.estados = i < 1 ? [{ tipo: 'vigente' }] : [{ tipo: 'pendiente' }]; + recetaInsumo.estadosDispensa = [{ tipo: 'sin-dispensa', fecha: moment().toDate() }]; + recetaInsumo.paciente = dataRecetaInsumo.paciente; + recetaInsumo.paciente.obraSocial = dataRecetaInsumo.paciente.obraSocial; + recetaInsumo.paciente.id = dataRecetaInsumo.paciente.id || dataRecetaInsumo.paciente._id; + recetaInsumo.profesional = dataRecetaInsumo.profesional; + recetaInsumo.profesional._id = dataRecetaInsumo.profesional.id || dataRecetaInsumo.profesional._id; + recetaInsumo.organizacion = dataRecetaInsumo.organizacion; + recetaInsumo.fechaRegistro = moment(dataRecetaInsumo.fechaRegistro).add(i * 30, 'days').toDate(); + recetaInsumo.fechaPrestacion = moment(dataRecetaInsumo.fechaPrestacion).toDate(); + if (dataRecetaInsumo.origenExterno) { + recetaInsumo.origenExterno = dataRecetaInsumo.origenExterno; + } + if (req.user) { + Auth.audit(recetaInsumo, req as any); + } else { + recetaInsumo.audit(req); + } + await recetaInsumo.save(); + recetas.push(recetaInsumo); + } catch (err) { + createLog.error('crearRecetaInsumo', { dataRecetaInsumo }, err, req); + return err; + } + } + return recetas; +} + +export async function buscarRecetasInsumosPorProfesional(req) { + try { + const profesionalId = req.params.id; + const { estadoReceta, desde, hasta, origenExternoApp, excluirEstado } = req.query; + if (!profesionalId || !Types.ObjectId.isValid(profesionalId)) { + throw new ParamsIncorrect(); + } + const filter: any = { + 'profesional.id': Types.ObjectId(profesionalId) + }; + if (estadoReceta) { + filter['estadoActual.tipo'] = estadoReceta; + } + if (desde || hasta) { + filter['fechaRegistro'] = {}; + if (desde) { + filter['fechaRegistro'].$gte = moment(desde).startOf('day').toDate(); + } + if (hasta) { + filter['fechaRegistro'].$lte = moment(hasta).endOf('day').toDate(); + } + } + if (origenExternoApp) { + filter['origenExterno.app'] = origenExternoApp; + } + if (excluirEstado) { + filter['estadoActual.tipo'] = { $ne: excluirEstado }; + } + const recetasInsumos = await RecetaInsumo.find(filter); + return recetasInsumos; + } catch (err) { + await informarLog.error('buscarRecetasInsumosPorProfesional', { params: req.params, query: req.query }, err); + return err; + } +} + +export async function suspender(recetaInsumoId, req) { + const motivo = req.body.motivo; + const observacion = req.body.observacion; + const profesional = req.body.profesional; + try { + const recetaInsumo: any = await RecetaInsumo.findById(recetaInsumoId); + if (!recetaInsumo) { + throw new RecetaNotFound(); + } + const recetasASuspender = await RecetaInsumo.find({ + 'insumo.concepto.conceptId': recetaInsumo.insumo.concepto.conceptId, + idRegistro: recetaInsumo.idRegistro + }); + const promises = recetasASuspender.map(async (receta: any) => { + if ((receta.estadoActual.tipo === 'vigente') || (receta.estadoDispensaActual.tipo !== 'dispensa-parcial' && receta.estadoActual.tipo === 'pendiente')) { + receta.estados.push({ + tipo: 'suspendida', + motivo, + observacion, + profesional, + fecha: new Date() + }); + Auth.audit(receta, req); + await receta.save(); + } + }); + await Promise.all(promises); + return { success: true }; + } catch (error) { + await updateLog.error('suspender', { motivo, observacion, profesional, recetaInsumoId }, error); + return error; + } +} From e27f22814b1e988aca05512eee95122254bf229f Mon Sep 17 00:00:00 2001 From: ma7payne Date: Wed, 13 May 2026 10:33:26 -0300 Subject: [PATCH 2/5] =?UTF-8?q?REC-178:=20Corrige=20ruta=20de=20colecci?= =?UTF-8?q?=C3=B3n=20de=20insumos=20(#2208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(REC-174): Generar rutas para prescripciones de insumos * feat(REC): corrige ruta de coleccion de insumos --------- Co-authored-by: aldoEMatamala --- modules/insumos/insumos-schema.ts | 1 - modules/insumos/insumos.routes.ts | 12 +++++++++--- .../recetas/recetasInsumos/receta-insumo.events.ts | 3 ++- .../recetas/recetasInsumos/receta-insumo.schema.ts | 1 - 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/modules/insumos/insumos-schema.ts b/modules/insumos/insumos-schema.ts index f8fd5c0532..7e550b7740 100644 --- a/modules/insumos/insumos-schema.ts +++ b/modules/insumos/insumos-schema.ts @@ -1,7 +1,6 @@ import * as mongoose from 'mongoose'; import { AuditPlugin } from '@andes/mongoose-plugin-audit'; - export const insumoSchema = new mongoose.Schema({ nombre: String, codigo: [{ diff --git a/modules/insumos/insumos.routes.ts b/modules/insumos/insumos.routes.ts index 72a6ccb354..c8e88a9bc4 100644 --- a/modules/insumos/insumos.routes.ts +++ b/modules/insumos/insumos.routes.ts @@ -7,9 +7,15 @@ class InsumosResource extends ResourceBase { resourceName = 'insumos'; middlewares = [Auth.authenticate()]; searchFileds = { - nombre: MongoQuery.partialString, - 'codigo.valor': MongoQuery.partialString, - tipo: MongoQuery.equalMatch, + nombre: (value: any) => { + if (value && value.charAt(0) === '^') { + const searchPattern = value.substring(1); + const escaped = searchPattern.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&'); + return { $regex: escaped, $options: 'i' }; + } + return value; + }, + tipo: MongoQuery.inArray, estado: MongoQuery.equalMatch, requiereEspecificacion: MongoQuery.equalMatch, }; diff --git a/modules/recetas/recetasInsumos/receta-insumo.events.ts b/modules/recetas/recetasInsumos/receta-insumo.events.ts index 15b66b5a97..9ae7d5a046 100644 --- a/modules/recetas/recetasInsumos/receta-insumo.events.ts +++ b/modules/recetas/recetasInsumos/receta-insumo.events.ts @@ -3,7 +3,7 @@ import { crearRecetaInsumo } from '../../recetas/recetasInsumos/recetaInsumosCon import { getProfesionActualizada } from '../../recetas/recetasController'; import * as moment from 'moment'; import { RecetaInsumo } from './receta-insumo.schema'; -import { createLog, informarLog, updateLog, jobsLog } from './../recetaLogs'; +import { createLog } from './../recetaLogs'; EventCore.on('prestacion:recetaInsumo:create', async ({ prestacion, registro }) => { const idRegistro = registro._id; @@ -39,6 +39,7 @@ EventCore.on('prestacion:recetaInsumo:create', async ({ prestacion, registro }) for (const insumo of registro.valor.insumos) { const receta: any = await RecetaInsumo.findOne({ 'insumo.id': insumo.generico.id, + 'insumo.nombre': insumo.generico.nombre, idRegistro }); if (!receta) { diff --git a/modules/recetas/recetasInsumos/receta-insumo.schema.ts b/modules/recetas/recetasInsumos/receta-insumo.schema.ts index 2d6af10eef..fa6d1598d7 100644 --- a/modules/recetas/recetasInsumos/receta-insumo.schema.ts +++ b/modules/recetas/recetasInsumos/receta-insumo.schema.ts @@ -11,7 +11,6 @@ const insumoSubSchema = new mongoose.Schema({ valor: String } ], - tipo: { type: String, enum: ['dispositivo', 'nutricion', 'magistral'] From c7699ecbb35e78593017ddb5b10a217a5fcf957e Mon Sep 17 00:00:00 2001 From: ma7payne Date: Wed, 13 May 2026 12:10:57 -0300 Subject: [PATCH 3/5] feat(REC): Implementa filtros para listado de recetas (#2144) --- modules/recetas/recetas.routes.ts | 8 ++- modules/recetas/recetasController.ts | 103 ++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/modules/recetas/recetas.routes.ts b/modules/recetas/recetas.routes.ts index 61a4dba8a5..b58128df87 100644 --- a/modules/recetas/recetas.routes.ts +++ b/modules/recetas/recetas.routes.ts @@ -2,7 +2,7 @@ import { asyncHandler, Request, Response } from '@andes/api-tool'; import { MongoQuery, ResourceBase } from '@andes/core'; import { Auth } from '../../auth/auth.class'; import { Receta } from './receta-schema'; -import { buscarRecetas, getMotivosReceta, setEstadoDispensa, suspender, actualizarAppNotificada, cancelarDispensa, create, buscarRecetasPorProfesional } from './recetasController'; +import { buscarRecetas, getMotivosReceta, setEstadoDispensa, suspender, actualizarAppNotificada, cancelarDispensa, create, buscarRecetasPorProfesional, buscarRecetasConFiltros } from './recetasController'; import { ParamsIncorrect } from './recetas.error'; class RecetasResource extends ResourceBase { @@ -41,6 +41,11 @@ export const getByProfesional = async (req, res) => { res.json(result); }; +export const getConFiltros = async (req, res) => { + const result = await buscarRecetasConFiltros(req); + res.json(result); +}; + export const patch = async (req, res) => { const operacion = req.body.op ? req.body.op.toLowerCase() : ''; let result, status; @@ -96,5 +101,6 @@ RecetasRouter.use(Auth.authenticate()); RecetasRouter.get('/recetas', authorizeByToken, asyncHandler(get)); RecetasRouter.get('/recetas/motivos', asyncHandler(getMotivos)); RecetasRouter.get('/recetas/profesional/:id', authorizeByToken,asyncHandler(getByProfesional)); +RecetasRouter.get('/recetas/filtros', authorizeByToken, asyncHandler(getConFiltros)); RecetasRouter.patch('/recetas', authorizeByToken, asyncHandler(patch)); RecetasRouter.post('/recetas', authorizeByToken, asyncHandler(post)); diff --git a/modules/recetas/recetasController.ts b/modules/recetas/recetasController.ts index f6f61df783..d21576ed04 100644 --- a/modules/recetas/recetasController.ts +++ b/modules/recetas/recetasController.ts @@ -158,7 +158,7 @@ export async function buscarRecetas(req) { const estadoDispensaArray = params.estadoDispensa.replace(/ /g, '').split(','); options['estadoDispensaActual.tipo'] = { $in: estadoDispensaArray }; } else { - options['estadoDispensaActual.tipo'] = 'sin-dispensa'; + options['estadoActual.tipo'] = null; } const estadoArray = params.estado ? params.estado.replace(/ /g, '').split(',') : []; const fechaFin = params.fechaFin ? moment(params.fechaFin).endOf('day').toDate() : moment().endOf('day').toDate(); @@ -197,6 +197,7 @@ export async function buscarRecetas(req) { } } let recetas: any = await Receta.find(options); + if (!recetas.length) { return []; } @@ -221,6 +222,100 @@ export async function buscarRecetas(req) { return recetas; } catch (err) { await informarLog.error('buscarRecetas', { params, options }, err, req); + + return err; + } +} + +/** + * Busca recetas filtrando por rango de fechas y estado. + * Incluye estados de receta y de dispensa simultáneamente. + * + * Parámetros (req.query): + * - fechaInicio: fecha inicial del rango (opcional) + * - fechaFin: fecha final del rango (opcional) + * - estado: uno o varios estados separados por coma (opcional) + * + * Ejemplo de estados: 'pendiente,vigente,vencida,sin-dispensa,dispensada,dispensa-parcial' + */ +export async function buscarRecetasConFiltros(req) { + try { + const { fechaInicio, fechaFin, estado, documento, sexo } = req.query; + const filter: any = {}; + const statusVal = req.query.status; + const estadoVal = estado; + let estadoParam = null; + + if (estadoVal && statusVal) { + estadoParam = `${estadoVal},${statusVal}`; + } else { + estadoParam = estadoVal ?? statusVal; + } + if (!estadoParam || String(estadoParam).trim() === '') { + estadoParam = 'vigente'; + } + + // Validación mínima: al menos un filtro + if (!documento && !sexo && !estadoParam) { + throw new ParamsIncorrect(); + } + + // Filtro por rango de fechas sobre fechaRegistro + if (fechaInicio || fechaFin) { + filter['fechaRegistro'] = {}; + if (fechaInicio) { + filter['fechaRegistro'].$gte = moment(fechaInicio, 'DD-MM-YYYY').startOf('day').toDate(); + } + if (fechaFin) { + filter['fechaRegistro'].$lte = moment(fechaFin, 'DD-MM-YYYY').endOf('day').toDate(); + } + } else { + // Default: último mes hasta hoy si no se especifican fechas + filter['fechaRegistro'] = { + $gte: moment().subtract(1, 'months').startOf('day').toDate(), + $lte: moment().endOf('day').toDate() + }; + } + + // Filtro por estado aplicado tanto a receta como a dispensa + if (estadoParam) { + const estadosRaw = String(estadoParam).replace(/ /g, '').split(',').filter(Boolean); + const incluyeTodas = estadosRaw.some(e => e.toLowerCase() === 'todas'); + // Si llega "todas" (solo o incluido), no aplicar filtro de estado + if (!incluyeTodas) { + const estados = estadosRaw; // ya normalizado + if (estados.length) { + const or: any[] = []; + if (estados.length === 1) { + const val = estados[0]; + or.push({ 'estadoActual.tipo': val }); + or.push({ 'estadoDispensaActual.tipo': val }); + } else { + estados.forEach(val => { + or.push({ 'estadoActual.tipo': val }); + or.push({ 'estadoDispensaActual.tipo': val }); + }); + } + filter['$or'] = or; + } + } else { + // Forzar que se tome cualquier estadoActual.tipo + filter['estadoActual.tipo'] = { $exists: true }; + } + } + + // Filtros por datos del paciente + if (documento) { + filter['paciente.documento'] = documento; + } + if (sexo) { + filter['paciente.sexo'] = sexo; + } + + const recetas = await Receta.find(filter); + return recetas; + } catch (err) { + await informarLog.error('buscarRecetasConFiltros', { query: req.query }, err, req); return err; } } @@ -682,8 +777,12 @@ export async function buscarRecetasPorProfesional(req) { filter['origenExterno.app'] = origenExternoApp; } if (excluirEstado) { - filter['estadoActual.tipo'] = { $nin: excluirEstado }; + const estados = typeof excluirEstado === 'string' + ? excluirEstado.split(',').map((e: string) => e.trim()) + : Array.isArray(excluirEstado) ? excluirEstado : [excluirEstado]; + filter['estadoActual.tipo'] = { $nin: estados }; } + const recetas = await Receta.find(filter); return recetas; } catch (err) { From c2847fa6dcf62b1e7ee99dd89e1453c3616de4c9 Mon Sep 17 00:00:00 2001 From: ma7payne Date: Thu, 14 May 2026 14:59:03 -0300 Subject: [PATCH 4/5] feat(REC): implementa verificacion de recetas (#2233) --- modules/recetas/recetas.routes.ts | 20 +++++++++++-- modules/recetas/recetasController.ts | 45 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/modules/recetas/recetas.routes.ts b/modules/recetas/recetas.routes.ts index b58128df87..1819560713 100644 --- a/modules/recetas/recetas.routes.ts +++ b/modules/recetas/recetas.routes.ts @@ -2,7 +2,7 @@ import { asyncHandler, Request, Response } from '@andes/api-tool'; import { MongoQuery, ResourceBase } from '@andes/core'; import { Auth } from '../../auth/auth.class'; import { Receta } from './receta-schema'; -import { buscarRecetas, getMotivosReceta, setEstadoDispensa, suspender, actualizarAppNotificada, cancelarDispensa, create, buscarRecetasPorProfesional, buscarRecetasConFiltros } from './recetasController'; +import { buscarRecetas, getMotivosReceta, setEstadoDispensa, suspender, actualizarAppNotificada, cancelarDispensa, create, buscarRecetasPorProfesional, buscarRecetasConFiltros, verificarRecetaExistente } from './recetasController'; import { ParamsIncorrect } from './recetas.error'; class RecetasResource extends ResourceBase { @@ -46,6 +46,21 @@ export const getConFiltros = async (req, res) => { res.json(result); }; +export const getVerificarReceta = async (req, res) => { + try { + const { documento, sexo, conceptId } = req.query; + if (!documento || !sexo) { + const error = new ParamsIncorrect('Se requieren los parámetros documento y sexo'); + return res.status(error.status).json(error); + } + const result = await verificarRecetaExistente(documento as string, sexo as string, conceptId as string); + res.json(result); + } catch (err) { + const status = (err as any).status || 400; + res.status(status).json(err); + } +}; + export const patch = async (req, res) => { const operacion = req.body.op ? req.body.op.toLowerCase() : ''; let result, status; @@ -100,7 +115,8 @@ const authorizeByToken = async (req: Request, res: Response, next) => RecetasRouter.use(Auth.authenticate()); RecetasRouter.get('/recetas', authorizeByToken, asyncHandler(get)); RecetasRouter.get('/recetas/motivos', asyncHandler(getMotivos)); -RecetasRouter.get('/recetas/profesional/:id', authorizeByToken,asyncHandler(getByProfesional)); RecetasRouter.get('/recetas/filtros', authorizeByToken, asyncHandler(getConFiltros)); +RecetasRouter.get('/recetas/profesional/:id', authorizeByToken, asyncHandler(getByProfesional)); +RecetasRouter.get('/recetas/verificar', authorizeByToken, asyncHandler(getVerificarReceta)); RecetasRouter.patch('/recetas', authorizeByToken, asyncHandler(patch)); RecetasRouter.post('/recetas', authorizeByToken, asyncHandler(post)); diff --git a/modules/recetas/recetasController.ts b/modules/recetas/recetasController.ts index d21576ed04..3351af3738 100644 --- a/modules/recetas/recetasController.ts +++ b/modules/recetas/recetasController.ts @@ -928,3 +928,48 @@ export async function actualizarEstadosDispensa() { await informarLog.error('actualizarEstadosDispensa', {}, error); } } + +/** + * Verifica si existe una receta vigente o pendiente para un paciente y un medicamento (por conceptId SNOMED) + * al momento en que se realiza la consulta. + * + * @param documento - DNI del paciente (identificador común entre sistemas) + * @param sexo - Sexo del paciente ('masculino' | 'femenino' | 'otro') + * @param conceptId - conceptId SNOMED del medicamento + * @returns { existe: boolean, receta?: any } + */ +export async function verificarRecetaExistente(documento: string, sexo: string, conceptId: string) { + if (!documento) { + throw new ParamsIncorrect('Se requiere el documento (DNI) del paciente'); + } + if (!sexo) { + throw new ParamsIncorrect('Se requiere el sexo del paciente'); + } + if (!conceptId) { + throw new ParamsIncorrect('Se requiere el conceptId del medicamento'); + } + + const ahora = moment().toDate(); + const parametro: any = await RecetasParametros.findOne({ key: 'fechaLimite' }); + const days = (parametro && parametro.value) ? Number(parametro.value) : 30; + const fechaLimiteVigentes = moment().subtract(days, 'days').startOf('day').toDate(); + + const receta = await Receta.findOne({ + 'paciente.documento': documento, + 'paciente.sexo': sexo, + 'medicamento.concepto.conceptId': conceptId, + 'estadoActual.tipo': { $in: ['vigente', 'pendiente'] }, + 'estadoDispensaActual.tipo': { $nin: ['dispensada'] }, + $or: [ + // Vigentes: que no hayan superado el límite de días desde su registro + { 'estadoActual.tipo': 'vigente', fechaRegistro: { $gte: fechaLimiteVigentes, $lte: ahora } }, + // Pendientes: fechaRegistro futura (aún no activa pero pertenece al tratamiento) + { 'estadoActual.tipo': 'pendiente', fechaRegistro: { $lte: moment().add(10, 'days').endOf('day').toDate() } } + ] + }).sort({ fechaRegistro: -1 }); + + return { + existe: !!receta, + receta: receta || null + }; +} From 6b31863d201983ef338d7f411ac77df9767d2fe3 Mon Sep 17 00:00:00 2001 From: Agustin Rodriguez Alvarez <63318331+agustin1996ra@users.noreply.github.com> Date: Mon, 18 May 2026 11:09:50 -0300 Subject: [PATCH 5/5] REC-216: Carga de fecha nacimiento si el paciente no lo tiene (#2220) --- modules/recetas/recetasController.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/recetas/recetasController.ts b/modules/recetas/recetasController.ts index 3351af3738..6e501f7069 100644 --- a/modules/recetas/recetasController.ts +++ b/modules/recetas/recetasController.ts @@ -653,6 +653,11 @@ export async function create(req) { if (!pacienteAndes) { throw new ParamsIncorrect('Paciente no encontrado'); } else { + const fechaNacimientoReceta = pacienteRecetar.fechaNacimiento ? new Date(pacienteRecetar.fechaNacimiento) : null; + if (!pacienteAndes.fechaNacimiento && fechaNacimientoReceta && !Number.isNaN(fechaNacimientoReceta.getTime())) { + pacienteAndes.fechaNacimiento = fechaNacimientoReceta; + await pacienteAndes.save(); + } pacienteAndes.obraSocial = (!pacienteRecetar.obraSocial) ? null : { origen: pacienteRecetar.obraSocial.otraOS ? 'RECETAR' : 'PUCO',