@peopl-health/nexus 2.0.7 → 2.0.8
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/bugReportController.js +63 -0
- package/lib/controllers/interactionController.js +44 -2
- package/lib/controllers/messageController.js +0 -30
- package/lib/controllers/qualityMessageController.js +81 -0
- package/lib/models/index.js +2 -0
- package/lib/models/interactionModel.js +1 -0
- package/lib/models/messageModel.js +1 -6
- package/lib/models/qualityMessageModel.js +19 -0
- package/lib/routes/index.js +13 -5
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { Message } = require('../models/messageModel');
|
|
2
|
+
const { addRecord, getRecordByFilter } = require('../services/airtableService');
|
|
3
|
+
const { Logging_ID } = require('../config/airtableConfig');
|
|
4
|
+
|
|
5
|
+
async function logBugReportToAirtable(reporter, whatsapp_id, description, severity, messageIds = []) {
|
|
6
|
+
try {
|
|
7
|
+
let conversation = null;
|
|
8
|
+
if (messageIds && messageIds.length > 0) {
|
|
9
|
+
const messageObjects = await Message.find({ _id: { $in: messageIds } }).sort({ timestamp: 1 });
|
|
10
|
+
conversation = messageObjects.map(msg => {
|
|
11
|
+
const timestamp = new Date(msg.timestamp).toISOString().slice(0, 16).replace('T', ' ');
|
|
12
|
+
const role = msg.from_me ? 'Assistant' : 'Patient';
|
|
13
|
+
return `[${timestamp}] ${role}: ${msg.body || '(media)'}`;
|
|
14
|
+
}).join('\n');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let patientId = null;
|
|
18
|
+
try {
|
|
19
|
+
const patientRecords = await getRecordByFilter(Logging_ID, 'estado_general', `{whatsapp_id}='${whatsapp_id}'`);
|
|
20
|
+
if (patientRecords && patientRecords.length > 0) {
|
|
21
|
+
patientId = patientRecords[0].record_logging_id;
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.warn('Could not find patient in estado_general:', err.message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const airtableData = {
|
|
28
|
+
reporter,
|
|
29
|
+
patient_id: patientId ? [patientId] : undefined,
|
|
30
|
+
description,
|
|
31
|
+
severity,
|
|
32
|
+
conversation: conversation || undefined
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
await addRecord(Logging_ID, 'bug_reports', airtableData);
|
|
36
|
+
console.log('Bug report logged to Airtable successfully');
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error logging bug report to Airtable:', error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const reportBugController = async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const { reporter, whatsapp_id, description, severity, messages } = req.body;
|
|
45
|
+
|
|
46
|
+
if (!reporter) return res.status(400).json({ success: false, error: 'Reporter username is required' });
|
|
47
|
+
if (!whatsapp_id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
|
|
48
|
+
if (!severity || !['low', 'medium', 'high'].includes(severity)) {
|
|
49
|
+
return res.status(400).json({ success: false, error: 'Severity must be low, medium, or high' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
logBugReportToAirtable(reporter, whatsapp_id, description, severity, messages).catch(err =>
|
|
53
|
+
console.error('Background bug report logging failed:', err)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
res.status(201).json({ success: true, message: 'Bug report submitted successfully' });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Error submitting bug report:', error);
|
|
59
|
+
res.status(500).json({ success: false, error: error.message });
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
module.exports = { reportBugController };
|
|
@@ -1,17 +1,59 @@
|
|
|
1
1
|
const { Interaction } = require('../models/interactionModel');
|
|
2
|
+
const { Message } = require('../models/messageModel');
|
|
2
3
|
const { INTERACTION_QUALITY_VALUES } = require('../config/interactionConfig');
|
|
4
|
+
const { addRecord, getRecordByFilter } = require('../services/airtableService');
|
|
5
|
+
const { Logging_ID } = require('../config/airtableConfig');
|
|
6
|
+
|
|
7
|
+
async function logInteractionToAirtable(messageIds, whatsapp_id, voter_username) {
|
|
8
|
+
try {
|
|
9
|
+
const messageObjects = await Message.find({ _id: { $in: messageIds } }).sort({ createdAt: -1 });
|
|
10
|
+
|
|
11
|
+
const conversation = messageObjects.map(msg => {
|
|
12
|
+
const timestamp = new Date(msg.timestamp).toISOString().slice(0, 16).replace('T', ' ');
|
|
13
|
+
const role = msg.from_me ? 'Assistant' : 'Patient';
|
|
14
|
+
return `[${timestamp}] ${role}: ${msg.body || '(media)'}`;
|
|
15
|
+
}).join('\n');
|
|
16
|
+
|
|
17
|
+
let patientId = null;
|
|
18
|
+
try {
|
|
19
|
+
const patientRecords = await getRecordByFilter(Logging_ID, 'estado_general', `{whatsapp_id}='${whatsapp_id}'`);
|
|
20
|
+
if (patientRecords && patientRecords.length > 0) {
|
|
21
|
+
patientId = patientRecords[0].record_logging_id;
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.warn('Could not find patient in estado_general:', err.message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const airtableData = {
|
|
28
|
+
patient_id: patientId ? [patientId] : undefined,
|
|
29
|
+
reporter: voter_username,
|
|
30
|
+
conversation
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
await addRecord(Logging_ID, 'interactions', airtableData);
|
|
34
|
+
console.log('Interaction logged to Airtable successfully');
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error logging interaction to Airtable:', error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
3
39
|
|
|
4
40
|
const addInteractionController = async (req, res) => {
|
|
5
41
|
try {
|
|
6
|
-
const { messages, whatsapp_id, quality, description } = req.body;
|
|
42
|
+
const { messages, whatsapp_id, voter_username, quality, description } = req.body;
|
|
7
43
|
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
8
44
|
return res.status(400).json({ success: false, error: 'Messages array is required and must not be empty' });
|
|
9
45
|
}
|
|
10
46
|
if (!whatsapp_id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
|
|
47
|
+
if (!voter_username) return res.status(400).json({ success: false, error: 'Voter username is required' });
|
|
11
48
|
if (!quality || !INTERACTION_QUALITY_VALUES.includes(quality)) {
|
|
12
49
|
return res.status(400).json({ success: false, error: `Quality must be one of: ${INTERACTION_QUALITY_VALUES.join(', ')}` });
|
|
13
50
|
}
|
|
14
|
-
const interaction = await Interaction.create({ messages, whatsapp_id, quality, description });
|
|
51
|
+
const interaction = await Interaction.create({ messages, whatsapp_id, voter_username, quality, description });
|
|
52
|
+
|
|
53
|
+
logInteractionToAirtable(messages, whatsapp_id, voter_username).catch(err =>
|
|
54
|
+
console.error('Background Airtable logging failed:', err)
|
|
55
|
+
);
|
|
56
|
+
|
|
15
57
|
res.status(201).json({ success: true, interaction });
|
|
16
58
|
} catch (error) {
|
|
17
59
|
console.error('Error adding interaction:', error);
|
|
@@ -348,40 +348,10 @@ const getLastInteractionController = async (req, res) => {
|
|
|
348
348
|
}
|
|
349
349
|
};
|
|
350
350
|
|
|
351
|
-
const getMessageQualityController = async (req, res) => {
|
|
352
|
-
try {
|
|
353
|
-
const { id } = req.params;
|
|
354
|
-
const message = await Message.findById(id);
|
|
355
|
-
if (!message) return res.status(404).json({ success: false, error: 'Message not found' });
|
|
356
|
-
res.status(200).json({ success: true, messageId: id, quality: message.quality });
|
|
357
|
-
} catch (error) {
|
|
358
|
-
console.error('Error fetching message quality:', error);
|
|
359
|
-
res.status(500).json({ success: false, error: error.message });
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
const updateMessageQualityController = async (req, res) => {
|
|
364
|
-
try {
|
|
365
|
-
const { id } = req.params;
|
|
366
|
-
const { quality } = req.body;
|
|
367
|
-
if (!['low', 'medium', 'high'].includes(quality)) {
|
|
368
|
-
return res.status(400).json({ success: false, error: 'Invalid quality value. Must be low, medium, or high' });
|
|
369
|
-
}
|
|
370
|
-
const message = await Message.findByIdAndUpdate(id, { quality }, { new: true });
|
|
371
|
-
if (!message) return res.status(404).json({ success: false, error: 'Message not found' });
|
|
372
|
-
res.status(200).json({ success: true, messageId: id, quality: message.quality });
|
|
373
|
-
} catch (error) {
|
|
374
|
-
console.error('Error updating message quality:', error);
|
|
375
|
-
res.status(500).json({ success: false, error: error.message });
|
|
376
|
-
}
|
|
377
|
-
};
|
|
378
|
-
|
|
379
351
|
module.exports = {
|
|
380
352
|
sendMessageController,
|
|
381
353
|
sendBulkMessageController,
|
|
382
354
|
sendBulkMessageAirtableController,
|
|
383
355
|
getLastInteractionController,
|
|
384
|
-
getMessageQualityController,
|
|
385
|
-
updateMessageQualityController,
|
|
386
356
|
configureMessageController
|
|
387
357
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { QualityMessage } = require('../models/qualityMessageModel');
|
|
2
|
+
const { Message } = require('../models/messageModel');
|
|
3
|
+
|
|
4
|
+
const addQualityVoteController = async (req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
const { message_id, voter_username, quality, notes, context } = req.body;
|
|
7
|
+
|
|
8
|
+
if (!message_id) return res.status(400).json({ success: false, error: 'Message ID is required' });
|
|
9
|
+
if (!voter_username) return res.status(400).json({ success: false, error: 'Voter username is required' });
|
|
10
|
+
if (!quality || !['low', 'medium', 'high'].includes(quality)) {
|
|
11
|
+
return res.status(400).json({ success: false, error: 'Quality must be low, medium, or high' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const message = await Message.findById(message_id);
|
|
15
|
+
if (!message) return res.status(404).json({ success: false, error: 'Message not found' });
|
|
16
|
+
|
|
17
|
+
const qualityVote = await QualityMessage.findOneAndUpdate(
|
|
18
|
+
{ message_id, voter_username },
|
|
19
|
+
{ quality, notes, context },
|
|
20
|
+
{ upsert: true, new: true }
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
res.status(201).json({ success: true, qualityVote });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Error adding quality vote:', error);
|
|
26
|
+
res.status(500).json({ success: false, error: error.message });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getQualityVotesByMessageController = async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const { message_id } = req.params;
|
|
33
|
+
const votes = await QualityMessage.find({ message_id }).sort({ createdAt: -1 });
|
|
34
|
+
|
|
35
|
+
const summary = {
|
|
36
|
+
total: votes.length,
|
|
37
|
+
low: votes.filter(v => v.quality === 'low').length,
|
|
38
|
+
medium: votes.filter(v => v.quality === 'medium').length,
|
|
39
|
+
high: votes.filter(v => v.quality === 'high').length
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
res.status(200).json({ success: true, messageId: message_id, summary, votes });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error fetching quality votes:', error);
|
|
45
|
+
res.status(500).json({ success: false, error: error.message });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getQualityVotesByVoterController = async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const { voter_username } = req.params;
|
|
52
|
+
const votes = await QualityMessage.find({ voter_username }).populate('message_id').sort({ createdAt: -1 });
|
|
53
|
+
res.status(200).json({ success: true, voterUsername: voter_username, count: votes.length, votes });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Error fetching voter quality votes:', error);
|
|
56
|
+
res.status(500).json({ success: false, error: error.message });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const getQualityVoteByMessageAndVoterController = async (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const { message_id, voter_username } = req.params;
|
|
63
|
+
const vote = await QualityMessage.findOne({ message_id, voter_username }).populate('message_id');
|
|
64
|
+
|
|
65
|
+
if (!vote) {
|
|
66
|
+
return res.status(404).json({ success: false, error: 'Vote not found' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
res.status(200).json({ success: true, vote });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Error fetching quality vote:', error);
|
|
72
|
+
res.status(500).json({ success: false, error: error.message });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
addQualityVoteController,
|
|
78
|
+
getQualityVotesByMessageController,
|
|
79
|
+
getQualityVotesByVoterController,
|
|
80
|
+
getQualityVoteByMessageAndVoterController
|
|
81
|
+
};
|
package/lib/models/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const { Message, getMessageValues, formatTimestamp } = require('./messageModel');
|
|
2
2
|
const { Thread } = require('./threadModel');
|
|
3
3
|
const { Interaction } = require('./interactionModel');
|
|
4
|
+
const { QualityMessage } = require('./qualityMessageModel');
|
|
4
5
|
|
|
5
6
|
module.exports = {
|
|
6
7
|
Message,
|
|
7
8
|
Thread,
|
|
8
9
|
Interaction,
|
|
10
|
+
QualityMessage,
|
|
9
11
|
getMessageValues,
|
|
10
12
|
formatTimestamp
|
|
11
13
|
};
|
|
@@ -4,6 +4,7 @@ const { INTERACTION_QUALITY_VALUES } = require('../config/interactionConfig');
|
|
|
4
4
|
const interactionSchema = new mongoose.Schema({
|
|
5
5
|
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message', required: true }],
|
|
6
6
|
whatsapp_id: { type: String, required: true, index: true },
|
|
7
|
+
voter_username: { type: String, required: true, index: true },
|
|
7
8
|
quality: {
|
|
8
9
|
type: String,
|
|
9
10
|
enum: INTERACTION_QUALITY_VALUES,
|
|
@@ -44,11 +44,6 @@ const messageSchema = new mongoose.Schema({
|
|
|
44
44
|
metadata: { type: Object, default: null }
|
|
45
45
|
},
|
|
46
46
|
clinical_context: { type: Object, default: null },
|
|
47
|
-
quality: {
|
|
48
|
-
type: String,
|
|
49
|
-
enum: ['low', 'medium', 'high'],
|
|
50
|
-
default: 'medium'
|
|
51
|
-
},
|
|
52
47
|
memoryType: {
|
|
53
48
|
type: String,
|
|
54
49
|
enum: ['active', 'archived'],
|
|
@@ -76,7 +71,7 @@ async function getClinicalContext(whatsappId) {
|
|
|
76
71
|
try {
|
|
77
72
|
const records = await getRecordByFilter(Monitoreo_ID, 'estado_general', `{whatsapp_id}='${whatsappId}'`);
|
|
78
73
|
if (records && records.length > 0 && records[0]['clinical-context-json']) {
|
|
79
|
-
return records[0]['clinical-context-json'];
|
|
74
|
+
return JSON.parse(records[0]['clinical-context-json']);
|
|
80
75
|
}
|
|
81
76
|
return null;
|
|
82
77
|
} catch (error) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const qualityMessageSchema = new mongoose.Schema({
|
|
4
|
+
message_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Message', required: true, index: true },
|
|
5
|
+
voter_username: { type: String, required: true, index: true },
|
|
6
|
+
quality: {
|
|
7
|
+
type: String,
|
|
8
|
+
enum: ['low', 'medium', 'high'],
|
|
9
|
+
required: true
|
|
10
|
+
},
|
|
11
|
+
notes: { type: String, default: null },
|
|
12
|
+
context: { type: String, default: null }
|
|
13
|
+
}, { timestamps: true });
|
|
14
|
+
|
|
15
|
+
qualityMessageSchema.index({ message_id: 1, voter_username: 1 }, { unique: true });
|
|
16
|
+
|
|
17
|
+
const QualityMessage = mongoose.model('QualityMessage', qualityMessageSchema);
|
|
18
|
+
|
|
19
|
+
module.exports = { QualityMessage, qualityMessageSchema };
|
package/lib/routes/index.js
CHANGED
|
@@ -20,7 +20,8 @@ const conversationRouteDefinitions = {
|
|
|
20
20
|
'GET /:phoneNumber/new': 'getNewMessagesController',
|
|
21
21
|
'POST /reply': 'getConversationReplyController',
|
|
22
22
|
'POST /send-template': 'sendTemplateToNewNumberController',
|
|
23
|
-
'POST /:phoneNumber/read': 'markMessagesAsReadController'
|
|
23
|
+
'POST /:phoneNumber/read': 'markMessagesAsReadController',
|
|
24
|
+
'POST /report-bug': 'reportBugController'
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
const mediaRouteDefinitions = {
|
|
@@ -33,8 +34,10 @@ const messageRouteDefinitions = {
|
|
|
33
34
|
'POST /send-bulk': 'sendBulkMessageController',
|
|
34
35
|
'POST /send-bulk-airtable': 'sendBulkMessageAirtableController',
|
|
35
36
|
'GET /last': 'getLastInteractionController',
|
|
36
|
-
'
|
|
37
|
-
'
|
|
37
|
+
'POST /quality': 'addQualityVoteController',
|
|
38
|
+
'GET /quality/:message_id': 'getQualityVotesByMessageController',
|
|
39
|
+
'GET /quality/:message_id/voter/:voter_username': 'getQualityVoteByMessageAndVoterController',
|
|
40
|
+
'GET /quality/voter/:voter_username': 'getQualityVotesByVoterController'
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
const interactionRouteDefinitions = {
|
|
@@ -78,11 +81,13 @@ const createRouter = (routeDefinitions, controllers) => {
|
|
|
78
81
|
|
|
79
82
|
// Import built-in controllers
|
|
80
83
|
const assistantController = require('../controllers/assistantController');
|
|
84
|
+
const bugReportController = require('../controllers/bugReportController');
|
|
81
85
|
const conversationController = require('../controllers/conversationController');
|
|
82
86
|
const interactionController = require('../controllers/interactionController');
|
|
83
87
|
const mediaController = require('../controllers/mediaController');
|
|
84
88
|
const messageController = require('../controllers/messageController');
|
|
85
89
|
const patientController = require('../controllers/patientController');
|
|
90
|
+
const qualityMessageController = require('../controllers/qualityMessageController');
|
|
86
91
|
const templateController = require('../controllers/templateController');
|
|
87
92
|
const templateFlowController = require('../controllers/templateFlowController');
|
|
88
93
|
const uploadController = require('../controllers/uploadController');
|
|
@@ -108,6 +113,7 @@ const builtInControllers = {
|
|
|
108
113
|
getConversationReplyController: conversationController.getConversationReplyController,
|
|
109
114
|
sendTemplateToNewNumberController: conversationController.sendTemplateToNewNumberController,
|
|
110
115
|
markMessagesAsReadController: conversationController.markMessagesAsReadController,
|
|
116
|
+
reportBugController: bugReportController.reportBugController,
|
|
111
117
|
|
|
112
118
|
// Interaction controllers
|
|
113
119
|
addInteractionController: interactionController.addInteractionController,
|
|
@@ -122,8 +128,10 @@ const builtInControllers = {
|
|
|
122
128
|
sendBulkMessageController: messageController.sendBulkMessageController,
|
|
123
129
|
sendBulkMessageAirtableController: messageController.sendBulkMessageAirtableController,
|
|
124
130
|
getLastInteractionController: messageController.getLastInteractionController,
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
addQualityVoteController: qualityMessageController.addQualityVoteController,
|
|
132
|
+
getQualityVotesByMessageController: qualityMessageController.getQualityVotesByMessageController,
|
|
133
|
+
getQualityVoteByMessageAndVoterController: qualityMessageController.getQualityVoteByMessageAndVoterController,
|
|
134
|
+
getQualityVotesByVoterController: qualityMessageController.getQualityVotesByVoterController,
|
|
127
135
|
|
|
128
136
|
// Patient controllers
|
|
129
137
|
getPatientInfoController: patientController.getPatientInfoController,
|