@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.
@@ -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: 'Unknown',
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?.latestMessage?.nombre_whatsapp || 'Unknown',
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 };
@@ -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
- ...airtableNameMap,
115
- ...toMap(contactNames, c => c?._id && !airtableNameMap[c._id] ? c._id : null, c => c.name || 'Unknown')
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, fallback = {}) => {
136
+ const createConversation = (conv, index) => {
132
137
  const msg = conv?.latestMessage;
133
- const phoneNumber = conv?._id || fallback.phoneNumber || `error_${index}`;
138
+ const phoneNumber = conv?._id || `error_${index}`;
134
139
  return {
135
140
  phoneNumber,
136
- name: nameMap[phoneNumber] || msg?.nombre_whatsapp || fallback.name || 'Unknown',
137
- lastMessage: msg?.body || fallback.lastMessage || '',
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 createConversation(conv, index, { name: 'Error Processing', lastMessage: 'Error processing conversation' });
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.6.0",
3
+ "version": "3.6.2",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",