@peopl-health/nexus 5.68.0 → 5.68.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,12 +6,12 @@ const URGENCY_LABELS = {
|
|
|
6
6
|
stat: 'inmediata', asap: 'lo antes posible', urgent: 'urgente', routine: 'rutina',
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const ASSESSMENT_ACTION = 'Acción solicitada: valoración médica prioritaria. ¿La valora en persona, la envía a urgencias o indica otra conducta?';
|
|
9
|
+
const REQUESTED_ACTION = '¿Aprueba la propuesta, la modifica o prefiere una valoración presencial?';
|
|
10
|
+
const ASSESSMENT_ACTION = 'Requiere valoración médica: ¿la valora en persona, la envía a urgencias o indica otra conducta?';
|
|
12
11
|
const DOCTOR_TITLE = /^(dr|dra)\.?\s/i;
|
|
13
12
|
|
|
14
13
|
const withTitle = (name) => (DOCTOR_TITLE.test(name) ? name : `Dr./Dra. ${name}`);
|
|
14
|
+
const sentence = (text) => (/[.!?]$/.test(text) ? text : `${text}.`);
|
|
15
15
|
|
|
16
16
|
function latestAlarm(review) {
|
|
17
17
|
const alarms = Array.isArray(review?.alarms) ? review.alarms : [];
|
|
@@ -28,19 +28,6 @@ function flagsByStatus(sections) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function symptomLines(sections, stamp, fallbackSymptom = null) {
|
|
32
|
-
const grade = [1, 2, 3].includes(sections?.grade) ? `G${sections.grade}` : 'grado no establecido';
|
|
33
|
-
const said = [inlineOf(sections?.symptom) || fallbackSymptom || 'síntoma no especificado', grade, ...stamp].filter(Boolean);
|
|
34
|
-
const { present, absent, unassessed } = flagsByStatus(sections);
|
|
35
|
-
if (!present.length && !absent.length && !unassessed.length) return [`Síntoma: ${said.join(' · ')}`, 'Banderas rojas: no documentadas.'];
|
|
36
|
-
return [
|
|
37
|
-
`Síntoma: ${said.join(' · ')}`,
|
|
38
|
-
`Banderas rojas presentes: ${present.length ? present.join(', ') : 'ninguna'}.`,
|
|
39
|
-
`Banderas rojas ausentes: ${absent.length ? absent.join(', ') : 'ninguna revisada'}.`,
|
|
40
|
-
...(unassessed.length ? [`Banderas rojas sin evaluar: ${unassessed.join(', ')}.`] : []),
|
|
41
|
-
];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
31
|
function vitalsLine(vitals) {
|
|
45
32
|
const parts = [];
|
|
46
33
|
if (vitals?.temperatureC != null) parts.push(`T ${vitals.temperatureC} °C`);
|
|
@@ -48,62 +35,66 @@ function vitalsLine(vitals) {
|
|
|
48
35
|
if (vitals?.systolic != null || vitals?.diastolic != null) parts.push(`PA ${vitals.systolic ?? '?'}/${vitals.diastolic ?? '?'} mmHg`);
|
|
49
36
|
if (vitals?.spo2 != null) parts.push(`SpO2 ${vitals.spo2}%`);
|
|
50
37
|
if (vitals?.respiratoryRate != null) parts.push(`FR ${vitals.respiratoryRate} rpm`);
|
|
51
|
-
return parts.length ? `Signos vitales: ${parts.join(' · ')}.` :
|
|
38
|
+
return parts.length ? `Signos vitales: ${parts.join(' · ')}.` : null;
|
|
52
39
|
}
|
|
53
40
|
|
|
54
|
-
function
|
|
41
|
+
function drugOf(symptom, item) {
|
|
55
42
|
const entry = formularyFor(symptom);
|
|
56
43
|
const drug = entry ? findDrug(entry, item) : null;
|
|
57
44
|
if (!drug) return null;
|
|
58
|
-
const name = [drug.drug, drug.
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
parts.push(`posición en la tabla: ${drug.tableLabel}`);
|
|
62
|
-
const why = inlineOf(item?.justification?.text);
|
|
63
|
-
if (why) parts.push(`motivo: ${why}`);
|
|
64
|
-
return parts.join(' — ');
|
|
45
|
+
const name = [drug.drug, drug.strength].filter(Boolean).join(' ');
|
|
46
|
+
const why = inlineOf(item?.justification?.text)?.replace(/[.;]+$/, '');
|
|
47
|
+
return [drug.instructions ? `${name} (${drug.instructions})` : name, why && `motivo: ${why}`].filter(Boolean).join(', ');
|
|
65
48
|
}
|
|
66
49
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
50
|
+
const proposedDrugs = ({ proposal }) => (proposal?.items ?? []).map((item) => drugOf(proposal.symptom, item)).filter(Boolean);
|
|
51
|
+
|
|
52
|
+
function proposalLine({ proposal, withheldReason }) {
|
|
53
|
+
const drugs = proposedDrugs({ proposal });
|
|
54
|
+
if (!drugs.length) return `→ Sin propuesta de tratamiento: ${sentence(inlineOf(withheldReason) || 'requiere valoración médica')}`;
|
|
55
|
+
const declared = [...(proposal.conditions ?? []), ...(proposal.ruledOut ?? []).map((key) => `descartado ${key}`)]
|
|
56
|
+
.map((key) => inlineOf(key)?.replace(/_/g, ' ')).filter(Boolean);
|
|
57
|
+
const table = formularyFor(proposal.symptom)?.label;
|
|
58
|
+
return [`→ Propuesta para su aprobación (tabla ${table}): ${sentence(drugs.join('; '))}`, declared.length && `Verificar: ${sentence(declared.join(', '))}`].filter(Boolean).join(' ');
|
|
76
59
|
}
|
|
77
60
|
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
|
|
61
|
+
function symptomParagraph(entry, fallbackSymptom) {
|
|
62
|
+
const sections = entry.sections ?? {};
|
|
63
|
+
const grade = [1, 2, 3].includes(sections.grade) ? `G${sections.grade}` : 'grado no establecido';
|
|
64
|
+
const { present, absent, unassessed } = flagsByStatus(sections);
|
|
65
|
+
const isUndocumented = !present.length && !absent.length && !unassessed.length;
|
|
66
|
+
const summary = inlineOf(sections.characterizationSummary);
|
|
67
|
+
return [
|
|
68
|
+
`${inlineOf(sections.symptom) || fallbackSymptom || 'Síntoma no especificado'} (${grade}):`,
|
|
69
|
+
summary && sentence(summary),
|
|
70
|
+
present.length && `${present.length > 1 ? 'Banderas rojas presentes' : 'Bandera roja presente'}: ${sentence(present.join(', '))}`,
|
|
71
|
+
unassessed.length && `Falta confirmar: ${sentence(unassessed.join(', '))}`,
|
|
72
|
+
isUndocumented && 'Banderas rojas no documentadas.',
|
|
73
|
+
!present.length && !unassessed.length && absent.length && 'Sin banderas rojas.',
|
|
74
|
+
proposalLine(entry),
|
|
75
|
+
].filter(Boolean).join(' ');
|
|
81
76
|
}
|
|
82
77
|
|
|
83
78
|
function composeDoctorBody({ alarm, shared, symptoms }) {
|
|
84
|
-
const several = symptoms.length > 1;
|
|
85
|
-
const name = (entry) => inlineOf(entry.sections?.symptom) || 'Síntoma';
|
|
86
|
-
const stamp = [
|
|
87
|
-
URGENCY_LABELS[alarm?.urgencyLevel] ?? inlineOf(alarm?.urgencyLevel),
|
|
88
|
-
alarm?.raisedAt ? mexicoStamp(alarm.raisedAt) : null,
|
|
89
|
-
];
|
|
90
79
|
const context = shared?.oncologyContext;
|
|
91
|
-
const
|
|
80
|
+
const treatment = inlineOf(context?.treatment);
|
|
81
|
+
const lastDose = inlineOf(context?.lastDose);
|
|
82
|
+
const priorManagement = inlineOf(context?.priorManagement);
|
|
83
|
+
const response = inlineOf(context?.response);
|
|
84
|
+
const urgency = [URGENCY_LABELS[alarm?.urgencyLevel] ?? inlineOf(alarm?.urgencyLevel), alarm?.raisedAt ? mexicoStamp(alarm.raisedAt) : null].filter(Boolean);
|
|
85
|
+
const priorities = inlineOf(shared?.doctorPriorities);
|
|
92
86
|
return [
|
|
93
|
-
|
|
94
|
-
''
|
|
95
|
-
|
|
87
|
+
urgency.length ? `Urgencia: ${urgency.join(' · ')}` : null,
|
|
88
|
+
`Tratamiento: ${treatment || 'no disponible'}; última dosis ${lastDose || 'no disponible'}.`,
|
|
89
|
+
priorManagement || response ? `Manejo previo: ${sentence([priorManagement, response && `respuesta ${response}`].filter(Boolean).join('; '))}` : null,
|
|
96
90
|
vitalsLine(shared?.vitals),
|
|
97
|
-
|
|
98
|
-
'',
|
|
99
|
-
`Contexto oncológico: tratamiento ${value(context?.treatment)}; última dosis ${value(context?.lastDose)}.`,
|
|
100
|
-
`Manejo previo y respuesta: ${value(context?.priorManagement)}; respuesta ${value(context?.response)}.`,
|
|
91
|
+
inlineOf(shared?.objectiveData) ? `Datos objetivos: ${sentence(inlineOf(shared.objectiveData))}` : null,
|
|
101
92
|
'',
|
|
102
|
-
|
|
103
|
-
...symptoms.flatMap((entry) => (several ? [`${proposalHeading(entry, name(entry))}:`, ...proposalBody(entry)] : proposalBody(entry))),
|
|
93
|
+
...symptoms.map((entry) => symptomParagraph(entry, symptoms.length > 1 ? null : inlineOf(alarm?.symptom))),
|
|
104
94
|
'',
|
|
105
|
-
|
|
106
|
-
|
|
95
|
+
priorities ? `Prioridades: ${sentence(priorities)}` : null,
|
|
96
|
+
symptoms.some((entry) => proposedDrugs(entry).length) ? REQUESTED_ACTION : ASSESSMENT_ACTION,
|
|
97
|
+
].filter((line) => line !== null).join('\n');
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
function identityLines({ identity, review }) {
|
|
@@ -23,7 +23,6 @@ const {
|
|
|
23
23
|
const { getPatientAuditLog } = require('../models/patientAuditLogModel');
|
|
24
24
|
const { isWithin24HourWindow } = require('../helpers/conversationWindowHelper');
|
|
25
25
|
const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
|
|
26
|
-
const { sendMessage } = require('../core/NexusMessaging');
|
|
27
26
|
const { generateEscalationDraft } = require('../clinical/services/escalationDraftService');
|
|
28
27
|
const { conversationLink, recordEmergency, writeUnresolvedRequest } = require('../clinical/services/clinicalAirtableService');
|
|
29
28
|
const { renderDoctorMessage, templateSummary, withIdentity } = require('../clinical/helpers/doctorReviewMessageHelper');
|
|
@@ -39,6 +38,8 @@ const { getEscalationApprover, listEscalationApprovers } = require('./physicianS
|
|
|
39
38
|
const { switchThreadStoppedStatus } = require('./threadService');
|
|
40
39
|
const { effectiveUrgencyOf, responseDeadlineOf, responseDeadlinePolicy } = require('./reviewDeadlineService');
|
|
41
40
|
|
|
41
|
+
const getMessaging = () => require('../core/NexusMessaging');
|
|
42
|
+
|
|
42
43
|
const AUDIT_SOURCE = 'escalation_review';
|
|
43
44
|
const RECENT_REVIEW_LIMIT = 20;
|
|
44
45
|
const TIMELINE_LIMIT = 500;
|
|
@@ -367,7 +368,7 @@ async function sendDoctorAlert({ message, approver, operator }) {
|
|
|
367
368
|
const outgoing = inWindow
|
|
368
369
|
? { code: doctorCode, body: message.body, triggeredBy: operator }
|
|
369
370
|
: { code: doctorCode, contentSid: configured('ESCALATION_REVIEW_DOCTOR_ALERT_IDENTIFIED_CONTENT_SID'), variables: message.template, triggeredBy: operator };
|
|
370
|
-
const result = await sendMessage(outgoing);
|
|
371
|
+
const result = await getMessaging().sendMessage(outgoing);
|
|
371
372
|
if (!result?.success && !result?.completed) {
|
|
372
373
|
throw httpError(502, 'The WhatsApp message to the physician could not be sent');
|
|
373
374
|
}
|
|
@@ -1608,7 +1609,7 @@ async function sendApprovedIndications({ id, outsideWindowRoute = null, sentBy,
|
|
|
1608
1609
|
|
|
1609
1610
|
let result;
|
|
1610
1611
|
try {
|
|
1611
|
-
result = await sendMessage(message);
|
|
1612
|
+
result = await getMessaging().sendMessage(message);
|
|
1612
1613
|
const alreadyDelivered = result?.status === 'duplicate_suppressed' && result?.completed === true;
|
|
1613
1614
|
if (!result?.success && !alreadyDelivered) {
|
|
1614
1615
|
throw httpError(502, 'The indications could not be sent to the patient');
|