payservedb 8.2.1 → 8.2.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/index.js +3 -0
- package/package.json +1 -1
- package/src/models/agent_department.js +59 -0
- package/src/models/agent_role.js +77 -0
- package/src/models/agents.js +48 -3
- package/src/models/customer_satisfaction_survey.js +278 -0
- package/src/models/customer_tickets.js +86 -1
- package/src/models/tickets_category.js +12 -1
package/index.js
CHANGED
|
@@ -230,6 +230,9 @@ const models = {
|
|
|
230
230
|
AgentNotification: require("./src/models/agent_notifications"),
|
|
231
231
|
TicketCategory: require("./src/models/tickets_category"),
|
|
232
232
|
InspectionCategory: require("./src/models/inspection_category"),
|
|
233
|
+
AgentRole: require("./src/models/agent_roles"),
|
|
234
|
+
CustomerSatisfactionSurvey: require("./src/models/customer_satisfaction_survey"),
|
|
235
|
+
AgentDepartment: require("./src/models/agent_departments")
|
|
233
236
|
};
|
|
234
237
|
|
|
235
238
|
// Function to get models dynamically from a specific database connection
|
package/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const agentDepartmentSchema = new mongoose.Schema({
|
|
4
|
+
name: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: [true, 'Department name is required'],
|
|
7
|
+
trim: true,
|
|
8
|
+
maxlength: 100
|
|
9
|
+
},
|
|
10
|
+
code: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: [true, 'Department code is required'],
|
|
13
|
+
trim: true,
|
|
14
|
+
uppercase: true,
|
|
15
|
+
maxlength: 20
|
|
16
|
+
},
|
|
17
|
+
description: {
|
|
18
|
+
type: String,
|
|
19
|
+
trim: true,
|
|
20
|
+
maxlength: 500
|
|
21
|
+
},
|
|
22
|
+
active: {
|
|
23
|
+
type: Boolean,
|
|
24
|
+
default: true
|
|
25
|
+
},
|
|
26
|
+
archived_at: {
|
|
27
|
+
type: Date
|
|
28
|
+
},
|
|
29
|
+
archived_by: {
|
|
30
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
31
|
+
ref: 'User'
|
|
32
|
+
},
|
|
33
|
+
created_by: {
|
|
34
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
35
|
+
ref: 'User',
|
|
36
|
+
required: true
|
|
37
|
+
},
|
|
38
|
+
updated_by: {
|
|
39
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
40
|
+
ref: 'User'
|
|
41
|
+
},
|
|
42
|
+
created_at: {
|
|
43
|
+
type: Date,
|
|
44
|
+
default: Date.now
|
|
45
|
+
},
|
|
46
|
+
updated_at: {
|
|
47
|
+
type: Date,
|
|
48
|
+
default: Date.now
|
|
49
|
+
}
|
|
50
|
+
}, {
|
|
51
|
+
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Compound index for unique department code per tenant (if needed)
|
|
55
|
+
agentDepartmentSchema.index({ code: 1 }, { unique: true });
|
|
56
|
+
agentDepartmentSchema.index({ name: 1 });
|
|
57
|
+
agentDepartmentSchema.index({ active: 1 });
|
|
58
|
+
|
|
59
|
+
module.exports = mongoose.model('AgentDepartment', agentDepartmentSchema);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const agentRoleSchema = new mongoose.Schema({
|
|
4
|
+
name: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: [true, 'Role name is required'],
|
|
7
|
+
trim: true,
|
|
8
|
+
maxlength: 100
|
|
9
|
+
},
|
|
10
|
+
code: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: [true, 'Role code is required'],
|
|
13
|
+
trim: true,
|
|
14
|
+
lowercase: true,
|
|
15
|
+
maxlength: 50
|
|
16
|
+
},
|
|
17
|
+
description: {
|
|
18
|
+
type: String,
|
|
19
|
+
trim: true,
|
|
20
|
+
maxlength: 500
|
|
21
|
+
},
|
|
22
|
+
level: {
|
|
23
|
+
type: Number,
|
|
24
|
+
default: 1,
|
|
25
|
+
min: 1,
|
|
26
|
+
max: 10,
|
|
27
|
+
comment: 'Role hierarchy level (1=lowest, 10=highest)'
|
|
28
|
+
},
|
|
29
|
+
department: {
|
|
30
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
31
|
+
ref: 'AgentDepartment',
|
|
32
|
+
required: false
|
|
33
|
+
},
|
|
34
|
+
permissions: [{
|
|
35
|
+
type: String,
|
|
36
|
+
trim: true
|
|
37
|
+
}],
|
|
38
|
+
active: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
default: true
|
|
41
|
+
},
|
|
42
|
+
archived_at: {
|
|
43
|
+
type: Date
|
|
44
|
+
},
|
|
45
|
+
archived_by: {
|
|
46
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
47
|
+
ref: 'User'
|
|
48
|
+
},
|
|
49
|
+
created_by: {
|
|
50
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
51
|
+
ref: 'User',
|
|
52
|
+
required: true
|
|
53
|
+
},
|
|
54
|
+
updated_by: {
|
|
55
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
56
|
+
ref: 'User'
|
|
57
|
+
},
|
|
58
|
+
created_at: {
|
|
59
|
+
type: Date,
|
|
60
|
+
default: Date.now
|
|
61
|
+
},
|
|
62
|
+
updated_at: {
|
|
63
|
+
type: Date,
|
|
64
|
+
default: Date.now
|
|
65
|
+
}
|
|
66
|
+
}, {
|
|
67
|
+
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Compound index for unique role code
|
|
71
|
+
agentRoleSchema.index({ code: 1 }, { unique: true });
|
|
72
|
+
agentRoleSchema.index({ name: 1 });
|
|
73
|
+
agentRoleSchema.index({ active: 1 });
|
|
74
|
+
agentRoleSchema.index({ level: 1 });
|
|
75
|
+
agentRoleSchema.index({ department: 1 });
|
|
76
|
+
|
|
77
|
+
module.exports = mongoose.model('AgentRole', agentRoleSchema);
|
package/src/models/agents.js
CHANGED
|
@@ -41,11 +41,12 @@ const agentSchema = new mongoose.Schema({
|
|
|
41
41
|
},
|
|
42
42
|
role: {
|
|
43
43
|
type: String,
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
required: [true, 'Role is required'],
|
|
45
|
+
trim: true
|
|
46
46
|
},
|
|
47
47
|
department: {
|
|
48
48
|
type: String,
|
|
49
|
+
required: [true, 'Department is required'],
|
|
49
50
|
trim: true
|
|
50
51
|
},
|
|
51
52
|
team_id: {
|
|
@@ -58,9 +59,31 @@ const agentSchema = new mongoose.Schema({
|
|
|
58
59
|
},
|
|
59
60
|
status: {
|
|
60
61
|
type: String,
|
|
61
|
-
enum: ['active', 'inactive', 'on_break', 'offline'],
|
|
62
|
+
enum: ['active', 'inactive', 'on_break', 'offline', 'suspended', 'terminated'],
|
|
62
63
|
default: 'active'
|
|
63
64
|
},
|
|
65
|
+
suspended_at: {
|
|
66
|
+
type: Date
|
|
67
|
+
},
|
|
68
|
+
suspended_by: {
|
|
69
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
70
|
+
ref: 'User'
|
|
71
|
+
},
|
|
72
|
+
suspended_reason: {
|
|
73
|
+
type: String,
|
|
74
|
+
trim: true
|
|
75
|
+
},
|
|
76
|
+
terminated_at: {
|
|
77
|
+
type: Date
|
|
78
|
+
},
|
|
79
|
+
terminated_by: {
|
|
80
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
81
|
+
ref: 'User'
|
|
82
|
+
},
|
|
83
|
+
terminated_reason: {
|
|
84
|
+
type: String,
|
|
85
|
+
trim: true
|
|
86
|
+
},
|
|
64
87
|
is_available: {
|
|
65
88
|
type: Boolean,
|
|
66
89
|
default: false
|
|
@@ -83,6 +106,28 @@ const agentSchema = new mongoose.Schema({
|
|
|
83
106
|
default: 0,
|
|
84
107
|
min: 0
|
|
85
108
|
},
|
|
109
|
+
permissions: {
|
|
110
|
+
type: mongoose.Schema.Types.Mixed,
|
|
111
|
+
default: {}
|
|
112
|
+
},
|
|
113
|
+
profile_image: {
|
|
114
|
+
type: String,
|
|
115
|
+
required: false,
|
|
116
|
+
default: null
|
|
117
|
+
},
|
|
118
|
+
role_history: [{
|
|
119
|
+
previous_role: String,
|
|
120
|
+
new_role: String,
|
|
121
|
+
changed_by: {
|
|
122
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
123
|
+
ref: 'User'
|
|
124
|
+
},
|
|
125
|
+
changed_at: {
|
|
126
|
+
type: Date,
|
|
127
|
+
default: Date.now
|
|
128
|
+
},
|
|
129
|
+
reason: String
|
|
130
|
+
}],
|
|
86
131
|
created_at: {
|
|
87
132
|
type: Date,
|
|
88
133
|
default: Date.now
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const customerSatisfactionSurveySchema = new mongoose.Schema({
|
|
4
|
+
ticket_id: {
|
|
5
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
6
|
+
ref: 'CustomerTicket',
|
|
7
|
+
required: true,
|
|
8
|
+
unique: true
|
|
9
|
+
},
|
|
10
|
+
ticket_number: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: true
|
|
13
|
+
},
|
|
14
|
+
customer_id: {
|
|
15
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
16
|
+
ref: 'Customer',
|
|
17
|
+
required: true
|
|
18
|
+
},
|
|
19
|
+
facility_id: {
|
|
20
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
21
|
+
ref: 'Facility',
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
assigned_agent_id: {
|
|
25
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
26
|
+
ref: 'User'
|
|
27
|
+
},
|
|
28
|
+
custom_caller: {
|
|
29
|
+
name: {
|
|
30
|
+
type: String,
|
|
31
|
+
trim: true,
|
|
32
|
+
comment: 'Name of the person who reported the ticket (if different from customer)'
|
|
33
|
+
},
|
|
34
|
+
phone: {
|
|
35
|
+
type: String,
|
|
36
|
+
trim: true,
|
|
37
|
+
comment: 'Phone number of the custom caller'
|
|
38
|
+
},
|
|
39
|
+
email: {
|
|
40
|
+
type: String,
|
|
41
|
+
trim: true,
|
|
42
|
+
comment: 'Email of the custom caller'
|
|
43
|
+
},
|
|
44
|
+
relationship: {
|
|
45
|
+
type: String,
|
|
46
|
+
trim: true,
|
|
47
|
+
comment: 'Relationship to customer (e.g., spouse, agent, caretaker)'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
survey_token: {
|
|
51
|
+
type: String,
|
|
52
|
+
required: true,
|
|
53
|
+
unique: true,
|
|
54
|
+
index: true
|
|
55
|
+
},
|
|
56
|
+
// Survey Responses
|
|
57
|
+
overall_satisfaction: {
|
|
58
|
+
type: Number,
|
|
59
|
+
min: 1,
|
|
60
|
+
max: 5,
|
|
61
|
+
comment: 'Overall satisfaction rating (1-5 stars)'
|
|
62
|
+
},
|
|
63
|
+
resolution_quality: {
|
|
64
|
+
type: Number,
|
|
65
|
+
min: 1,
|
|
66
|
+
max: 5,
|
|
67
|
+
comment: 'Quality of resolution (1-5 stars)'
|
|
68
|
+
},
|
|
69
|
+
response_time_satisfaction: {
|
|
70
|
+
type: Number,
|
|
71
|
+
min: 1,
|
|
72
|
+
max: 5,
|
|
73
|
+
comment: 'Satisfaction with response time (1-5 stars)'
|
|
74
|
+
},
|
|
75
|
+
agent_professionalism: {
|
|
76
|
+
type: Number,
|
|
77
|
+
min: 1,
|
|
78
|
+
max: 5,
|
|
79
|
+
comment: 'Agent professionalism rating (1-5 stars)'
|
|
80
|
+
},
|
|
81
|
+
agent_knowledge: {
|
|
82
|
+
type: Number,
|
|
83
|
+
min: 1,
|
|
84
|
+
max: 5,
|
|
85
|
+
comment: 'Agent knowledge rating (1-5 stars)'
|
|
86
|
+
},
|
|
87
|
+
communication_clarity: {
|
|
88
|
+
type: Number,
|
|
89
|
+
min: 1,
|
|
90
|
+
max: 5,
|
|
91
|
+
comment: 'Communication clarity rating (1-5 stars)'
|
|
92
|
+
},
|
|
93
|
+
// NPS (Net Promoter Score)
|
|
94
|
+
would_recommend: {
|
|
95
|
+
type: Number,
|
|
96
|
+
min: 0,
|
|
97
|
+
max: 10,
|
|
98
|
+
comment: 'Likelihood to recommend (0-10 scale)'
|
|
99
|
+
},
|
|
100
|
+
// Open-ended feedback
|
|
101
|
+
positive_feedback: {
|
|
102
|
+
type: String,
|
|
103
|
+
maxlength: 1000,
|
|
104
|
+
comment: 'What did we do well?'
|
|
105
|
+
},
|
|
106
|
+
improvement_feedback: {
|
|
107
|
+
type: String,
|
|
108
|
+
maxlength: 1000,
|
|
109
|
+
comment: 'What could we improve?'
|
|
110
|
+
},
|
|
111
|
+
additional_comments: {
|
|
112
|
+
type: String,
|
|
113
|
+
maxlength: 2000
|
|
114
|
+
},
|
|
115
|
+
// Issue resolution
|
|
116
|
+
issue_fully_resolved: {
|
|
117
|
+
type: Boolean,
|
|
118
|
+
comment: 'Was the issue fully resolved?'
|
|
119
|
+
},
|
|
120
|
+
issue_resolution_details: {
|
|
121
|
+
type: String,
|
|
122
|
+
maxlength: 500,
|
|
123
|
+
comment: 'If not fully resolved, please explain'
|
|
124
|
+
},
|
|
125
|
+
// Metadata
|
|
126
|
+
status: {
|
|
127
|
+
type: String,
|
|
128
|
+
enum: ['pending', 'completed', 'expired'],
|
|
129
|
+
default: 'pending'
|
|
130
|
+
},
|
|
131
|
+
sent_at: {
|
|
132
|
+
type: Date,
|
|
133
|
+
default: Date.now
|
|
134
|
+
},
|
|
135
|
+
completed_at: {
|
|
136
|
+
type: Date
|
|
137
|
+
},
|
|
138
|
+
expired_at: {
|
|
139
|
+
type: Date,
|
|
140
|
+
comment: 'Survey expires 7 days after sending'
|
|
141
|
+
},
|
|
142
|
+
reminder_sent: {
|
|
143
|
+
type: Boolean,
|
|
144
|
+
default: false
|
|
145
|
+
},
|
|
146
|
+
reminder_sent_at: {
|
|
147
|
+
type: Date
|
|
148
|
+
},
|
|
149
|
+
ip_address: String,
|
|
150
|
+
user_agent: String,
|
|
151
|
+
response_time_seconds: {
|
|
152
|
+
type: Number,
|
|
153
|
+
comment: 'Time taken to complete survey in seconds'
|
|
154
|
+
},
|
|
155
|
+
created_at: {
|
|
156
|
+
type: Date,
|
|
157
|
+
default: Date.now
|
|
158
|
+
},
|
|
159
|
+
updated_at: {
|
|
160
|
+
type: Date,
|
|
161
|
+
default: Date.now
|
|
162
|
+
}
|
|
163
|
+
}, {
|
|
164
|
+
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Indexes for performance
|
|
168
|
+
customerSatisfactionSurveySchema.index({ survey_token: 1 });
|
|
169
|
+
customerSatisfactionSurveySchema.index({ customer_id: 1, created_at: -1 });
|
|
170
|
+
customerSatisfactionSurveySchema.index({ facility_id: 1, status: 1 });
|
|
171
|
+
customerSatisfactionSurveySchema.index({ assigned_agent_id: 1, status: 1 });
|
|
172
|
+
customerSatisfactionSurveySchema.index({ status: 1, expired_at: 1 });
|
|
173
|
+
customerSatisfactionSurveySchema.index({ ticket_id: 1 });
|
|
174
|
+
|
|
175
|
+
// Virtual for calculating NPS category
|
|
176
|
+
customerSatisfactionSurveySchema.virtual('nps_category').get(function() {
|
|
177
|
+
if (this.would_recommend === null || this.would_recommend === undefined) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (this.would_recommend >= 9) return 'promoter';
|
|
181
|
+
if (this.would_recommend >= 7) return 'passive';
|
|
182
|
+
return 'detractor';
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Virtual for calculating average rating
|
|
186
|
+
customerSatisfactionSurveySchema.virtual('average_rating').get(function() {
|
|
187
|
+
const ratings = [
|
|
188
|
+
this.overall_satisfaction,
|
|
189
|
+
this.resolution_quality,
|
|
190
|
+
this.response_time_satisfaction,
|
|
191
|
+
this.agent_professionalism,
|
|
192
|
+
this.agent_knowledge,
|
|
193
|
+
this.communication_clarity
|
|
194
|
+
].filter(r => r !== null && r !== undefined);
|
|
195
|
+
|
|
196
|
+
if (ratings.length === 0) return null;
|
|
197
|
+
return (ratings.reduce((sum, r) => sum + r, 0) / ratings.length).toFixed(2);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Method to check if survey is expired
|
|
201
|
+
customerSatisfactionSurveySchema.methods.isExpired = function() {
|
|
202
|
+
return this.expired_at && new Date() > this.expired_at;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Method to mark survey as completed
|
|
206
|
+
customerSatisfactionSurveySchema.methods.markCompleted = function() {
|
|
207
|
+
this.status = 'completed';
|
|
208
|
+
this.completed_at = new Date();
|
|
209
|
+
return this.save();
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Static method to get analytics
|
|
213
|
+
customerSatisfactionSurveySchema.statics.getAnalytics = async function(filters = {}) {
|
|
214
|
+
const match = { status: 'completed' };
|
|
215
|
+
|
|
216
|
+
if (filters.facility_id) match.facility_id = mongoose.Types.ObjectId(filters.facility_id);
|
|
217
|
+
if (filters.assigned_agent_id) match.assigned_agent_id = mongoose.Types.ObjectId(filters.assigned_agent_id);
|
|
218
|
+
if (filters.start_date) match.completed_at = { $gte: new Date(filters.start_date) };
|
|
219
|
+
if (filters.end_date) {
|
|
220
|
+
match.completed_at = match.completed_at || {};
|
|
221
|
+
match.completed_at.$lte = new Date(filters.end_date);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const analytics = await this.aggregate([
|
|
225
|
+
{ $match: match },
|
|
226
|
+
{
|
|
227
|
+
$group: {
|
|
228
|
+
_id: null,
|
|
229
|
+
total_responses: { $sum: 1 },
|
|
230
|
+
avg_overall_satisfaction: { $avg: '$overall_satisfaction' },
|
|
231
|
+
avg_resolution_quality: { $avg: '$resolution_quality' },
|
|
232
|
+
avg_response_time_satisfaction: { $avg: '$response_time_satisfaction' },
|
|
233
|
+
avg_agent_professionalism: { $avg: '$agent_professionalism' },
|
|
234
|
+
avg_agent_knowledge: { $avg: '$agent_knowledge' },
|
|
235
|
+
avg_communication_clarity: { $avg: '$communication_clarity' },
|
|
236
|
+
avg_nps: { $avg: '$would_recommend' },
|
|
237
|
+
fully_resolved_count: {
|
|
238
|
+
$sum: { $cond: ['$issue_fully_resolved', 1, 0] }
|
|
239
|
+
},
|
|
240
|
+
promoters: {
|
|
241
|
+
$sum: { $cond: [{ $gte: ['$would_recommend', 9] }, 1, 0] }
|
|
242
|
+
},
|
|
243
|
+
passives: {
|
|
244
|
+
$sum: { $cond: [{ $and: [{ $gte: ['$would_recommend', 7] }, { $lt: ['$would_recommend', 9] }] }, 1, 0] }
|
|
245
|
+
},
|
|
246
|
+
detractors: {
|
|
247
|
+
$sum: { $cond: [{ $lt: ['$would_recommend', 7] }, 1, 0] }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
if (analytics.length === 0) {
|
|
254
|
+
return {
|
|
255
|
+
total_responses: 0,
|
|
256
|
+
avg_overall_satisfaction: 0,
|
|
257
|
+
nps_score: 0
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const result = analytics[0];
|
|
262
|
+
const total = result.total_responses;
|
|
263
|
+
|
|
264
|
+
// Calculate NPS score: (% Promoters - % Detractors)
|
|
265
|
+
const nps_score = total > 0
|
|
266
|
+
? Math.round(((result.promoters - result.detractors) / total) * 100)
|
|
267
|
+
: 0;
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
...result,
|
|
271
|
+
nps_score,
|
|
272
|
+
resolution_rate: total > 0
|
|
273
|
+
? Math.round((result.fully_resolved_count / total) * 100)
|
|
274
|
+
: 0
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
module.exports = mongoose.model('CustomerSatisfactionSurvey', customerSatisfactionSurveySchema);
|
|
@@ -42,7 +42,7 @@ const customerTicketSchema = new mongoose.Schema({
|
|
|
42
42
|
},
|
|
43
43
|
status: {
|
|
44
44
|
type: String,
|
|
45
|
-
enum: ['open', 'in_progress', 'pending_customer', 'escalated', 'resolved', 'closed', 'reopened'],
|
|
45
|
+
enum: ['open', 'in_progress', 'pending_customer', 'escalated', 'resolved', 'closed', 'reopened', 'archived'],
|
|
46
46
|
default: 'open'
|
|
47
47
|
},
|
|
48
48
|
category_id: {
|
|
@@ -55,6 +55,39 @@ const customerTicketSchema = new mongoose.Schema({
|
|
|
55
55
|
enum: ['portal', 'email', 'phone', 'chat', 'in_person', 'mobile_app'],
|
|
56
56
|
default: 'portal'
|
|
57
57
|
},
|
|
58
|
+
custom_caller: {
|
|
59
|
+
name: {
|
|
60
|
+
type: String,
|
|
61
|
+
trim: true
|
|
62
|
+
},
|
|
63
|
+
phone: {
|
|
64
|
+
type: String,
|
|
65
|
+
trim: true
|
|
66
|
+
},
|
|
67
|
+
email: {
|
|
68
|
+
type: String,
|
|
69
|
+
trim: true
|
|
70
|
+
},
|
|
71
|
+
relationship: {
|
|
72
|
+
type: String,
|
|
73
|
+
trim: true,
|
|
74
|
+
comment: 'Relationship to customer (e.g., spouse, agent, caretaker)'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
survey_token: {
|
|
78
|
+
type: String,
|
|
79
|
+
unique: true,
|
|
80
|
+
sparse: true,
|
|
81
|
+
comment: 'Unique token for customer survey link'
|
|
82
|
+
},
|
|
83
|
+
survey_sent_at: {
|
|
84
|
+
type: Date,
|
|
85
|
+
comment: 'When the survey invitation was sent'
|
|
86
|
+
},
|
|
87
|
+
survey_completed_at: {
|
|
88
|
+
type: Date,
|
|
89
|
+
comment: 'When the customer completed the survey'
|
|
90
|
+
},
|
|
58
91
|
sla_status: {
|
|
59
92
|
type: String,
|
|
60
93
|
enum: ['within_sla', 'approaching_breach', 'breached'],
|
|
@@ -131,6 +164,58 @@ const customerTicketSchema = new mongoose.Schema({
|
|
|
131
164
|
closed_at: {
|
|
132
165
|
type: Date
|
|
133
166
|
},
|
|
167
|
+
escalated_at: {
|
|
168
|
+
type: Date
|
|
169
|
+
},
|
|
170
|
+
escalated_to: {
|
|
171
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
172
|
+
ref: 'User'
|
|
173
|
+
},
|
|
174
|
+
escalation_reason: {
|
|
175
|
+
type: String
|
|
176
|
+
},
|
|
177
|
+
audit_log: [{
|
|
178
|
+
user_id: {
|
|
179
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
180
|
+
ref: 'User'
|
|
181
|
+
},
|
|
182
|
+
user_name: String,
|
|
183
|
+
user_role: String,
|
|
184
|
+
action: {
|
|
185
|
+
type: String,
|
|
186
|
+
enum: [
|
|
187
|
+
'created', 'updated', 'status_changed', 'assigned', 'reassigned',
|
|
188
|
+
'escalated', 'auto_escalated', 'resolved', 'closed', 'reopened',
|
|
189
|
+
'category_changed', 'priority_changed', 'sla_updated',
|
|
190
|
+
'interaction_added', 'attachment_added', 'tag_added', 'tag_removed',
|
|
191
|
+
'customer_response', 'agent_note'
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
field_changed: String,
|
|
195
|
+
old_value: mongoose.Schema.Types.Mixed,
|
|
196
|
+
new_value: mongoose.Schema.Types.Mixed,
|
|
197
|
+
description: String,
|
|
198
|
+
ip_address: String,
|
|
199
|
+
user_agent: String,
|
|
200
|
+
metadata: mongoose.Schema.Types.Mixed,
|
|
201
|
+
timestamp: {
|
|
202
|
+
type: Date,
|
|
203
|
+
default: Date.now
|
|
204
|
+
}
|
|
205
|
+
}],
|
|
206
|
+
activity_log: [{
|
|
207
|
+
agent_id: {
|
|
208
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
209
|
+
ref: 'User'
|
|
210
|
+
},
|
|
211
|
+
action: String,
|
|
212
|
+
description: String,
|
|
213
|
+
metadata: mongoose.Schema.Types.Mixed,
|
|
214
|
+
timestamp: {
|
|
215
|
+
type: Date,
|
|
216
|
+
default: Date.now
|
|
217
|
+
}
|
|
218
|
+
}],
|
|
134
219
|
created_at: {
|
|
135
220
|
type: Date,
|
|
136
221
|
default: Date.now
|
|
@@ -19,7 +19,7 @@ const ticketCategorySchema = new mongoose.Schema({
|
|
|
19
19
|
enum: ['low', 'medium', 'high', 'urgent', 'critical'],
|
|
20
20
|
default: 'medium'
|
|
21
21
|
},
|
|
22
|
-
|
|
22
|
+
sla_minutes: {
|
|
23
23
|
type: Number,
|
|
24
24
|
required: true,
|
|
25
25
|
min: 1
|
|
@@ -33,6 +33,17 @@ const ticketCategorySchema = new mongoose.Schema({
|
|
|
33
33
|
type: Boolean,
|
|
34
34
|
default: true
|
|
35
35
|
},
|
|
36
|
+
active: {
|
|
37
|
+
type: Boolean,
|
|
38
|
+
default: true
|
|
39
|
+
},
|
|
40
|
+
archived_at: {
|
|
41
|
+
type: Date
|
|
42
|
+
},
|
|
43
|
+
archived_by: {
|
|
44
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
45
|
+
ref: 'User'
|
|
46
|
+
},
|
|
36
47
|
created_by: {
|
|
37
48
|
type: mongoose.Schema.Types.ObjectId,
|
|
38
49
|
ref: 'User',
|