@astralibx/call-log-engine 0.2.0 → 0.3.0
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/README.md +29 -4
- package/dist/index.cjs +517 -227
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +57 -13
- package/dist/index.d.ts +57 -13
- package/dist/index.mjs +515 -227
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
var zod = require('zod');
|
|
4
4
|
var core = require('@astralibx/core');
|
|
5
5
|
var callLogTypes = require('@astralibx/call-log-types');
|
|
6
|
-
var
|
|
6
|
+
var crypto6 = require('crypto');
|
|
7
7
|
var mongoose = require('mongoose');
|
|
8
8
|
var express = require('express');
|
|
9
9
|
|
|
10
10
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
11
|
|
|
12
|
-
var
|
|
12
|
+
var crypto6__default = /*#__PURE__*/_interopDefault(crypto6);
|
|
13
13
|
|
|
14
14
|
// src/index.ts
|
|
15
15
|
var PipelineStageSchema = new mongoose.Schema(
|
|
16
16
|
{
|
|
17
|
-
stageId: { type: String, required: true, default: () =>
|
|
17
|
+
stageId: { type: String, required: true, default: () => crypto6__default.default.randomUUID() },
|
|
18
18
|
name: { type: String, required: true },
|
|
19
19
|
color: { type: String, required: true },
|
|
20
20
|
order: { type: Number, required: true },
|
|
@@ -25,7 +25,7 @@ var PipelineStageSchema = new mongoose.Schema(
|
|
|
25
25
|
);
|
|
26
26
|
var PipelineSchema = new mongoose.Schema(
|
|
27
27
|
{
|
|
28
|
-
pipelineId: { type: String, required: true, unique: true, default: () =>
|
|
28
|
+
pipelineId: { type: String, required: true, unique: true, default: () => crypto6__default.default.randomUUID() },
|
|
29
29
|
name: { type: String, required: true },
|
|
30
30
|
description: { type: String },
|
|
31
31
|
stages: { type: [PipelineStageSchema], required: true },
|
|
@@ -55,7 +55,7 @@ var ContactRefSchema = new mongoose.Schema(
|
|
|
55
55
|
);
|
|
56
56
|
var TimelineEntrySchema = new mongoose.Schema(
|
|
57
57
|
{
|
|
58
|
-
entryId: { type: String, required: true, default: () =>
|
|
58
|
+
entryId: { type: String, required: true, default: () => crypto6__default.default.randomUUID() },
|
|
59
59
|
type: {
|
|
60
60
|
type: String,
|
|
61
61
|
required: true,
|
|
@@ -90,7 +90,7 @@ var StageChangeSchema = new mongoose.Schema(
|
|
|
90
90
|
);
|
|
91
91
|
var CallLogSchema = new mongoose.Schema(
|
|
92
92
|
{
|
|
93
|
-
callLogId: { type: String, required: true, unique: true, default: () =>
|
|
93
|
+
callLogId: { type: String, required: true, unique: true, default: () => crypto6__default.default.randomUUID() },
|
|
94
94
|
pipelineId: { type: String, required: true, index: true },
|
|
95
95
|
currentStageId: { type: String, required: true, index: true },
|
|
96
96
|
contactRef: { type: ContactRefSchema, required: true },
|
|
@@ -115,6 +115,11 @@ var CallLogSchema = new mongoose.Schema(
|
|
|
115
115
|
timeline: { type: [TimelineEntrySchema], default: [] },
|
|
116
116
|
stageHistory: { type: [StageChangeSchema], default: [] },
|
|
117
117
|
durationMinutes: { type: Number, min: 0 },
|
|
118
|
+
channel: { type: String, default: "phone", index: true },
|
|
119
|
+
outcome: { type: String, default: "pending", index: true },
|
|
120
|
+
isFollowUp: { type: Boolean, default: false, index: true },
|
|
121
|
+
isDeleted: { type: Boolean, default: false },
|
|
122
|
+
deletedAt: { type: Date },
|
|
118
123
|
isClosed: { type: Boolean, default: false, index: true },
|
|
119
124
|
closedAt: { type: Date },
|
|
120
125
|
tenantId: { type: String, sparse: true },
|
|
@@ -127,6 +132,7 @@ var CallLogSchema = new mongoose.Schema(
|
|
|
127
132
|
CallLogSchema.index({ "contactRef.externalId": 1 });
|
|
128
133
|
CallLogSchema.index({ pipelineId: 1, currentStageId: 1 });
|
|
129
134
|
CallLogSchema.index({ agentId: 1, isClosed: 1 });
|
|
135
|
+
CallLogSchema.index({ isDeleted: 1, isClosed: 1 });
|
|
130
136
|
function createCallLogModel(connection, prefix) {
|
|
131
137
|
const collectionName = prefix ? `${prefix}_call_logs` : "call_logs";
|
|
132
138
|
return connection.model("CallLog", CallLogSchema, collectionName);
|
|
@@ -140,6 +146,7 @@ var ERROR_CODE = {
|
|
|
140
146
|
StageInUse: "CALL_STAGE_IN_USE",
|
|
141
147
|
CallLogNotFound: "CALL_LOG_NOT_FOUND",
|
|
142
148
|
CallLogClosed: "CALL_LOG_CLOSED",
|
|
149
|
+
CallLogDeleted: "CALL_LOG_DELETED",
|
|
143
150
|
ContactNotFound: "CALL_CONTACT_NOT_FOUND",
|
|
144
151
|
AgentCapacityFull: "CALL_AGENT_CAPACITY_FULL",
|
|
145
152
|
InvalidConfig: "CALL_INVALID_CONFIG",
|
|
@@ -154,6 +161,7 @@ var ERROR_MESSAGE = {
|
|
|
154
161
|
StageInUse: "Cannot remove stage that has active calls",
|
|
155
162
|
CallLogNotFound: "Call log not found",
|
|
156
163
|
CallLogClosed: "Cannot modify a closed call log",
|
|
164
|
+
CallLogDeleted: "Call log has been deleted",
|
|
157
165
|
ContactNotFound: "Contact not found",
|
|
158
166
|
AgentCapacityFull: "Agent has reached maximum concurrent calls",
|
|
159
167
|
AuthFailed: "Authentication failed"
|
|
@@ -179,7 +187,9 @@ var SYSTEM_TIMELINE_FN = {
|
|
|
179
187
|
stageChanged: (from, to) => `Stage changed from "${from}" to "${to}"`,
|
|
180
188
|
callAssigned: (agentName) => `Call assigned to ${agentName}`,
|
|
181
189
|
callReassigned: (from, to) => `Call reassigned from ${from} to ${to}`,
|
|
182
|
-
followUpSet: (date) => `Follow-up scheduled for ${date}
|
|
190
|
+
followUpSet: (date) => `Follow-up scheduled for ${date}`,
|
|
191
|
+
callDeleted: (agentName) => `Call deleted by ${agentName}`,
|
|
192
|
+
followUpCallCreated: () => "This call is a follow-up to a previous interaction"
|
|
183
193
|
};
|
|
184
194
|
|
|
185
195
|
// src/schemas/call-log-settings.schema.ts
|
|
@@ -203,6 +213,8 @@ var CallLogSettingsSchema = new mongoose.Schema(
|
|
|
203
213
|
key: { type: String, required: true, default: "global" },
|
|
204
214
|
availableTags: { type: [String], default: [] },
|
|
205
215
|
availableCategories: { type: [String], default: [] },
|
|
216
|
+
availableChannels: { type: [String], default: ["phone", "whatsapp", "telegram", "in_app_chat"] },
|
|
217
|
+
availableOutcomes: { type: [String], default: ["pending", "interested", "not_interested", "no_answer", "busy", "callback_requested", "subscribed", "complaint"] },
|
|
206
218
|
priorityLevels: {
|
|
207
219
|
type: [PriorityConfigSchema],
|
|
208
220
|
default: () => DEFAULT_PRIORITY_LEVELS.map((p) => ({ ...p }))
|
|
@@ -465,10 +477,10 @@ var PipelineService = class {
|
|
|
465
477
|
async create(data) {
|
|
466
478
|
const stages = data.stages.map((s) => ({
|
|
467
479
|
...s,
|
|
468
|
-
stageId:
|
|
480
|
+
stageId: crypto6__default.default.randomUUID()
|
|
469
481
|
}));
|
|
470
482
|
validatePipelineStages(stages);
|
|
471
|
-
const pipelineId =
|
|
483
|
+
const pipelineId = crypto6__default.default.randomUUID();
|
|
472
484
|
if (data.isDefault) {
|
|
473
485
|
await this.Pipeline.updateMany(
|
|
474
486
|
{ ...this.tenantFilter, isDefault: true },
|
|
@@ -537,7 +549,7 @@ var PipelineService = class {
|
|
|
537
549
|
const pipeline = await this.get(pipelineId);
|
|
538
550
|
const newStage = {
|
|
539
551
|
...stage,
|
|
540
|
-
stageId:
|
|
552
|
+
stageId: crypto6__default.default.randomUUID()
|
|
541
553
|
};
|
|
542
554
|
const updatedStages = [...pipeline.stages, newStage];
|
|
543
555
|
validatePipelineStages(updatedStages, pipelineId);
|
|
@@ -636,7 +648,7 @@ var TimelineService = class {
|
|
|
636
648
|
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
637
649
|
if (callLog.isClosed) throw new CallLogClosedError(callLogId, "add note");
|
|
638
650
|
const entry = {
|
|
639
|
-
entryId:
|
|
651
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
640
652
|
type: callLogTypes.TimelineEntryType.Note,
|
|
641
653
|
content,
|
|
642
654
|
authorId,
|
|
@@ -661,7 +673,7 @@ var TimelineService = class {
|
|
|
661
673
|
const callLog = await this.CallLog.findOne({ callLogId });
|
|
662
674
|
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
663
675
|
const entry = {
|
|
664
|
-
entryId:
|
|
676
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
665
677
|
type: callLogTypes.TimelineEntryType.System,
|
|
666
678
|
content,
|
|
667
679
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -730,13 +742,23 @@ var CallLogService = class {
|
|
|
730
742
|
if (!pipeline) throw new PipelineNotFoundError(data.pipelineId);
|
|
731
743
|
const defaultStage = pipeline.stages.find((s) => s.isDefault);
|
|
732
744
|
if (!defaultStage) throw new PipelineNotFoundError(data.pipelineId);
|
|
733
|
-
const callLogId =
|
|
745
|
+
const callLogId = crypto6__default.default.randomUUID();
|
|
734
746
|
const initialEntry = {
|
|
735
|
-
entryId:
|
|
747
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
736
748
|
type: callLogTypes.TimelineEntryType.System,
|
|
737
749
|
content: SYSTEM_TIMELINE.CallCreated,
|
|
738
750
|
createdAt: /* @__PURE__ */ new Date()
|
|
739
751
|
};
|
|
752
|
+
const timelineEntries = [initialEntry];
|
|
753
|
+
if (data.isFollowUp) {
|
|
754
|
+
const followUpEntry = {
|
|
755
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
756
|
+
type: callLogTypes.TimelineEntryType.System,
|
|
757
|
+
content: SYSTEM_TIMELINE_FN.followUpCallCreated(),
|
|
758
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
759
|
+
};
|
|
760
|
+
timelineEntries.push(followUpEntry);
|
|
761
|
+
}
|
|
740
762
|
const callLog = await this.CallLog.create({
|
|
741
763
|
callLogId,
|
|
742
764
|
pipelineId: data.pipelineId,
|
|
@@ -750,7 +772,10 @@ var CallLogService = class {
|
|
|
750
772
|
category: data.category,
|
|
751
773
|
nextFollowUpDate: data.nextFollowUpDate,
|
|
752
774
|
durationMinutes: data.durationMinutes,
|
|
753
|
-
|
|
775
|
+
channel: data.channel,
|
|
776
|
+
outcome: data.outcome,
|
|
777
|
+
isFollowUp: data.isFollowUp ?? false,
|
|
778
|
+
timeline: timelineEntries,
|
|
754
779
|
stageHistory: [],
|
|
755
780
|
isClosed: false,
|
|
756
781
|
...data.tenantId ? { tenantId: data.tenantId } : {},
|
|
@@ -772,6 +797,9 @@ var CallLogService = class {
|
|
|
772
797
|
if (data.tags !== void 0) setFields.tags = data.tags;
|
|
773
798
|
if (data.category !== void 0) setFields.category = data.category;
|
|
774
799
|
if (data.durationMinutes !== void 0) setFields.durationMinutes = data.durationMinutes;
|
|
800
|
+
if (data.channel !== void 0) setFields.channel = data.channel;
|
|
801
|
+
if (data.outcome !== void 0) setFields.outcome = data.outcome;
|
|
802
|
+
if (data.isFollowUp !== void 0) setFields.isFollowUp = data.isFollowUp;
|
|
775
803
|
if (data.nextFollowUpDate !== void 0) {
|
|
776
804
|
const prevDate = callLog.nextFollowUpDate?.toISOString();
|
|
777
805
|
const newDate = data.nextFollowUpDate?.toISOString();
|
|
@@ -779,7 +807,7 @@ var CallLogService = class {
|
|
|
779
807
|
setFields.nextFollowUpDate = data.nextFollowUpDate;
|
|
780
808
|
if (data.nextFollowUpDate) {
|
|
781
809
|
const followUpEntry = {
|
|
782
|
-
entryId:
|
|
810
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
783
811
|
type: callLogTypes.TimelineEntryType.FollowUpSet,
|
|
784
812
|
content: SYSTEM_TIMELINE_FN.followUpSet(new Date(data.nextFollowUpDate).toISOString()),
|
|
785
813
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -806,6 +834,58 @@ var CallLogService = class {
|
|
|
806
834
|
this.logger.info("Call log updated", { callLogId, fields: Object.keys(data) });
|
|
807
835
|
return updated;
|
|
808
836
|
}
|
|
837
|
+
async list(filter = {}) {
|
|
838
|
+
const query = {};
|
|
839
|
+
if (filter.pipelineId) query.pipelineId = filter.pipelineId;
|
|
840
|
+
if (filter.currentStageId) query.currentStageId = filter.currentStageId;
|
|
841
|
+
if (filter.agentId) query.agentId = filter.agentId;
|
|
842
|
+
if (filter.tags && filter.tags.length > 0) query.tags = { $in: filter.tags };
|
|
843
|
+
if (filter.category) query.category = filter.category;
|
|
844
|
+
if (filter.isClosed !== void 0) query.isClosed = filter.isClosed;
|
|
845
|
+
if (filter.contactExternalId) query["contactRef.externalId"] = filter.contactExternalId;
|
|
846
|
+
if (filter.contactName) query["contactRef.displayName"] = { $regex: filter.contactName, $options: "i" };
|
|
847
|
+
if (filter.contactPhone) query["contactRef.phone"] = { $regex: `^${filter.contactPhone}` };
|
|
848
|
+
if (filter.contactEmail) query["contactRef.email"] = filter.contactEmail;
|
|
849
|
+
if (filter.priority) query.priority = filter.priority;
|
|
850
|
+
if (filter.direction) query.direction = filter.direction;
|
|
851
|
+
if (filter.channel) query.channel = filter.channel;
|
|
852
|
+
if (filter.outcome) query.outcome = filter.outcome;
|
|
853
|
+
if (filter.isFollowUp !== void 0) query.isFollowUp = filter.isFollowUp;
|
|
854
|
+
if (!filter.includeDeleted) query.isDeleted = { $ne: true };
|
|
855
|
+
if (filter.dateRange?.from || filter.dateRange?.to) {
|
|
856
|
+
const dateFilter = {};
|
|
857
|
+
if (filter.dateRange.from) dateFilter.$gte = new Date(filter.dateRange.from);
|
|
858
|
+
if (filter.dateRange.to) dateFilter.$lte = new Date(filter.dateRange.to);
|
|
859
|
+
query.callDate = dateFilter;
|
|
860
|
+
}
|
|
861
|
+
const page = filter.page ?? 1;
|
|
862
|
+
const limit = filter.limit ?? 20;
|
|
863
|
+
const skip = (page - 1) * limit;
|
|
864
|
+
const sort = filter.sort ?? { callDate: -1 };
|
|
865
|
+
const [callLogs, total] = await Promise.all([
|
|
866
|
+
this.CallLog.find(query).sort(sort).skip(skip).limit(limit),
|
|
867
|
+
this.CallLog.countDocuments(query)
|
|
868
|
+
]);
|
|
869
|
+
return { callLogs, total, page, limit };
|
|
870
|
+
}
|
|
871
|
+
async get(callLogId) {
|
|
872
|
+
const callLog = await this.CallLog.findOne({ callLogId, isDeleted: { $ne: true } });
|
|
873
|
+
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
874
|
+
return callLog;
|
|
875
|
+
}
|
|
876
|
+
async getByContact(externalId) {
|
|
877
|
+
return this.CallLog.find({ "contactRef.externalId": externalId, isDeleted: { $ne: true } }).sort({ callDate: -1 });
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
var CallLogLifecycleService = class {
|
|
881
|
+
constructor(CallLog, Pipeline, timeline, logger, hooks, options) {
|
|
882
|
+
this.CallLog = CallLog;
|
|
883
|
+
this.Pipeline = Pipeline;
|
|
884
|
+
this.timeline = timeline;
|
|
885
|
+
this.logger = logger;
|
|
886
|
+
this.hooks = hooks;
|
|
887
|
+
this.options = options;
|
|
888
|
+
}
|
|
809
889
|
async changeStage(callLogId, newStageId, agentId) {
|
|
810
890
|
const callLog = await this.CallLog.findOne({ callLogId });
|
|
811
891
|
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
@@ -822,7 +902,7 @@ var CallLogService = class {
|
|
|
822
902
|
const stageStartTime = lastHistory?.changedAt?.getTime() ?? callLog.createdAt?.getTime() ?? now.getTime();
|
|
823
903
|
const timeInStageMs = now.getTime() - stageStartTime;
|
|
824
904
|
const stageChangeEntry = {
|
|
825
|
-
entryId:
|
|
905
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
826
906
|
type: callLogTypes.TimelineEntryType.StageChange,
|
|
827
907
|
content: SYSTEM_TIMELINE_FN.stageChanged(currentStage.name, newStage.name),
|
|
828
908
|
fromStageId: currentStageId,
|
|
@@ -871,7 +951,7 @@ var CallLogService = class {
|
|
|
871
951
|
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
872
952
|
const previousAgentId = callLog.agentId?.toString();
|
|
873
953
|
const assignmentEntry = {
|
|
874
|
-
entryId:
|
|
954
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
875
955
|
type: callLogTypes.TimelineEntryType.Assignment,
|
|
876
956
|
content: previousAgentId ? SYSTEM_TIMELINE_FN.callReassigned(previousAgentId, agentId) : SYSTEM_TIMELINE_FN.callAssigned(agentId),
|
|
877
957
|
toAgentId: agentId,
|
|
@@ -899,88 +979,13 @@ var CallLogService = class {
|
|
|
899
979
|
}
|
|
900
980
|
return updated;
|
|
901
981
|
}
|
|
902
|
-
async list(filter = {}) {
|
|
903
|
-
const query = {};
|
|
904
|
-
if (filter.pipelineId) query.pipelineId = filter.pipelineId;
|
|
905
|
-
if (filter.currentStageId) query.currentStageId = filter.currentStageId;
|
|
906
|
-
if (filter.agentId) query.agentId = filter.agentId;
|
|
907
|
-
if (filter.tags && filter.tags.length > 0) query.tags = { $in: filter.tags };
|
|
908
|
-
if (filter.category) query.category = filter.category;
|
|
909
|
-
if (filter.isClosed !== void 0) query.isClosed = filter.isClosed;
|
|
910
|
-
if (filter.contactExternalId) query["contactRef.externalId"] = filter.contactExternalId;
|
|
911
|
-
if (filter.contactName) query["contactRef.displayName"] = { $regex: filter.contactName, $options: "i" };
|
|
912
|
-
if (filter.contactPhone) query["contactRef.phone"] = { $regex: `^${filter.contactPhone}` };
|
|
913
|
-
if (filter.contactEmail) query["contactRef.email"] = filter.contactEmail;
|
|
914
|
-
if (filter.priority) query.priority = filter.priority;
|
|
915
|
-
if (filter.direction) query.direction = filter.direction;
|
|
916
|
-
if (filter.dateRange?.from || filter.dateRange?.to) {
|
|
917
|
-
const dateFilter = {};
|
|
918
|
-
if (filter.dateRange.from) dateFilter.$gte = new Date(filter.dateRange.from);
|
|
919
|
-
if (filter.dateRange.to) dateFilter.$lte = new Date(filter.dateRange.to);
|
|
920
|
-
query.callDate = dateFilter;
|
|
921
|
-
}
|
|
922
|
-
const page = filter.page ?? 1;
|
|
923
|
-
const limit = filter.limit ?? 20;
|
|
924
|
-
const skip = (page - 1) * limit;
|
|
925
|
-
const sort = filter.sort ?? { callDate: -1 };
|
|
926
|
-
const [callLogs, total] = await Promise.all([
|
|
927
|
-
this.CallLog.find(query).sort(sort).skip(skip).limit(limit),
|
|
928
|
-
this.CallLog.countDocuments(query)
|
|
929
|
-
]);
|
|
930
|
-
return { callLogs, total, page, limit };
|
|
931
|
-
}
|
|
932
|
-
async get(callLogId) {
|
|
933
|
-
const callLog = await this.CallLog.findOne({ callLogId });
|
|
934
|
-
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
935
|
-
return callLog;
|
|
936
|
-
}
|
|
937
|
-
async getByContact(externalId) {
|
|
938
|
-
return this.CallLog.find({ "contactRef.externalId": externalId }).sort({ callDate: -1 });
|
|
939
|
-
}
|
|
940
|
-
async getFollowUpsDue(agentId, dateRange) {
|
|
941
|
-
const now = /* @__PURE__ */ new Date();
|
|
942
|
-
const query = {
|
|
943
|
-
nextFollowUpDate: { $lte: now },
|
|
944
|
-
isClosed: false,
|
|
945
|
-
followUpNotifiedAt: null
|
|
946
|
-
};
|
|
947
|
-
if (agentId) query.agentId = agentId;
|
|
948
|
-
if (dateRange?.from || dateRange?.to) {
|
|
949
|
-
const dateFilter = {};
|
|
950
|
-
if (dateRange.from) dateFilter.$gte = new Date(dateRange.from);
|
|
951
|
-
if (dateRange.to) dateFilter.$lte = new Date(dateRange.to);
|
|
952
|
-
query.nextFollowUpDate = { ...query.nextFollowUpDate, ...dateFilter };
|
|
953
|
-
}
|
|
954
|
-
return this.CallLog.find(query).sort({ nextFollowUpDate: 1 });
|
|
955
|
-
}
|
|
956
|
-
async bulkChangeStage(callLogIds, newStageId, agentId) {
|
|
957
|
-
const succeeded = [];
|
|
958
|
-
const failed = [];
|
|
959
|
-
for (const callLogId of callLogIds) {
|
|
960
|
-
try {
|
|
961
|
-
await this.changeStage(callLogId, newStageId, agentId);
|
|
962
|
-
succeeded.push(callLogId);
|
|
963
|
-
} catch (err) {
|
|
964
|
-
failed.push({
|
|
965
|
-
callLogId,
|
|
966
|
-
error: err instanceof Error ? err.message : String(err)
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
this.logger.info("Bulk stage change completed", {
|
|
971
|
-
total: callLogIds.length,
|
|
972
|
-
succeeded: succeeded.length,
|
|
973
|
-
failed: failed.length
|
|
974
|
-
});
|
|
975
|
-
return { succeeded, failed, total: callLogIds.length };
|
|
976
|
-
}
|
|
977
982
|
async close(callLogId, agentId) {
|
|
978
983
|
const callLog = await this.CallLog.findOne({ callLogId });
|
|
979
984
|
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
980
985
|
if (callLog.isClosed) throw new CallLogClosedError(callLogId, "close");
|
|
981
986
|
const now = /* @__PURE__ */ new Date();
|
|
982
987
|
const closeEntry = {
|
|
983
|
-
entryId:
|
|
988
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
984
989
|
type: callLogTypes.TimelineEntryType.System,
|
|
985
990
|
content: SYSTEM_TIMELINE.CallClosed,
|
|
986
991
|
authorId: agentId,
|
|
@@ -1012,7 +1017,7 @@ var CallLogService = class {
|
|
|
1012
1017
|
if (!callLog.isClosed) throw new CallLogClosedError(callLogId, "reopen");
|
|
1013
1018
|
const now = /* @__PURE__ */ new Date();
|
|
1014
1019
|
const reopenEntry = {
|
|
1015
|
-
entryId:
|
|
1020
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
1016
1021
|
type: callLogTypes.TimelineEntryType.System,
|
|
1017
1022
|
content: SYSTEM_TIMELINE.CallReopened,
|
|
1018
1023
|
authorId: agentId,
|
|
@@ -1035,6 +1040,72 @@ var CallLogService = class {
|
|
|
1035
1040
|
this.logger.info("Call log reopened", { callLogId, agentId });
|
|
1036
1041
|
return updated;
|
|
1037
1042
|
}
|
|
1043
|
+
async bulkChangeStage(callLogIds, newStageId, agentId) {
|
|
1044
|
+
const succeeded = [];
|
|
1045
|
+
const failed = [];
|
|
1046
|
+
for (const callLogId of callLogIds) {
|
|
1047
|
+
try {
|
|
1048
|
+
await this.changeStage(callLogId, newStageId, agentId);
|
|
1049
|
+
succeeded.push(callLogId);
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
failed.push({
|
|
1052
|
+
callLogId,
|
|
1053
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
this.logger.info("Bulk stage change completed", {
|
|
1058
|
+
total: callLogIds.length,
|
|
1059
|
+
succeeded: succeeded.length,
|
|
1060
|
+
failed: failed.length
|
|
1061
|
+
});
|
|
1062
|
+
return { succeeded, failed, total: callLogIds.length };
|
|
1063
|
+
}
|
|
1064
|
+
async getFollowUpsDue(agentId, dateRange) {
|
|
1065
|
+
const now = /* @__PURE__ */ new Date();
|
|
1066
|
+
const query = {
|
|
1067
|
+
nextFollowUpDate: { $lte: now },
|
|
1068
|
+
isClosed: false,
|
|
1069
|
+
followUpNotifiedAt: null,
|
|
1070
|
+
isDeleted: { $ne: true }
|
|
1071
|
+
};
|
|
1072
|
+
if (agentId) query.agentId = agentId;
|
|
1073
|
+
if (dateRange?.from || dateRange?.to) {
|
|
1074
|
+
const dateFilter = {};
|
|
1075
|
+
if (dateRange.from) dateFilter.$gte = new Date(dateRange.from);
|
|
1076
|
+
if (dateRange.to) dateFilter.$lte = new Date(dateRange.to);
|
|
1077
|
+
query.nextFollowUpDate = { ...query.nextFollowUpDate, ...dateFilter };
|
|
1078
|
+
}
|
|
1079
|
+
return this.CallLog.find(query).sort({ nextFollowUpDate: 1 });
|
|
1080
|
+
}
|
|
1081
|
+
async softDelete(callLogId, agentId, agentName) {
|
|
1082
|
+
const callLog = await this.CallLog.findOne({ callLogId });
|
|
1083
|
+
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
1084
|
+
const now = /* @__PURE__ */ new Date();
|
|
1085
|
+
const deleteEntry = {
|
|
1086
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
1087
|
+
type: callLogTypes.TimelineEntryType.System,
|
|
1088
|
+
content: agentName ? SYSTEM_TIMELINE_FN.callDeleted(agentName) : SYSTEM_TIMELINE_FN.callDeleted(agentId ?? "unknown"),
|
|
1089
|
+
authorId: agentId,
|
|
1090
|
+
createdAt: now
|
|
1091
|
+
};
|
|
1092
|
+
const updated = await this.CallLog.findOneAndUpdate(
|
|
1093
|
+
{ callLogId },
|
|
1094
|
+
{
|
|
1095
|
+
$set: { isDeleted: true, deletedAt: now },
|
|
1096
|
+
$push: {
|
|
1097
|
+
timeline: {
|
|
1098
|
+
$each: [deleteEntry],
|
|
1099
|
+
$slice: -this.options.maxTimelineEntries
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
{ new: true }
|
|
1104
|
+
);
|
|
1105
|
+
if (!updated) throw new CallLogNotFoundError(callLogId);
|
|
1106
|
+
this.logger.info("Call log soft deleted", { callLogId });
|
|
1107
|
+
return updated;
|
|
1108
|
+
}
|
|
1038
1109
|
};
|
|
1039
1110
|
|
|
1040
1111
|
// src/services/analytics.service.ts
|
|
@@ -1065,6 +1136,7 @@ var AnalyticsService = class {
|
|
|
1065
1136
|
const now = /* @__PURE__ */ new Date();
|
|
1066
1137
|
const matchStage = {
|
|
1067
1138
|
agentId,
|
|
1139
|
+
isDeleted: { $ne: true },
|
|
1068
1140
|
...this.buildDateMatch(dateRange)
|
|
1069
1141
|
};
|
|
1070
1142
|
const pipeline = [
|
|
@@ -1157,7 +1229,10 @@ var AnalyticsService = class {
|
|
|
1157
1229
|
}
|
|
1158
1230
|
async getAgentLeaderboard(dateRange) {
|
|
1159
1231
|
const now = /* @__PURE__ */ new Date();
|
|
1160
|
-
const matchStage =
|
|
1232
|
+
const matchStage = {
|
|
1233
|
+
isDeleted: { $ne: true },
|
|
1234
|
+
...this.buildDateMatch(dateRange)
|
|
1235
|
+
};
|
|
1161
1236
|
const pipeline = [
|
|
1162
1237
|
{ $match: matchStage },
|
|
1163
1238
|
{
|
|
@@ -1213,103 +1288,12 @@ var AnalyticsService = class {
|
|
|
1213
1288
|
closeRate: stat.totalCalls > 0 ? Math.round(stat.callsClosed / stat.totalCalls * 1e4) / 100 : 0
|
|
1214
1289
|
})));
|
|
1215
1290
|
}
|
|
1216
|
-
async
|
|
1217
|
-
const
|
|
1218
|
-
const pipelineName = pipeline?.name ?? pipelineId;
|
|
1219
|
-
const stages = pipeline?.stages ?? [];
|
|
1220
|
-
const matchStage = {
|
|
1221
|
-
pipelineId,
|
|
1222
|
-
...this.buildDateMatch(dateRange)
|
|
1223
|
-
};
|
|
1224
|
-
const [totalResult, stageAgg] = await Promise.all([
|
|
1225
|
-
this.CallLog.countDocuments(matchStage),
|
|
1226
|
-
this.CallLog.aggregate([
|
|
1227
|
-
{ $match: matchStage },
|
|
1228
|
-
{ $unwind: { path: "$stageHistory", preserveNullAndEmptyArrays: false } },
|
|
1229
|
-
{
|
|
1230
|
-
$group: {
|
|
1231
|
-
_id: "$stageHistory.toStageId",
|
|
1232
|
-
count: { $sum: 1 },
|
|
1233
|
-
totalTimeMs: { $sum: "$stageHistory.timeInStageMs" }
|
|
1234
|
-
}
|
|
1235
|
-
},
|
|
1236
|
-
{
|
|
1237
|
-
$project: {
|
|
1238
|
-
stageId: "$_id",
|
|
1239
|
-
count: 1,
|
|
1240
|
-
totalTimeMs: 1
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
])
|
|
1244
|
-
]);
|
|
1245
|
-
const totalCalls = totalResult;
|
|
1246
|
-
const stageMap = new Map(stageAgg.map((s) => [s.stageId, s]));
|
|
1247
|
-
let bottleneckStage = null;
|
|
1248
|
-
let maxAvgTime = 0;
|
|
1249
|
-
const stageStats = stages.map((s) => {
|
|
1250
|
-
const agg = stageMap.get(s.stageId);
|
|
1251
|
-
const count = agg?.count ?? 0;
|
|
1252
|
-
const avgTimeMs = count > 0 ? Math.round((agg?.totalTimeMs ?? 0) / count) : 0;
|
|
1253
|
-
const conversionRate = totalCalls > 0 ? Math.round(count / totalCalls * 1e4) / 100 : 0;
|
|
1254
|
-
if (avgTimeMs > maxAvgTime) {
|
|
1255
|
-
maxAvgTime = avgTimeMs;
|
|
1256
|
-
bottleneckStage = s.stageId;
|
|
1257
|
-
}
|
|
1258
|
-
return {
|
|
1259
|
-
stageId: s.stageId,
|
|
1260
|
-
stageName: s.name,
|
|
1261
|
-
count,
|
|
1262
|
-
avgTimeMs,
|
|
1263
|
-
conversionRate
|
|
1264
|
-
};
|
|
1265
|
-
});
|
|
1266
|
-
this.logger.info("Pipeline stats computed", { pipelineId, totalCalls });
|
|
1267
|
-
return {
|
|
1268
|
-
pipelineId,
|
|
1269
|
-
pipelineName,
|
|
1270
|
-
totalCalls,
|
|
1271
|
-
stages: stageStats,
|
|
1272
|
-
bottleneckStage
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
|
-
async getPipelineFunnel(pipelineId, dateRange) {
|
|
1276
|
-
const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
|
|
1277
|
-
const pipelineName = pipeline?.name ?? pipelineId;
|
|
1278
|
-
const stages = pipeline?.stages ?? [];
|
|
1291
|
+
async getTeamStats(teamId, dateRange = {}) {
|
|
1292
|
+
const now = /* @__PURE__ */ new Date();
|
|
1279
1293
|
const matchStage = {
|
|
1280
|
-
|
|
1294
|
+
isDeleted: { $ne: true },
|
|
1281
1295
|
...this.buildDateMatch(dateRange)
|
|
1282
1296
|
};
|
|
1283
|
-
const [enteredAgg, exitedAgg] = await Promise.all([
|
|
1284
|
-
this.CallLog.aggregate([
|
|
1285
|
-
{ $match: matchStage },
|
|
1286
|
-
{ $unwind: "$stageHistory" },
|
|
1287
|
-
{ $group: { _id: "$stageHistory.toStageId", count: { $sum: 1 } } }
|
|
1288
|
-
]),
|
|
1289
|
-
this.CallLog.aggregate([
|
|
1290
|
-
{ $match: matchStage },
|
|
1291
|
-
{ $unwind: "$stageHistory" },
|
|
1292
|
-
{ $group: { _id: "$stageHistory.fromStageId", count: { $sum: 1 } } }
|
|
1293
|
-
])
|
|
1294
|
-
]);
|
|
1295
|
-
const enteredMap = new Map(enteredAgg.map((r) => [String(r._id), r.count]));
|
|
1296
|
-
const exitedMap = new Map(exitedAgg.map((r) => [String(r._id), r.count]));
|
|
1297
|
-
const funnelStages = stages.map((s) => {
|
|
1298
|
-
const entered = enteredMap.get(s.stageId) ?? 0;
|
|
1299
|
-
const exited = exitedMap.get(s.stageId) ?? 0;
|
|
1300
|
-
return {
|
|
1301
|
-
stageId: s.stageId,
|
|
1302
|
-
stageName: s.name,
|
|
1303
|
-
entered,
|
|
1304
|
-
exited,
|
|
1305
|
-
dropOff: Math.max(0, entered - exited)
|
|
1306
|
-
};
|
|
1307
|
-
});
|
|
1308
|
-
return { pipelineId, pipelineName, stages: funnelStages };
|
|
1309
|
-
}
|
|
1310
|
-
async getTeamStats(teamId, dateRange = {}) {
|
|
1311
|
-
const now = /* @__PURE__ */ new Date();
|
|
1312
|
-
const matchStage = this.buildDateMatch(dateRange);
|
|
1313
1297
|
const pipeline = [
|
|
1314
1298
|
{ $match: matchStage },
|
|
1315
1299
|
{
|
|
@@ -1366,8 +1350,12 @@ var AnalyticsService = class {
|
|
|
1366
1350
|
const totalCalls = agentStats.reduce((sum, a) => sum + a.totalCalls, 0);
|
|
1367
1351
|
return { teamId: teamId ?? null, agentStats, totalCalls };
|
|
1368
1352
|
}
|
|
1369
|
-
async getDailyReport(dateRange) {
|
|
1370
|
-
const matchStage =
|
|
1353
|
+
async getDailyReport(dateRange, agentId) {
|
|
1354
|
+
const matchStage = {
|
|
1355
|
+
isDeleted: { $ne: true },
|
|
1356
|
+
...this.buildDateMatch(dateRange)
|
|
1357
|
+
};
|
|
1358
|
+
if (agentId) matchStage["agentId"] = agentId;
|
|
1371
1359
|
const [dailyAgg, directionAgg, pipelineAgg, agentAgg] = await Promise.all([
|
|
1372
1360
|
this.CallLog.aggregate([
|
|
1373
1361
|
{ $match: matchStage },
|
|
@@ -1447,13 +1435,14 @@ var AnalyticsService = class {
|
|
|
1447
1435
|
const now = /* @__PURE__ */ new Date();
|
|
1448
1436
|
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1449
1437
|
const [openCalls, closedToday, overdueFollowUps, callsToday] = await Promise.all([
|
|
1450
|
-
this.CallLog.countDocuments({ isClosed: false }),
|
|
1451
|
-
this.CallLog.countDocuments({ closedAt: { $gte: midnight } }),
|
|
1438
|
+
this.CallLog.countDocuments({ isClosed: false, isDeleted: { $ne: true } }),
|
|
1439
|
+
this.CallLog.countDocuments({ closedAt: { $gte: midnight }, isDeleted: { $ne: true } }),
|
|
1452
1440
|
this.CallLog.countDocuments({
|
|
1453
1441
|
nextFollowUpDate: { $lt: now },
|
|
1454
|
-
isClosed: false
|
|
1442
|
+
isClosed: false,
|
|
1443
|
+
isDeleted: { $ne: true }
|
|
1455
1444
|
}),
|
|
1456
|
-
this.CallLog.countDocuments({ callDate: { $gte: midnight } })
|
|
1445
|
+
this.CallLog.countDocuments({ callDate: { $gte: midnight }, isDeleted: { $ne: true } })
|
|
1457
1446
|
]);
|
|
1458
1447
|
return {
|
|
1459
1448
|
openCalls,
|
|
@@ -1467,7 +1456,7 @@ var AnalyticsService = class {
|
|
|
1467
1456
|
const now = /* @__PURE__ */ new Date();
|
|
1468
1457
|
const from = new Date(now.getTime() - weeks * 7 * 24 * 60 * 60 * 1e3);
|
|
1469
1458
|
const results = await this.CallLog.aggregate([
|
|
1470
|
-
{ $match: { callDate: { $gte: from, $lte: now } } },
|
|
1459
|
+
{ $match: { callDate: { $gte: from, $lte: now }, isDeleted: { $ne: true } } },
|
|
1471
1460
|
{
|
|
1472
1461
|
$group: {
|
|
1473
1462
|
_id: {
|
|
@@ -1494,8 +1483,11 @@ var AnalyticsService = class {
|
|
|
1494
1483
|
return results;
|
|
1495
1484
|
}
|
|
1496
1485
|
async getOverallReport(dateRange) {
|
|
1497
|
-
const matchStage =
|
|
1498
|
-
|
|
1486
|
+
const matchStage = {
|
|
1487
|
+
isDeleted: { $ne: true },
|
|
1488
|
+
...this.buildDateMatch(dateRange)
|
|
1489
|
+
};
|
|
1490
|
+
const [summaryAgg, tagAgg, categoryAgg, peakHoursAgg, followUpAgg, channelAgg, outcomeAgg] = await Promise.all([
|
|
1499
1491
|
this.CallLog.aggregate([
|
|
1500
1492
|
{ $match: matchStage },
|
|
1501
1493
|
{
|
|
@@ -1540,14 +1532,14 @@ var AnalyticsService = class {
|
|
|
1540
1532
|
_id: null,
|
|
1541
1533
|
total: { $sum: 1 },
|
|
1542
1534
|
withFollowUp: {
|
|
1543
|
-
$sum: { $cond: [{ $
|
|
1535
|
+
$sum: { $cond: [{ $eq: ["$isFollowUp", true] }, 1, 0] }
|
|
1544
1536
|
},
|
|
1545
1537
|
completed: {
|
|
1546
1538
|
$sum: {
|
|
1547
1539
|
$cond: [
|
|
1548
1540
|
{
|
|
1549
1541
|
$and: [
|
|
1550
|
-
{ $
|
|
1542
|
+
{ $eq: ["$isFollowUp", true] },
|
|
1551
1543
|
{ $eq: ["$isClosed", true] }
|
|
1552
1544
|
]
|
|
1553
1545
|
},
|
|
@@ -1558,11 +1550,31 @@ var AnalyticsService = class {
|
|
|
1558
1550
|
}
|
|
1559
1551
|
}
|
|
1560
1552
|
}
|
|
1553
|
+
]),
|
|
1554
|
+
this.CallLog.aggregate([
|
|
1555
|
+
{ $match: matchStage },
|
|
1556
|
+
{ $group: { _id: "$channel", count: { $sum: 1 } } },
|
|
1557
|
+
{ $sort: { count: -1 } }
|
|
1558
|
+
]),
|
|
1559
|
+
this.CallLog.aggregate([
|
|
1560
|
+
{ $match: matchStage },
|
|
1561
|
+
{ $group: { _id: "$outcome", count: { $sum: 1 } } },
|
|
1562
|
+
{ $sort: { count: -1 } }
|
|
1561
1563
|
])
|
|
1562
1564
|
]);
|
|
1563
1565
|
const summary = summaryAgg[0] ?? { totalCalls: 0, closedCalls: 0, avgTimeToCloseMs: 0 };
|
|
1564
|
-
const followUp = followUpAgg[0] ?? { withFollowUp: 0, completed: 0 };
|
|
1566
|
+
const followUp = followUpAgg[0] ?? { total: 0, withFollowUp: 0, completed: 0 };
|
|
1565
1567
|
const followUpComplianceRate = followUp.withFollowUp > 0 ? Math.round(followUp.completed / followUp.withFollowUp * 1e4) / 100 : 0;
|
|
1568
|
+
const followUpCalls = followUp.withFollowUp;
|
|
1569
|
+
const followUpRatio = followUp.total > 0 ? Math.round(followUp.withFollowUp / followUp.total * 1e4) / 100 : 0;
|
|
1570
|
+
const channelDistribution = channelAgg.map((r) => ({
|
|
1571
|
+
channel: r._id != null ? String(r._id) : "unknown",
|
|
1572
|
+
count: r.count
|
|
1573
|
+
}));
|
|
1574
|
+
const outcomeDistribution = outcomeAgg.map((r) => ({
|
|
1575
|
+
outcome: r._id != null ? String(r._id) : "unknown",
|
|
1576
|
+
count: r.count
|
|
1577
|
+
}));
|
|
1566
1578
|
return {
|
|
1567
1579
|
totalCalls: summary.totalCalls,
|
|
1568
1580
|
closedCalls: summary.closedCalls,
|
|
@@ -1570,7 +1582,180 @@ var AnalyticsService = class {
|
|
|
1570
1582
|
followUpComplianceRate,
|
|
1571
1583
|
tagDistribution: tagAgg.map((r) => ({ tag: String(r._id), count: r.count })),
|
|
1572
1584
|
categoryDistribution: categoryAgg.map((r) => ({ category: String(r._id), count: r.count })),
|
|
1573
|
-
peakCallHours: peakHoursAgg.map((r) => ({ hour: Number(r._id), count: r.count }))
|
|
1585
|
+
peakCallHours: peakHoursAgg.map((r) => ({ hour: Number(r._id), count: r.count })),
|
|
1586
|
+
channelDistribution,
|
|
1587
|
+
outcomeDistribution,
|
|
1588
|
+
followUpCalls,
|
|
1589
|
+
followUpRatio
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
// src/services/pipeline-analytics.service.ts
|
|
1595
|
+
var PipelineAnalyticsService = class {
|
|
1596
|
+
constructor(CallLog, Pipeline, logger, resolveAgent, tenantId) {
|
|
1597
|
+
this.CallLog = CallLog;
|
|
1598
|
+
this.Pipeline = Pipeline;
|
|
1599
|
+
this.logger = logger;
|
|
1600
|
+
this.resolveAgent = resolveAgent;
|
|
1601
|
+
this.tenantId = tenantId;
|
|
1602
|
+
}
|
|
1603
|
+
buildDateMatch(dateRange, field = "callDate") {
|
|
1604
|
+
if (!dateRange.from && !dateRange.to) return {};
|
|
1605
|
+
const dateFilter = {};
|
|
1606
|
+
if (dateRange.from) dateFilter.$gte = new Date(dateRange.from);
|
|
1607
|
+
if (dateRange.to) dateFilter.$lte = new Date(dateRange.to);
|
|
1608
|
+
return { [field]: dateFilter };
|
|
1609
|
+
}
|
|
1610
|
+
async getPipelineStats(pipelineId, dateRange) {
|
|
1611
|
+
const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
|
|
1612
|
+
const pipelineName = pipeline?.name ?? pipelineId;
|
|
1613
|
+
const stages = pipeline?.stages ?? [];
|
|
1614
|
+
const matchStage = {
|
|
1615
|
+
pipelineId,
|
|
1616
|
+
isDeleted: { $ne: true },
|
|
1617
|
+
...this.buildDateMatch(dateRange)
|
|
1618
|
+
};
|
|
1619
|
+
const [totalResult, stageAgg] = await Promise.all([
|
|
1620
|
+
this.CallLog.countDocuments(matchStage),
|
|
1621
|
+
this.CallLog.aggregate([
|
|
1622
|
+
{ $match: matchStage },
|
|
1623
|
+
{ $unwind: { path: "$stageHistory", preserveNullAndEmptyArrays: false } },
|
|
1624
|
+
{
|
|
1625
|
+
$group: {
|
|
1626
|
+
_id: "$stageHistory.toStageId",
|
|
1627
|
+
count: { $sum: 1 },
|
|
1628
|
+
totalTimeMs: { $sum: "$stageHistory.timeInStageMs" }
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
$project: {
|
|
1633
|
+
stageId: "$_id",
|
|
1634
|
+
count: 1,
|
|
1635
|
+
totalTimeMs: 1
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
])
|
|
1639
|
+
]);
|
|
1640
|
+
const totalCalls = totalResult;
|
|
1641
|
+
const stageMap = new Map(stageAgg.map((s) => [s.stageId, s]));
|
|
1642
|
+
let bottleneckStage = null;
|
|
1643
|
+
let maxAvgTime = 0;
|
|
1644
|
+
const stageStats = stages.map((s) => {
|
|
1645
|
+
const agg = stageMap.get(s.stageId);
|
|
1646
|
+
const count = agg?.count ?? 0;
|
|
1647
|
+
const avgTimeMs = count > 0 ? Math.round((agg?.totalTimeMs ?? 0) / count) : 0;
|
|
1648
|
+
const conversionRate = totalCalls > 0 ? Math.round(count / totalCalls * 1e4) / 100 : 0;
|
|
1649
|
+
if (avgTimeMs > maxAvgTime) {
|
|
1650
|
+
maxAvgTime = avgTimeMs;
|
|
1651
|
+
bottleneckStage = s.stageId;
|
|
1652
|
+
}
|
|
1653
|
+
return {
|
|
1654
|
+
stageId: s.stageId,
|
|
1655
|
+
stageName: s.name,
|
|
1656
|
+
count,
|
|
1657
|
+
avgTimeMs,
|
|
1658
|
+
conversionRate
|
|
1659
|
+
};
|
|
1660
|
+
});
|
|
1661
|
+
this.logger.info("Pipeline stats computed", { pipelineId, totalCalls });
|
|
1662
|
+
return {
|
|
1663
|
+
pipelineId,
|
|
1664
|
+
pipelineName,
|
|
1665
|
+
totalCalls,
|
|
1666
|
+
stages: stageStats,
|
|
1667
|
+
bottleneckStage
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
async getPipelineFunnel(pipelineId, dateRange) {
|
|
1671
|
+
const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
|
|
1672
|
+
const pipelineName = pipeline?.name ?? pipelineId;
|
|
1673
|
+
const stages = pipeline?.stages ?? [];
|
|
1674
|
+
const matchStage = {
|
|
1675
|
+
pipelineId,
|
|
1676
|
+
isDeleted: { $ne: true },
|
|
1677
|
+
...this.buildDateMatch(dateRange)
|
|
1678
|
+
};
|
|
1679
|
+
const [enteredAgg, exitedAgg] = await Promise.all([
|
|
1680
|
+
this.CallLog.aggregate([
|
|
1681
|
+
{ $match: matchStage },
|
|
1682
|
+
{ $unwind: "$stageHistory" },
|
|
1683
|
+
{ $group: { _id: "$stageHistory.toStageId", count: { $sum: 1 } } }
|
|
1684
|
+
]),
|
|
1685
|
+
this.CallLog.aggregate([
|
|
1686
|
+
{ $match: matchStage },
|
|
1687
|
+
{ $unwind: "$stageHistory" },
|
|
1688
|
+
{ $group: { _id: "$stageHistory.fromStageId", count: { $sum: 1 } } }
|
|
1689
|
+
])
|
|
1690
|
+
]);
|
|
1691
|
+
const enteredMap = new Map(enteredAgg.map((r) => [String(r._id), r.count]));
|
|
1692
|
+
const exitedMap = new Map(exitedAgg.map((r) => [String(r._id), r.count]));
|
|
1693
|
+
const funnelStages = stages.map((s) => {
|
|
1694
|
+
const entered = enteredMap.get(s.stageId) ?? 0;
|
|
1695
|
+
const exited = exitedMap.get(s.stageId) ?? 0;
|
|
1696
|
+
return {
|
|
1697
|
+
stageId: s.stageId,
|
|
1698
|
+
stageName: s.name,
|
|
1699
|
+
entered,
|
|
1700
|
+
exited,
|
|
1701
|
+
dropOff: Math.max(0, entered - exited)
|
|
1702
|
+
};
|
|
1703
|
+
});
|
|
1704
|
+
return { pipelineId, pipelineName, stages: funnelStages };
|
|
1705
|
+
}
|
|
1706
|
+
async getChannelDistribution(dateRange) {
|
|
1707
|
+
const matchStage = {
|
|
1708
|
+
isDeleted: { $ne: true },
|
|
1709
|
+
...this.buildDateMatch(dateRange)
|
|
1710
|
+
};
|
|
1711
|
+
const results = await this.CallLog.aggregate([
|
|
1712
|
+
{ $match: matchStage },
|
|
1713
|
+
{ $group: { _id: "$channel", count: { $sum: 1 } } },
|
|
1714
|
+
{ $sort: { count: -1 } }
|
|
1715
|
+
]);
|
|
1716
|
+
return results.map((r) => ({
|
|
1717
|
+
channel: r._id != null ? String(r._id) : "unknown",
|
|
1718
|
+
count: r.count
|
|
1719
|
+
}));
|
|
1720
|
+
}
|
|
1721
|
+
async getOutcomeDistribution(dateRange) {
|
|
1722
|
+
const matchStage = {
|
|
1723
|
+
isDeleted: { $ne: true },
|
|
1724
|
+
...this.buildDateMatch(dateRange)
|
|
1725
|
+
};
|
|
1726
|
+
const results = await this.CallLog.aggregate([
|
|
1727
|
+
{ $match: matchStage },
|
|
1728
|
+
{ $group: { _id: "$outcome", count: { $sum: 1 } } },
|
|
1729
|
+
{ $sort: { count: -1 } }
|
|
1730
|
+
]);
|
|
1731
|
+
return results.map((r) => ({
|
|
1732
|
+
outcome: r._id != null ? String(r._id) : "unknown",
|
|
1733
|
+
count: r.count
|
|
1734
|
+
}));
|
|
1735
|
+
}
|
|
1736
|
+
async getFollowUpStats(dateRange) {
|
|
1737
|
+
const matchStage = {
|
|
1738
|
+
isDeleted: { $ne: true },
|
|
1739
|
+
...this.buildDateMatch(dateRange)
|
|
1740
|
+
};
|
|
1741
|
+
const results = await this.CallLog.aggregate([
|
|
1742
|
+
{ $match: matchStage },
|
|
1743
|
+
{
|
|
1744
|
+
$group: {
|
|
1745
|
+
_id: null,
|
|
1746
|
+
total: { $sum: 1 },
|
|
1747
|
+
followUpCalls: {
|
|
1748
|
+
$sum: { $cond: [{ $eq: ["$isFollowUp", true] }, 1, 0] }
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
]);
|
|
1753
|
+
const stat = results[0] ?? { total: 0, followUpCalls: 0 };
|
|
1754
|
+
const followUpRatio = stat.total > 0 ? Math.round(stat.followUpCalls / stat.total * 1e4) / 100 : 0;
|
|
1755
|
+
return {
|
|
1756
|
+
followUpCalls: stat.followUpCalls,
|
|
1757
|
+
totalCalls: stat.total,
|
|
1758
|
+
followUpRatio
|
|
1574
1759
|
};
|
|
1575
1760
|
}
|
|
1576
1761
|
};
|
|
@@ -1578,9 +1763,9 @@ var AnalyticsService = class {
|
|
|
1578
1763
|
// src/services/export.service.ts
|
|
1579
1764
|
var CSV_HEADER = "callLogId,contactName,contactPhone,contactEmail,direction,pipelineId,currentStageId,priority,agentId,callDate,isClosed,tags";
|
|
1580
1765
|
var ExportService = class {
|
|
1581
|
-
constructor(CallLog,
|
|
1766
|
+
constructor(CallLog, pipelineAnalytics, logger) {
|
|
1582
1767
|
this.CallLog = CallLog;
|
|
1583
|
-
this.
|
|
1768
|
+
this.pipelineAnalytics = pipelineAnalytics;
|
|
1584
1769
|
this.logger = logger;
|
|
1585
1770
|
}
|
|
1586
1771
|
async exportCallLog(callLogId, format) {
|
|
@@ -1622,7 +1807,7 @@ var ExportService = class {
|
|
|
1622
1807
|
);
|
|
1623
1808
|
}
|
|
1624
1809
|
async exportPipelineReport(pipelineId, dateRange, format) {
|
|
1625
|
-
const report = await this.
|
|
1810
|
+
const report = await this.pipelineAnalytics.getPipelineStats(pipelineId, dateRange);
|
|
1626
1811
|
if (format === "csv") {
|
|
1627
1812
|
const header = "pipelineId,pipelineName,totalCalls,stageId,stageName,count,avgTimeMs,conversionRate,bottleneckStage";
|
|
1628
1813
|
const rows = [header];
|
|
@@ -1803,17 +1988,25 @@ function createPipelineRoutes(pipeline, logger) {
|
|
|
1803
1988
|
});
|
|
1804
1989
|
return router;
|
|
1805
1990
|
}
|
|
1806
|
-
function
|
|
1991
|
+
function getScopedAgentId(req, enableAgentScoping) {
|
|
1992
|
+
if (!enableAgentScoping) return void 0;
|
|
1993
|
+
const user = req.user;
|
|
1994
|
+
if (!user) return void 0;
|
|
1995
|
+
if (user.role === "owner") return void 0;
|
|
1996
|
+
return user.adminUserId;
|
|
1997
|
+
}
|
|
1998
|
+
function createCallLogRoutes(services, logger, enableAgentScoping = false) {
|
|
1807
1999
|
const router = express.Router();
|
|
1808
|
-
const { callLogs, timeline } = services;
|
|
2000
|
+
const { callLogs, lifecycle, timeline } = services;
|
|
1809
2001
|
router.get("/follow-ups", async (req, res) => {
|
|
1810
2002
|
try {
|
|
1811
|
-
const
|
|
2003
|
+
const scopedAgentId = getScopedAgentId(req, enableAgentScoping);
|
|
2004
|
+
const agentId = scopedAgentId ?? req.query["agentId"];
|
|
1812
2005
|
const dateRange = {
|
|
1813
2006
|
from: req.query["from"],
|
|
1814
2007
|
to: req.query["to"]
|
|
1815
2008
|
};
|
|
1816
|
-
const result = await
|
|
2009
|
+
const result = await lifecycle.getFollowUpsDue(agentId, dateRange);
|
|
1817
2010
|
core.sendSuccess(res, result);
|
|
1818
2011
|
} catch (error) {
|
|
1819
2012
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -1824,7 +2017,7 @@ function createCallLogRoutes(services, logger) {
|
|
|
1824
2017
|
router.put("/-/bulk/stage", async (req, res) => {
|
|
1825
2018
|
try {
|
|
1826
2019
|
const { callLogIds, newStageId, agentId } = req.body;
|
|
1827
|
-
const result = await
|
|
2020
|
+
const result = await lifecycle.bulkChangeStage(callLogIds, newStageId, agentId);
|
|
1828
2021
|
core.sendSuccess(res, result);
|
|
1829
2022
|
} catch (error) {
|
|
1830
2023
|
if (error instanceof AlxCallLogError) {
|
|
@@ -1842,7 +2035,12 @@ function createCallLogRoutes(services, logger) {
|
|
|
1842
2035
|
const filter = {};
|
|
1843
2036
|
if (query["pipelineId"]) filter["pipelineId"] = query["pipelineId"];
|
|
1844
2037
|
if (query["currentStageId"]) filter["currentStageId"] = query["currentStageId"];
|
|
1845
|
-
|
|
2038
|
+
const scopedAgentId = getScopedAgentId(req, enableAgentScoping);
|
|
2039
|
+
if (scopedAgentId) {
|
|
2040
|
+
filter["agentId"] = scopedAgentId;
|
|
2041
|
+
} else if (query["agentId"]) {
|
|
2042
|
+
filter["agentId"] = query["agentId"];
|
|
2043
|
+
}
|
|
1846
2044
|
if (query["category"]) filter["category"] = query["category"];
|
|
1847
2045
|
if (query["isClosed"] !== void 0) filter["isClosed"] = query["isClosed"] === "true";
|
|
1848
2046
|
if (query["contactExternalId"]) filter["contactExternalId"] = query["contactExternalId"];
|
|
@@ -1851,6 +2049,14 @@ function createCallLogRoutes(services, logger) {
|
|
|
1851
2049
|
if (query["contactEmail"]) filter["contactEmail"] = query["contactEmail"];
|
|
1852
2050
|
if (query["priority"]) filter["priority"] = query["priority"];
|
|
1853
2051
|
if (query["direction"]) filter["direction"] = query["direction"];
|
|
2052
|
+
const channel = query["channel"];
|
|
2053
|
+
const outcome = query["outcome"];
|
|
2054
|
+
const isFollowUp = query["isFollowUp"] !== void 0 ? query["isFollowUp"] === "true" : void 0;
|
|
2055
|
+
const includeDeleted = query["includeDeleted"] === "true";
|
|
2056
|
+
if (channel) filter["channel"] = channel;
|
|
2057
|
+
if (outcome) filter["outcome"] = outcome;
|
|
2058
|
+
if (isFollowUp !== void 0) filter["isFollowUp"] = isFollowUp;
|
|
2059
|
+
if (includeDeleted) filter["includeDeleted"] = true;
|
|
1854
2060
|
if (query["page"]) filter["page"] = parseInt(query["page"], 10);
|
|
1855
2061
|
if (query["limit"]) filter["limit"] = parseInt(query["limit"], 10);
|
|
1856
2062
|
if (query["from"] || query["to"]) {
|
|
@@ -1906,10 +2112,26 @@ function createCallLogRoutes(services, logger) {
|
|
|
1906
2112
|
core.sendError(res, message, 500);
|
|
1907
2113
|
}
|
|
1908
2114
|
});
|
|
2115
|
+
router.delete("/:id", async (req, res) => {
|
|
2116
|
+
try {
|
|
2117
|
+
const agentId = req.user?.adminUserId;
|
|
2118
|
+
const agentName = req.user?.displayName;
|
|
2119
|
+
const result = await lifecycle.softDelete(req.params["id"], agentId, agentName);
|
|
2120
|
+
core.sendSuccess(res, result);
|
|
2121
|
+
} catch (error) {
|
|
2122
|
+
if (error instanceof AlxCallLogError) {
|
|
2123
|
+
core.sendError(res, error.message, 404);
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2127
|
+
logger.error("Failed to soft delete call log", { id: req.params["id"], error: message });
|
|
2128
|
+
core.sendError(res, message, 500);
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
1909
2131
|
router.put("/:id/stage", async (req, res) => {
|
|
1910
2132
|
try {
|
|
1911
2133
|
const { newStageId, agentId } = req.body;
|
|
1912
|
-
const result = await
|
|
2134
|
+
const result = await lifecycle.changeStage(req.params["id"], newStageId, agentId);
|
|
1913
2135
|
core.sendSuccess(res, result);
|
|
1914
2136
|
} catch (error) {
|
|
1915
2137
|
if (error instanceof AlxCallLogError) {
|
|
@@ -1924,7 +2146,7 @@ function createCallLogRoutes(services, logger) {
|
|
|
1924
2146
|
router.put("/:id/assign", async (req, res) => {
|
|
1925
2147
|
try {
|
|
1926
2148
|
const { agentId, assignedBy } = req.body;
|
|
1927
|
-
const result = await
|
|
2149
|
+
const result = await lifecycle.assign(req.params["id"], agentId, assignedBy);
|
|
1928
2150
|
core.sendSuccess(res, result);
|
|
1929
2151
|
} catch (error) {
|
|
1930
2152
|
if (error instanceof AlxCallLogError) {
|
|
@@ -1939,7 +2161,7 @@ function createCallLogRoutes(services, logger) {
|
|
|
1939
2161
|
router.put("/:id/close", async (req, res) => {
|
|
1940
2162
|
try {
|
|
1941
2163
|
const { agentId } = req.body;
|
|
1942
|
-
const result = await
|
|
2164
|
+
const result = await lifecycle.close(req.params["id"], agentId);
|
|
1943
2165
|
core.sendSuccess(res, result);
|
|
1944
2166
|
} catch (error) {
|
|
1945
2167
|
if (error instanceof AlxCallLogError) {
|
|
@@ -1954,7 +2176,7 @@ function createCallLogRoutes(services, logger) {
|
|
|
1954
2176
|
router.put("/:id/reopen", async (req, res) => {
|
|
1955
2177
|
try {
|
|
1956
2178
|
const { agentId } = req.body;
|
|
1957
|
-
const result = await
|
|
2179
|
+
const result = await lifecycle.reopen(req.params["id"], agentId);
|
|
1958
2180
|
core.sendSuccess(res, result);
|
|
1959
2181
|
} catch (error) {
|
|
1960
2182
|
if (error instanceof AlxCallLogError) {
|
|
@@ -2034,8 +2256,15 @@ function createContactRoutes(services, logger) {
|
|
|
2034
2256
|
});
|
|
2035
2257
|
return router;
|
|
2036
2258
|
}
|
|
2037
|
-
function createAnalyticsRoutes(analytics, logger) {
|
|
2259
|
+
function createAnalyticsRoutes(analytics, pipelineAnalytics, logger, enableAgentScoping = false) {
|
|
2038
2260
|
const router = express.Router();
|
|
2261
|
+
function getScopedAgentId2(req) {
|
|
2262
|
+
if (!enableAgentScoping) return void 0;
|
|
2263
|
+
const user = req.user;
|
|
2264
|
+
if (!user) return void 0;
|
|
2265
|
+
if (user.role === "owner") return void 0;
|
|
2266
|
+
return user.adminUserId;
|
|
2267
|
+
}
|
|
2039
2268
|
function parseDateRange(query) {
|
|
2040
2269
|
return {
|
|
2041
2270
|
from: query["from"],
|
|
@@ -2054,6 +2283,11 @@ function createAnalyticsRoutes(analytics, logger) {
|
|
|
2054
2283
|
});
|
|
2055
2284
|
router.get("/agent/:agentId", async (req, res) => {
|
|
2056
2285
|
try {
|
|
2286
|
+
const scopedAgentId = getScopedAgentId2(req);
|
|
2287
|
+
if (scopedAgentId && scopedAgentId !== req.params["agentId"]) {
|
|
2288
|
+
core.sendError(res, "Forbidden: you may only view your own agent stats", 403);
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2057
2291
|
const dateRange = parseDateRange(req.query);
|
|
2058
2292
|
const result = await analytics.getAgentStats(req.params["agentId"], dateRange);
|
|
2059
2293
|
core.sendSuccess(res, result);
|
|
@@ -2081,7 +2315,7 @@ function createAnalyticsRoutes(analytics, logger) {
|
|
|
2081
2315
|
router.get("/pipeline/:id", async (req, res) => {
|
|
2082
2316
|
try {
|
|
2083
2317
|
const dateRange = parseDateRange(req.query);
|
|
2084
|
-
const result = await
|
|
2318
|
+
const result = await pipelineAnalytics.getPipelineStats(req.params["id"], dateRange);
|
|
2085
2319
|
core.sendSuccess(res, result);
|
|
2086
2320
|
} catch (error) {
|
|
2087
2321
|
if (error instanceof AlxCallLogError) {
|
|
@@ -2096,7 +2330,7 @@ function createAnalyticsRoutes(analytics, logger) {
|
|
|
2096
2330
|
router.get("/pipeline/:id/funnel", async (req, res) => {
|
|
2097
2331
|
try {
|
|
2098
2332
|
const dateRange = parseDateRange(req.query);
|
|
2099
|
-
const result = await
|
|
2333
|
+
const result = await pipelineAnalytics.getPipelineFunnel(req.params["id"], dateRange);
|
|
2100
2334
|
core.sendSuccess(res, result);
|
|
2101
2335
|
} catch (error) {
|
|
2102
2336
|
if (error instanceof AlxCallLogError) {
|
|
@@ -2108,6 +2342,39 @@ function createAnalyticsRoutes(analytics, logger) {
|
|
|
2108
2342
|
core.sendError(res, message, 500);
|
|
2109
2343
|
}
|
|
2110
2344
|
});
|
|
2345
|
+
router.get("/channel-distribution", async (req, res) => {
|
|
2346
|
+
try {
|
|
2347
|
+
const dateRange = parseDateRange(req.query);
|
|
2348
|
+
const result = await pipelineAnalytics.getChannelDistribution(dateRange);
|
|
2349
|
+
core.sendSuccess(res, result);
|
|
2350
|
+
} catch (error) {
|
|
2351
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2352
|
+
logger.error("Failed to get channel distribution", { error: message });
|
|
2353
|
+
core.sendError(res, message, 500);
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
router.get("/outcome-distribution", async (req, res) => {
|
|
2357
|
+
try {
|
|
2358
|
+
const dateRange = parseDateRange(req.query);
|
|
2359
|
+
const result = await pipelineAnalytics.getOutcomeDistribution(dateRange);
|
|
2360
|
+
core.sendSuccess(res, result);
|
|
2361
|
+
} catch (error) {
|
|
2362
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2363
|
+
logger.error("Failed to get outcome distribution", { error: message });
|
|
2364
|
+
core.sendError(res, message, 500);
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
router.get("/follow-up-stats", async (req, res) => {
|
|
2368
|
+
try {
|
|
2369
|
+
const dateRange = parseDateRange(req.query);
|
|
2370
|
+
const result = await pipelineAnalytics.getFollowUpStats(dateRange);
|
|
2371
|
+
core.sendSuccess(res, result);
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2374
|
+
logger.error("Failed to get follow-up stats", { error: message });
|
|
2375
|
+
core.sendError(res, message, 500);
|
|
2376
|
+
}
|
|
2377
|
+
});
|
|
2111
2378
|
router.get("/team", async (req, res) => {
|
|
2112
2379
|
try {
|
|
2113
2380
|
const { teamId } = req.query;
|
|
@@ -2122,8 +2389,9 @@ function createAnalyticsRoutes(analytics, logger) {
|
|
|
2122
2389
|
});
|
|
2123
2390
|
router.get("/daily", async (req, res) => {
|
|
2124
2391
|
try {
|
|
2392
|
+
const scopedAgentId = getScopedAgentId2(req);
|
|
2125
2393
|
const dateRange = parseDateRange(req.query);
|
|
2126
|
-
const result = await analytics.getDailyReport(dateRange);
|
|
2394
|
+
const result = await analytics.getDailyReport(dateRange, scopedAgentId);
|
|
2127
2395
|
core.sendSuccess(res, result);
|
|
2128
2396
|
} catch (error) {
|
|
2129
2397
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -2248,7 +2516,7 @@ function createSettingsRoutes(settings, exportSvc, logger) {
|
|
|
2248
2516
|
// src/routes/index.ts
|
|
2249
2517
|
function createRoutes(services, options) {
|
|
2250
2518
|
const router = express.Router();
|
|
2251
|
-
const { logger, authenticateRequest } = options;
|
|
2519
|
+
const { logger, authenticateRequest, enableAgentScoping = false } = options;
|
|
2252
2520
|
let authMiddleware;
|
|
2253
2521
|
if (authenticateRequest) {
|
|
2254
2522
|
authMiddleware = async (req, res, next) => {
|
|
@@ -2268,9 +2536,9 @@ function createRoutes(services, options) {
|
|
|
2268
2536
|
}
|
|
2269
2537
|
const protectedRouter = express.Router();
|
|
2270
2538
|
protectedRouter.use("/pipelines", createPipelineRoutes(services.pipelines, logger));
|
|
2271
|
-
protectedRouter.use("/calls", createCallLogRoutes({ callLogs: services.callLogs, timeline: services.timeline }, logger));
|
|
2539
|
+
protectedRouter.use("/calls", createCallLogRoutes({ callLogs: services.callLogs, lifecycle: services.lifecycle, timeline: services.timeline }, logger, enableAgentScoping));
|
|
2272
2540
|
protectedRouter.use("/contacts", createContactRoutes({ callLogs: services.callLogs, timeline: services.timeline }, logger));
|
|
2273
|
-
protectedRouter.use("/analytics", createAnalyticsRoutes(services.analytics, logger));
|
|
2541
|
+
protectedRouter.use("/analytics", createAnalyticsRoutes(services.analytics, services.pipelineAnalytics, logger, enableAgentScoping));
|
|
2274
2542
|
protectedRouter.use("/", createSettingsRoutes(services.settings, services.export, logger));
|
|
2275
2543
|
if (authMiddleware) {
|
|
2276
2544
|
router.use(authMiddleware, protectedRouter);
|
|
@@ -2325,7 +2593,7 @@ var FollowUpWorker = class {
|
|
|
2325
2593
|
$set: { followUpNotifiedAt: /* @__PURE__ */ new Date() },
|
|
2326
2594
|
$push: {
|
|
2327
2595
|
timeline: {
|
|
2328
|
-
entryId:
|
|
2596
|
+
entryId: crypto6__default.default.randomUUID(),
|
|
2329
2597
|
type: callLogTypes.TimelineEntryType.FollowUpCompleted,
|
|
2330
2598
|
content: SYSTEM_TIMELINE.FollowUpCompleted,
|
|
2331
2599
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -2378,7 +2646,8 @@ var CallLogEngineConfigSchema = zod.z.object({
|
|
|
2378
2646
|
}).optional(),
|
|
2379
2647
|
options: zod.z.object({
|
|
2380
2648
|
maxTimelineEntries: zod.z.number().int().positive().optional(),
|
|
2381
|
-
followUpCheckIntervalMs: zod.z.number().int().positive().optional()
|
|
2649
|
+
followUpCheckIntervalMs: zod.z.number().int().positive().optional(),
|
|
2650
|
+
enableAgentScoping: zod.z.boolean().optional()
|
|
2382
2651
|
}).optional()
|
|
2383
2652
|
});
|
|
2384
2653
|
function createCallLogEngine(config) {
|
|
@@ -2408,14 +2677,30 @@ function createCallLogEngine(config) {
|
|
|
2408
2677
|
config.hooks ?? {},
|
|
2409
2678
|
resolvedOptions
|
|
2410
2679
|
);
|
|
2680
|
+
const callLogLifecycleService = new CallLogLifecycleService(
|
|
2681
|
+
CallLog,
|
|
2682
|
+
Pipeline,
|
|
2683
|
+
timelineService,
|
|
2684
|
+
logger,
|
|
2685
|
+
config.hooks ?? {},
|
|
2686
|
+
resolvedOptions
|
|
2687
|
+
);
|
|
2411
2688
|
const analyticsService = new AnalyticsService(CallLog, Pipeline, logger, config.agents?.resolveAgent);
|
|
2412
|
-
const
|
|
2689
|
+
const pipelineAnalyticsService = new PipelineAnalyticsService(
|
|
2690
|
+
CallLog,
|
|
2691
|
+
Pipeline,
|
|
2692
|
+
logger,
|
|
2693
|
+
config.agents?.resolveAgent
|
|
2694
|
+
);
|
|
2695
|
+
const exportService = new ExportService(CallLog, pipelineAnalyticsService, logger);
|
|
2413
2696
|
const routes = createRoutes(
|
|
2414
2697
|
{
|
|
2415
2698
|
pipelines: pipelineService,
|
|
2416
2699
|
callLogs: callLogService,
|
|
2700
|
+
lifecycle: callLogLifecycleService,
|
|
2417
2701
|
timeline: timelineService,
|
|
2418
2702
|
analytics: analyticsService,
|
|
2703
|
+
pipelineAnalytics: pipelineAnalyticsService,
|
|
2419
2704
|
settings: settingsService,
|
|
2420
2705
|
export: exportService
|
|
2421
2706
|
},
|
|
@@ -2427,7 +2712,8 @@ function createCallLogEngine(config) {
|
|
|
2427
2712
|
if (!token) return null;
|
|
2428
2713
|
return config.adapters.authenticateAgent(token);
|
|
2429
2714
|
} : void 0,
|
|
2430
|
-
logger
|
|
2715
|
+
logger,
|
|
2716
|
+
enableAgentScoping: resolvedOptions.enableAgentScoping
|
|
2431
2717
|
}
|
|
2432
2718
|
);
|
|
2433
2719
|
const followUpWorker = new FollowUpWorker({
|
|
@@ -2444,8 +2730,10 @@ function createCallLogEngine(config) {
|
|
|
2444
2730
|
return {
|
|
2445
2731
|
pipelines: pipelineService,
|
|
2446
2732
|
callLogs: callLogService,
|
|
2733
|
+
lifecycle: callLogLifecycleService,
|
|
2447
2734
|
timeline: timelineService,
|
|
2448
2735
|
analytics: analyticsService,
|
|
2736
|
+
pipelineAnalytics: pipelineAnalyticsService,
|
|
2449
2737
|
settings: settingsService,
|
|
2450
2738
|
export: exportService,
|
|
2451
2739
|
routes,
|
|
@@ -2465,6 +2753,7 @@ exports.AnalyticsService = AnalyticsService;
|
|
|
2465
2753
|
exports.AuthFailedError = AuthFailedError;
|
|
2466
2754
|
exports.CALL_LOG_DEFAULTS = CALL_LOG_DEFAULTS;
|
|
2467
2755
|
exports.CallLogClosedError = CallLogClosedError;
|
|
2756
|
+
exports.CallLogLifecycleService = CallLogLifecycleService;
|
|
2468
2757
|
exports.CallLogNotFoundError = CallLogNotFoundError;
|
|
2469
2758
|
exports.CallLogSchema = CallLogSchema;
|
|
2470
2759
|
exports.CallLogService = CallLogService;
|
|
@@ -2478,6 +2767,7 @@ exports.FollowUpWorker = FollowUpWorker;
|
|
|
2478
2767
|
exports.InvalidConfigError = InvalidConfigError;
|
|
2479
2768
|
exports.InvalidPipelineError = InvalidPipelineError;
|
|
2480
2769
|
exports.PIPELINE_DEFAULTS = PIPELINE_DEFAULTS;
|
|
2770
|
+
exports.PipelineAnalyticsService = PipelineAnalyticsService;
|
|
2481
2771
|
exports.PipelineNotFoundError = PipelineNotFoundError;
|
|
2482
2772
|
exports.PipelineSchema = PipelineSchema;
|
|
2483
2773
|
exports.PipelineService = PipelineService;
|