@peopl-health/nexus 3.6.0 → 3.6.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.
- package/lib/controllers/conversationController.js +16 -2
- package/lib/controllers/dashboardController.js +14 -2
- package/lib/helpers/phoneFilterHelper.js +17 -0
- package/lib/helpers/trendHelper.js +113 -0
- package/lib/routes/index.js +2 -0
- package/lib/services/conversationService.js +15 -13
- package/lib/services/dashboardService.js +21 -1
- package/package.json +1 -1
|
@@ -275,11 +275,25 @@ const searchConversationsController = async (req, res) => {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
let threadNameMap = {};
|
|
279
|
+
let messageNameMap = {};
|
|
280
|
+
if (phoneNumbers.length > 0) {
|
|
281
|
+
const threads = await Thread.find({ code: { $in: phoneNumbers } }).select('code nombre').lean();
|
|
282
|
+
threads.forEach(t => { if (t.code && t.nombre) threadNameMap[t.code] = t.nombre; });
|
|
283
|
+
|
|
284
|
+
const lastPatientMessages = await Message.aggregate([
|
|
285
|
+
{ $match: { numero: { $in: phoneNumbers }, from_me: false, nombre_whatsapp: { $exists: true, $nin: [null, ''] } } },
|
|
286
|
+
{ $sort: { createdAt: -1 } },
|
|
287
|
+
{ $group: { _id: '$numero', nombre_whatsapp: { $first: '$nombre_whatsapp' } } }
|
|
288
|
+
]);
|
|
289
|
+
lastPatientMessages.forEach(m => { messageNameMap[m._id] = m.nombre_whatsapp; });
|
|
290
|
+
}
|
|
291
|
+
|
|
278
292
|
const processedConversations = conversations.map(conv => {
|
|
279
293
|
if (!conv || !conv.latestMessage) {
|
|
280
294
|
return {
|
|
281
295
|
phoneNumber: conv?._id || 'unknown',
|
|
282
|
-
name: '
|
|
296
|
+
name: 'Desconocido',
|
|
283
297
|
patientId: airtablePatientIdMap[conv?._id] || null,
|
|
284
298
|
lastMessage: '',
|
|
285
299
|
lastMessageTime: new Date(),
|
|
@@ -312,7 +326,7 @@ const searchConversationsController = async (req, res) => {
|
|
|
312
326
|
|
|
313
327
|
return {
|
|
314
328
|
phoneNumber: conv._id,
|
|
315
|
-
name: airtableNameMap[conv._id] || conv
|
|
329
|
+
name: airtableNameMap[conv._id] || threadNameMap[conv._id] || messageNameMap[conv._id] || 'Desconocido',
|
|
316
330
|
patientId: airtablePatientIdMap[conv._id] || null,
|
|
317
331
|
lastMessage: conv?.latestMessage?.body || '',
|
|
318
332
|
lastMessageTime: conv?.latestMessage?.createdAt || conv?.latestMessage?.timestamp || new Date(),
|
|
@@ -2,7 +2,7 @@ const { logger } = require('../utils/logger');
|
|
|
2
2
|
|
|
3
3
|
const { validateAndAdaptBox } = require('../helpers/dashboardHelper');
|
|
4
4
|
|
|
5
|
-
const { getAllBoxes, getStatsById, updateBox } = require('../services/dashboardService');
|
|
5
|
+
const { getAllBoxes, getStatsById, updateBox, getDailyTrend } = require('../services/dashboardService');
|
|
6
6
|
|
|
7
7
|
const getDashboardController = async (req, res) => {
|
|
8
8
|
try {
|
|
@@ -48,8 +48,20 @@ const updateDashboardControllerById = async (req, res) => {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
const getDashboardTrendController = async (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const days = parseInt(req.query.days, 10) || 60;
|
|
54
|
+
const result = await getDailyTrend(days);
|
|
55
|
+
res.json({ success: true, ...result });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
logger.error('[DashboardController] Error fetching dashboard trend:', { error: error.message });
|
|
58
|
+
res.status(500).json({ success: false, error: error.message || 'Failed to fetch dashboard trend' });
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
51
62
|
module.exports = {
|
|
52
63
|
getDashboardController,
|
|
53
64
|
getDashboardStatsControllerById,
|
|
54
|
-
updateDashboardControllerById
|
|
65
|
+
updateDashboardControllerById,
|
|
66
|
+
getDashboardTrendController
|
|
55
67
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const EXCLUDED_PHONE_NUMBERS = [
|
|
2
|
+
'whatsapp:+51951538602',
|
|
3
|
+
'whatsapp:+51976302758',
|
|
4
|
+
'whatsapp:+5215511638690',
|
|
5
|
+
'whatsapp:+5215653318508',
|
|
6
|
+
'whatsapp:+51985959446',
|
|
7
|
+
'Whatsapp:+51937258780'
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function getExcludedPhoneNumbersFilter() {
|
|
11
|
+
return {
|
|
12
|
+
$exists: true,
|
|
13
|
+
$nin: EXCLUDED_PHONE_NUMBERS
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { getExcludedPhoneNumbersFilter };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const moment = require('moment-timezone');
|
|
2
|
+
|
|
3
|
+
const { getExcludedPhoneNumbersFilter } = require('./phoneFilterHelper');
|
|
4
|
+
|
|
5
|
+
const MEXICO_TZ = 'America/Mexico_City';
|
|
6
|
+
|
|
7
|
+
function getMexicoDateRange(days = 60) {
|
|
8
|
+
const today = moment.tz(MEXICO_TZ);
|
|
9
|
+
const startDate = today.clone().subtract(days + 1, 'days').startOf('day').toDate();
|
|
10
|
+
return { startDate, today };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildBaseStages(startDate, matchConditions, extraProjectFields = {}) {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
$match: {
|
|
17
|
+
createdAt: { $gte: startDate, $exists: true },
|
|
18
|
+
...matchConditions
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
$project: {
|
|
23
|
+
mexicoDate: {
|
|
24
|
+
$dateToString: { format: '%Y-%m-%d', date: '$createdAt', timezone: MEXICO_TZ }
|
|
25
|
+
},
|
|
26
|
+
phoneNumber: { $ifNull: ['$numero', '$phoneNumber'] },
|
|
27
|
+
...extraProjectFields
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
$match: { phoneNumber: getExcludedPhoneNumbersFilter() }
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildPatientMessagesPipeline(startDate) {
|
|
37
|
+
return [
|
|
38
|
+
...buildBaseStages(startDate, { from_me: false }),
|
|
39
|
+
{
|
|
40
|
+
$group: {
|
|
41
|
+
_id: '$mexicoDate',
|
|
42
|
+
totalMessages: { $sum: 1 },
|
|
43
|
+
uniquePeople: { $addToSet: '$phoneNumber' }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
$project: { date: '$_id', totalMessages: 1, uniquePeople: { $size: '$uniquePeople' } }
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildToolDistributionPipeline(startDate) {
|
|
53
|
+
return [
|
|
54
|
+
...buildBaseStages(
|
|
55
|
+
startDate,
|
|
56
|
+
{ from_me: true, tools_executed: { $exists: true, $ne: [] } },
|
|
57
|
+
{ tools_executed: 1 }
|
|
58
|
+
),
|
|
59
|
+
{ $unwind: '$tools_executed' },
|
|
60
|
+
{
|
|
61
|
+
$project: {
|
|
62
|
+
mexicoDate: 1,
|
|
63
|
+
toolName: { $ifNull: ['$tools_executed.tool_name', 'Unknown'] }
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
$group: {
|
|
68
|
+
_id: { date: '$mexicoDate', toolName: '$toolName' },
|
|
69
|
+
count: { $sum: 1 }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
$group: {
|
|
74
|
+
_id: '$_id.date',
|
|
75
|
+
tools: { $push: { toolName: '$_id.toolName', count: '$count' } },
|
|
76
|
+
activatedFunctions: { $sum: '$count' }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
$project: { date: '$_id', tools: 1, activatedFunctions: 1 }
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function indexByDate(data) {
|
|
86
|
+
const index = {};
|
|
87
|
+
for (const item of data) index[item.date] = item;
|
|
88
|
+
return index;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mergeTrendData(patientData, toolData, today, days = 60) {
|
|
92
|
+
const patients = indexByDate(patientData);
|
|
93
|
+
const tools = indexByDate(toolData);
|
|
94
|
+
|
|
95
|
+
const result = [];
|
|
96
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
97
|
+
const dateStr = today.clone().subtract(i, 'days').format('YYYY-MM-DD');
|
|
98
|
+
const dayPatient = patients[dateStr];
|
|
99
|
+
const dayTool = tools[dateStr];
|
|
100
|
+
|
|
101
|
+
result.push({
|
|
102
|
+
date: dateStr,
|
|
103
|
+
totalMessages: dayPatient?.totalMessages || 0,
|
|
104
|
+
uniquePeople: dayPatient?.uniquePeople || 0,
|
|
105
|
+
activatedFunctions: dayTool?.activatedFunctions || 0,
|
|
106
|
+
tools: dayTool?.tools || []
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { getMexicoDateRange, buildPatientMessagesPipeline, buildToolDistributionPipeline, mergeTrendData };
|
package/lib/routes/index.js
CHANGED
|
@@ -83,6 +83,7 @@ const templateRouteDefinitions = {
|
|
|
83
83
|
const dashboardRouteDefinitions = {
|
|
84
84
|
'GET /': 'getDashboardController',
|
|
85
85
|
'GET /stats/:id': 'getDashboardStatsControllerById',
|
|
86
|
+
'GET /trend': 'getDashboardTrendController',
|
|
86
87
|
'POST /:id': 'updateDashboardControllerById'
|
|
87
88
|
};
|
|
88
89
|
|
|
@@ -193,6 +194,7 @@ const builtInControllers = {
|
|
|
193
194
|
// Dashboard controllers
|
|
194
195
|
getDashboardController: dashboardController.getDashboardController,
|
|
195
196
|
getDashboardStatsControllerById: dashboardController.getDashboardStatsControllerById,
|
|
197
|
+
getDashboardTrendController: dashboardController.getDashboardTrendController,
|
|
196
198
|
updateDashboardControllerById: dashboardController.updateDashboardControllerById
|
|
197
199
|
};
|
|
198
200
|
|
|
@@ -104,15 +104,20 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
104
104
|
|
|
105
105
|
const phoneNumbers = conversations.map(c => c._id).filter(Boolean);
|
|
106
106
|
let airtableNameMap = {};
|
|
107
|
+
let threadNameMap = {};
|
|
107
108
|
if (phoneNumbers.length) {
|
|
108
109
|
const formula = `OR(${phoneNumbers.map(p => `{whatsapp_id} = "${p}"`).join(', ')})`;
|
|
109
110
|
const records = await getRecordByFilter(Historial_Clinico_ID, 'estado_general', formula);
|
|
110
|
-
airtableNameMap = toMap(records || [], r => r.whatsapp_id, r => r.name);
|
|
111
|
+
airtableNameMap = toMap((records || []).filter(r => r.name), r => r.whatsapp_id, r => r.name);
|
|
112
|
+
|
|
113
|
+
const threads = await Thread.find({ code: { $in: phoneNumbers } }).select('code nombre').lean();
|
|
114
|
+
threadNameMap = toMap((threads || []).filter(t => t.nombre), t => t.code, t => t.nombre);
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
const nameMap = {
|
|
114
|
-
...
|
|
115
|
-
...
|
|
118
|
+
...toMap(contactNames, c => c?._id ? c._id : null, c => c.name || 'Desconocido'),
|
|
119
|
+
...threadNameMap,
|
|
120
|
+
...airtableNameMap
|
|
116
121
|
};
|
|
117
122
|
const unreadMap = toMap(unreadCounts, i => i?._id, i => i.unreadCount || 0);
|
|
118
123
|
|
|
@@ -128,13 +133,13 @@ const getMediaType = (media) => {
|
|
|
128
133
|
};
|
|
129
134
|
|
|
130
135
|
const processConversations = async (conversations, nameMap, unreadMap) => {
|
|
131
|
-
const createConversation = (conv, index
|
|
136
|
+
const createConversation = (conv, index) => {
|
|
132
137
|
const msg = conv?.latestMessage;
|
|
133
|
-
const phoneNumber = conv?._id ||
|
|
138
|
+
const phoneNumber = conv?._id || `error_${index}`;
|
|
134
139
|
return {
|
|
135
140
|
phoneNumber,
|
|
136
|
-
name: nameMap[phoneNumber]
|
|
137
|
-
lastMessage: msg?.body ||
|
|
141
|
+
name: nameMap[phoneNumber],
|
|
142
|
+
lastMessage: msg?.body || '',
|
|
138
143
|
lastMessageTime: msg?.createdAt || msg?.timestamp || new Date(),
|
|
139
144
|
messageCount: conv?.messageCount || 0,
|
|
140
145
|
unreadCount: unreadMap[phoneNumber] || 0,
|
|
@@ -148,16 +153,13 @@ const processConversations = async (conversations, nameMap, unreadMap) => {
|
|
|
148
153
|
try {
|
|
149
154
|
const result = (conversations || []).map((conv, index) => {
|
|
150
155
|
try {
|
|
151
|
-
if (!conv?.latestMessage) {
|
|
152
|
-
logger.warn('[processConversations] Missing latestMessage', { index, id: conv?._id });
|
|
153
|
-
return createConversation(conv, index, { name: 'Unknown' });
|
|
154
|
-
}
|
|
156
|
+
if (!conv?.latestMessage) logger.warn('[processConversations] Missing latestMessage', { index, id: conv?._id });
|
|
155
157
|
return createConversation(conv, index);
|
|
156
158
|
} catch (error) {
|
|
157
159
|
logger.error('[processConversations] Error processing', { index, error: error.message });
|
|
158
|
-
return
|
|
160
|
+
return null;
|
|
159
161
|
}
|
|
160
|
-
});
|
|
162
|
+
}).filter(Boolean);
|
|
161
163
|
|
|
162
164
|
logger.info('[processConversations] Completed', { count: result.length });
|
|
163
165
|
return result;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const { logger } = require('../utils/logger');
|
|
2
2
|
const MapCache = require('../utils/MapCache');
|
|
3
3
|
|
|
4
|
+
const { Message } = require('../models/messageModel');
|
|
5
|
+
|
|
4
6
|
const { fetchBoxesFromAirtable, fetchDetailsFromAirtable, attachPreview } = require('../helpers/dashboardHelper');
|
|
7
|
+
const { getMexicoDateRange, buildPatientMessagesPipeline, buildToolDistributionPipeline, mergeTrendData } = require('../helpers/trendHelper');
|
|
5
8
|
|
|
6
9
|
const boxCache = new MapCache({ maxSize: 100 });
|
|
7
10
|
const detailCache = new MapCache({ maxSize: 100 });
|
|
@@ -61,8 +64,25 @@ async function updateBox(id, data) {
|
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
async function getDailyTrend(days) {
|
|
68
|
+
const { startDate, today } = getMexicoDateRange(days);
|
|
69
|
+
|
|
70
|
+
const patientPipeline = buildPatientMessagesPipeline(startDate);
|
|
71
|
+
const patientData = await Message.aggregate(patientPipeline);
|
|
72
|
+
|
|
73
|
+
const toolPipeline = buildToolDistributionPipeline(startDate);
|
|
74
|
+
const toolData = await Message.aggregate(toolPipeline);
|
|
75
|
+
|
|
76
|
+
const dailyData = mergeTrendData(patientData, toolData, today, days);
|
|
77
|
+
|
|
78
|
+
logger.info('[DashboardService] Daily trend fetched', { days, totalDays: dailyData.length });
|
|
79
|
+
|
|
80
|
+
return { dailyData, totalDays: dailyData.length };
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
module.exports = {
|
|
65
84
|
getAllBoxes,
|
|
66
85
|
getStatsById,
|
|
67
|
-
updateBox
|
|
86
|
+
updateBox,
|
|
87
|
+
getDailyTrend
|
|
68
88
|
};
|