@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.mjs
CHANGED
|
@@ -2,14 +2,14 @@ import { z } from 'zod';
|
|
|
2
2
|
import { AlxError, noopLogger, sendError, sendSuccess } from '@astralibx/core';
|
|
3
3
|
import { TimelineEntryType, CallPriority, CallDirection, DEFAULT_OPTIONS } from '@astralibx/call-log-types';
|
|
4
4
|
export { DEFAULT_OPTIONS } from '@astralibx/call-log-types';
|
|
5
|
-
import
|
|
5
|
+
import crypto6 from 'crypto';
|
|
6
6
|
import { Schema } from 'mongoose';
|
|
7
7
|
import { Router } from 'express';
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
10
10
|
var PipelineStageSchema = new Schema(
|
|
11
11
|
{
|
|
12
|
-
stageId: { type: String, required: true, default: () =>
|
|
12
|
+
stageId: { type: String, required: true, default: () => crypto6.randomUUID() },
|
|
13
13
|
name: { type: String, required: true },
|
|
14
14
|
color: { type: String, required: true },
|
|
15
15
|
order: { type: Number, required: true },
|
|
@@ -20,7 +20,7 @@ var PipelineStageSchema = new Schema(
|
|
|
20
20
|
);
|
|
21
21
|
var PipelineSchema = new Schema(
|
|
22
22
|
{
|
|
23
|
-
pipelineId: { type: String, required: true, unique: true, default: () =>
|
|
23
|
+
pipelineId: { type: String, required: true, unique: true, default: () => crypto6.randomUUID() },
|
|
24
24
|
name: { type: String, required: true },
|
|
25
25
|
description: { type: String },
|
|
26
26
|
stages: { type: [PipelineStageSchema], required: true },
|
|
@@ -50,7 +50,7 @@ var ContactRefSchema = new Schema(
|
|
|
50
50
|
);
|
|
51
51
|
var TimelineEntrySchema = new Schema(
|
|
52
52
|
{
|
|
53
|
-
entryId: { type: String, required: true, default: () =>
|
|
53
|
+
entryId: { type: String, required: true, default: () => crypto6.randomUUID() },
|
|
54
54
|
type: {
|
|
55
55
|
type: String,
|
|
56
56
|
required: true,
|
|
@@ -85,7 +85,7 @@ var StageChangeSchema = new Schema(
|
|
|
85
85
|
);
|
|
86
86
|
var CallLogSchema = new Schema(
|
|
87
87
|
{
|
|
88
|
-
callLogId: { type: String, required: true, unique: true, default: () =>
|
|
88
|
+
callLogId: { type: String, required: true, unique: true, default: () => crypto6.randomUUID() },
|
|
89
89
|
pipelineId: { type: String, required: true, index: true },
|
|
90
90
|
currentStageId: { type: String, required: true, index: true },
|
|
91
91
|
contactRef: { type: ContactRefSchema, required: true },
|
|
@@ -110,6 +110,11 @@ var CallLogSchema = new Schema(
|
|
|
110
110
|
timeline: { type: [TimelineEntrySchema], default: [] },
|
|
111
111
|
stageHistory: { type: [StageChangeSchema], default: [] },
|
|
112
112
|
durationMinutes: { type: Number, min: 0 },
|
|
113
|
+
channel: { type: String, default: "phone", index: true },
|
|
114
|
+
outcome: { type: String, default: "pending", index: true },
|
|
115
|
+
isFollowUp: { type: Boolean, default: false, index: true },
|
|
116
|
+
isDeleted: { type: Boolean, default: false },
|
|
117
|
+
deletedAt: { type: Date },
|
|
113
118
|
isClosed: { type: Boolean, default: false, index: true },
|
|
114
119
|
closedAt: { type: Date },
|
|
115
120
|
tenantId: { type: String, sparse: true },
|
|
@@ -122,6 +127,7 @@ var CallLogSchema = new Schema(
|
|
|
122
127
|
CallLogSchema.index({ "contactRef.externalId": 1 });
|
|
123
128
|
CallLogSchema.index({ pipelineId: 1, currentStageId: 1 });
|
|
124
129
|
CallLogSchema.index({ agentId: 1, isClosed: 1 });
|
|
130
|
+
CallLogSchema.index({ isDeleted: 1, isClosed: 1 });
|
|
125
131
|
function createCallLogModel(connection, prefix) {
|
|
126
132
|
const collectionName = prefix ? `${prefix}_call_logs` : "call_logs";
|
|
127
133
|
return connection.model("CallLog", CallLogSchema, collectionName);
|
|
@@ -135,6 +141,7 @@ var ERROR_CODE = {
|
|
|
135
141
|
StageInUse: "CALL_STAGE_IN_USE",
|
|
136
142
|
CallLogNotFound: "CALL_LOG_NOT_FOUND",
|
|
137
143
|
CallLogClosed: "CALL_LOG_CLOSED",
|
|
144
|
+
CallLogDeleted: "CALL_LOG_DELETED",
|
|
138
145
|
ContactNotFound: "CALL_CONTACT_NOT_FOUND",
|
|
139
146
|
AgentCapacityFull: "CALL_AGENT_CAPACITY_FULL",
|
|
140
147
|
InvalidConfig: "CALL_INVALID_CONFIG",
|
|
@@ -149,6 +156,7 @@ var ERROR_MESSAGE = {
|
|
|
149
156
|
StageInUse: "Cannot remove stage that has active calls",
|
|
150
157
|
CallLogNotFound: "Call log not found",
|
|
151
158
|
CallLogClosed: "Cannot modify a closed call log",
|
|
159
|
+
CallLogDeleted: "Call log has been deleted",
|
|
152
160
|
ContactNotFound: "Contact not found",
|
|
153
161
|
AgentCapacityFull: "Agent has reached maximum concurrent calls",
|
|
154
162
|
AuthFailed: "Authentication failed"
|
|
@@ -174,7 +182,9 @@ var SYSTEM_TIMELINE_FN = {
|
|
|
174
182
|
stageChanged: (from, to) => `Stage changed from "${from}" to "${to}"`,
|
|
175
183
|
callAssigned: (agentName) => `Call assigned to ${agentName}`,
|
|
176
184
|
callReassigned: (from, to) => `Call reassigned from ${from} to ${to}`,
|
|
177
|
-
followUpSet: (date) => `Follow-up scheduled for ${date}
|
|
185
|
+
followUpSet: (date) => `Follow-up scheduled for ${date}`,
|
|
186
|
+
callDeleted: (agentName) => `Call deleted by ${agentName}`,
|
|
187
|
+
followUpCallCreated: () => "This call is a follow-up to a previous interaction"
|
|
178
188
|
};
|
|
179
189
|
|
|
180
190
|
// src/schemas/call-log-settings.schema.ts
|
|
@@ -198,6 +208,8 @@ var CallLogSettingsSchema = new Schema(
|
|
|
198
208
|
key: { type: String, required: true, default: "global" },
|
|
199
209
|
availableTags: { type: [String], default: [] },
|
|
200
210
|
availableCategories: { type: [String], default: [] },
|
|
211
|
+
availableChannels: { type: [String], default: ["phone", "whatsapp", "telegram", "in_app_chat"] },
|
|
212
|
+
availableOutcomes: { type: [String], default: ["pending", "interested", "not_interested", "no_answer", "busy", "callback_requested", "subscribed", "complaint"] },
|
|
201
213
|
priorityLevels: {
|
|
202
214
|
type: [PriorityConfigSchema],
|
|
203
215
|
default: () => DEFAULT_PRIORITY_LEVELS.map((p) => ({ ...p }))
|
|
@@ -460,10 +472,10 @@ var PipelineService = class {
|
|
|
460
472
|
async create(data) {
|
|
461
473
|
const stages = data.stages.map((s) => ({
|
|
462
474
|
...s,
|
|
463
|
-
stageId:
|
|
475
|
+
stageId: crypto6.randomUUID()
|
|
464
476
|
}));
|
|
465
477
|
validatePipelineStages(stages);
|
|
466
|
-
const pipelineId =
|
|
478
|
+
const pipelineId = crypto6.randomUUID();
|
|
467
479
|
if (data.isDefault) {
|
|
468
480
|
await this.Pipeline.updateMany(
|
|
469
481
|
{ ...this.tenantFilter, isDefault: true },
|
|
@@ -532,7 +544,7 @@ var PipelineService = class {
|
|
|
532
544
|
const pipeline = await this.get(pipelineId);
|
|
533
545
|
const newStage = {
|
|
534
546
|
...stage,
|
|
535
|
-
stageId:
|
|
547
|
+
stageId: crypto6.randomUUID()
|
|
536
548
|
};
|
|
537
549
|
const updatedStages = [...pipeline.stages, newStage];
|
|
538
550
|
validatePipelineStages(updatedStages, pipelineId);
|
|
@@ -631,7 +643,7 @@ var TimelineService = class {
|
|
|
631
643
|
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
632
644
|
if (callLog.isClosed) throw new CallLogClosedError(callLogId, "add note");
|
|
633
645
|
const entry = {
|
|
634
|
-
entryId:
|
|
646
|
+
entryId: crypto6.randomUUID(),
|
|
635
647
|
type: TimelineEntryType.Note,
|
|
636
648
|
content,
|
|
637
649
|
authorId,
|
|
@@ -656,7 +668,7 @@ var TimelineService = class {
|
|
|
656
668
|
const callLog = await this.CallLog.findOne({ callLogId });
|
|
657
669
|
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
658
670
|
const entry = {
|
|
659
|
-
entryId:
|
|
671
|
+
entryId: crypto6.randomUUID(),
|
|
660
672
|
type: TimelineEntryType.System,
|
|
661
673
|
content,
|
|
662
674
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -725,13 +737,23 @@ var CallLogService = class {
|
|
|
725
737
|
if (!pipeline) throw new PipelineNotFoundError(data.pipelineId);
|
|
726
738
|
const defaultStage = pipeline.stages.find((s) => s.isDefault);
|
|
727
739
|
if (!defaultStage) throw new PipelineNotFoundError(data.pipelineId);
|
|
728
|
-
const callLogId =
|
|
740
|
+
const callLogId = crypto6.randomUUID();
|
|
729
741
|
const initialEntry = {
|
|
730
|
-
entryId:
|
|
742
|
+
entryId: crypto6.randomUUID(),
|
|
731
743
|
type: TimelineEntryType.System,
|
|
732
744
|
content: SYSTEM_TIMELINE.CallCreated,
|
|
733
745
|
createdAt: /* @__PURE__ */ new Date()
|
|
734
746
|
};
|
|
747
|
+
const timelineEntries = [initialEntry];
|
|
748
|
+
if (data.isFollowUp) {
|
|
749
|
+
const followUpEntry = {
|
|
750
|
+
entryId: crypto6.randomUUID(),
|
|
751
|
+
type: TimelineEntryType.System,
|
|
752
|
+
content: SYSTEM_TIMELINE_FN.followUpCallCreated(),
|
|
753
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
754
|
+
};
|
|
755
|
+
timelineEntries.push(followUpEntry);
|
|
756
|
+
}
|
|
735
757
|
const callLog = await this.CallLog.create({
|
|
736
758
|
callLogId,
|
|
737
759
|
pipelineId: data.pipelineId,
|
|
@@ -745,7 +767,10 @@ var CallLogService = class {
|
|
|
745
767
|
category: data.category,
|
|
746
768
|
nextFollowUpDate: data.nextFollowUpDate,
|
|
747
769
|
durationMinutes: data.durationMinutes,
|
|
748
|
-
|
|
770
|
+
channel: data.channel,
|
|
771
|
+
outcome: data.outcome,
|
|
772
|
+
isFollowUp: data.isFollowUp ?? false,
|
|
773
|
+
timeline: timelineEntries,
|
|
749
774
|
stageHistory: [],
|
|
750
775
|
isClosed: false,
|
|
751
776
|
...data.tenantId ? { tenantId: data.tenantId } : {},
|
|
@@ -767,6 +792,9 @@ var CallLogService = class {
|
|
|
767
792
|
if (data.tags !== void 0) setFields.tags = data.tags;
|
|
768
793
|
if (data.category !== void 0) setFields.category = data.category;
|
|
769
794
|
if (data.durationMinutes !== void 0) setFields.durationMinutes = data.durationMinutes;
|
|
795
|
+
if (data.channel !== void 0) setFields.channel = data.channel;
|
|
796
|
+
if (data.outcome !== void 0) setFields.outcome = data.outcome;
|
|
797
|
+
if (data.isFollowUp !== void 0) setFields.isFollowUp = data.isFollowUp;
|
|
770
798
|
if (data.nextFollowUpDate !== void 0) {
|
|
771
799
|
const prevDate = callLog.nextFollowUpDate?.toISOString();
|
|
772
800
|
const newDate = data.nextFollowUpDate?.toISOString();
|
|
@@ -774,7 +802,7 @@ var CallLogService = class {
|
|
|
774
802
|
setFields.nextFollowUpDate = data.nextFollowUpDate;
|
|
775
803
|
if (data.nextFollowUpDate) {
|
|
776
804
|
const followUpEntry = {
|
|
777
|
-
entryId:
|
|
805
|
+
entryId: crypto6.randomUUID(),
|
|
778
806
|
type: TimelineEntryType.FollowUpSet,
|
|
779
807
|
content: SYSTEM_TIMELINE_FN.followUpSet(new Date(data.nextFollowUpDate).toISOString()),
|
|
780
808
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -801,6 +829,58 @@ var CallLogService = class {
|
|
|
801
829
|
this.logger.info("Call log updated", { callLogId, fields: Object.keys(data) });
|
|
802
830
|
return updated;
|
|
803
831
|
}
|
|
832
|
+
async list(filter = {}) {
|
|
833
|
+
const query = {};
|
|
834
|
+
if (filter.pipelineId) query.pipelineId = filter.pipelineId;
|
|
835
|
+
if (filter.currentStageId) query.currentStageId = filter.currentStageId;
|
|
836
|
+
if (filter.agentId) query.agentId = filter.agentId;
|
|
837
|
+
if (filter.tags && filter.tags.length > 0) query.tags = { $in: filter.tags };
|
|
838
|
+
if (filter.category) query.category = filter.category;
|
|
839
|
+
if (filter.isClosed !== void 0) query.isClosed = filter.isClosed;
|
|
840
|
+
if (filter.contactExternalId) query["contactRef.externalId"] = filter.contactExternalId;
|
|
841
|
+
if (filter.contactName) query["contactRef.displayName"] = { $regex: filter.contactName, $options: "i" };
|
|
842
|
+
if (filter.contactPhone) query["contactRef.phone"] = { $regex: `^${filter.contactPhone}` };
|
|
843
|
+
if (filter.contactEmail) query["contactRef.email"] = filter.contactEmail;
|
|
844
|
+
if (filter.priority) query.priority = filter.priority;
|
|
845
|
+
if (filter.direction) query.direction = filter.direction;
|
|
846
|
+
if (filter.channel) query.channel = filter.channel;
|
|
847
|
+
if (filter.outcome) query.outcome = filter.outcome;
|
|
848
|
+
if (filter.isFollowUp !== void 0) query.isFollowUp = filter.isFollowUp;
|
|
849
|
+
if (!filter.includeDeleted) query.isDeleted = { $ne: true };
|
|
850
|
+
if (filter.dateRange?.from || filter.dateRange?.to) {
|
|
851
|
+
const dateFilter = {};
|
|
852
|
+
if (filter.dateRange.from) dateFilter.$gte = new Date(filter.dateRange.from);
|
|
853
|
+
if (filter.dateRange.to) dateFilter.$lte = new Date(filter.dateRange.to);
|
|
854
|
+
query.callDate = dateFilter;
|
|
855
|
+
}
|
|
856
|
+
const page = filter.page ?? 1;
|
|
857
|
+
const limit = filter.limit ?? 20;
|
|
858
|
+
const skip = (page - 1) * limit;
|
|
859
|
+
const sort = filter.sort ?? { callDate: -1 };
|
|
860
|
+
const [callLogs, total] = await Promise.all([
|
|
861
|
+
this.CallLog.find(query).sort(sort).skip(skip).limit(limit),
|
|
862
|
+
this.CallLog.countDocuments(query)
|
|
863
|
+
]);
|
|
864
|
+
return { callLogs, total, page, limit };
|
|
865
|
+
}
|
|
866
|
+
async get(callLogId) {
|
|
867
|
+
const callLog = await this.CallLog.findOne({ callLogId, isDeleted: { $ne: true } });
|
|
868
|
+
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
869
|
+
return callLog;
|
|
870
|
+
}
|
|
871
|
+
async getByContact(externalId) {
|
|
872
|
+
return this.CallLog.find({ "contactRef.externalId": externalId, isDeleted: { $ne: true } }).sort({ callDate: -1 });
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
var CallLogLifecycleService = class {
|
|
876
|
+
constructor(CallLog, Pipeline, timeline, logger, hooks, options) {
|
|
877
|
+
this.CallLog = CallLog;
|
|
878
|
+
this.Pipeline = Pipeline;
|
|
879
|
+
this.timeline = timeline;
|
|
880
|
+
this.logger = logger;
|
|
881
|
+
this.hooks = hooks;
|
|
882
|
+
this.options = options;
|
|
883
|
+
}
|
|
804
884
|
async changeStage(callLogId, newStageId, agentId) {
|
|
805
885
|
const callLog = await this.CallLog.findOne({ callLogId });
|
|
806
886
|
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
@@ -817,7 +897,7 @@ var CallLogService = class {
|
|
|
817
897
|
const stageStartTime = lastHistory?.changedAt?.getTime() ?? callLog.createdAt?.getTime() ?? now.getTime();
|
|
818
898
|
const timeInStageMs = now.getTime() - stageStartTime;
|
|
819
899
|
const stageChangeEntry = {
|
|
820
|
-
entryId:
|
|
900
|
+
entryId: crypto6.randomUUID(),
|
|
821
901
|
type: TimelineEntryType.StageChange,
|
|
822
902
|
content: SYSTEM_TIMELINE_FN.stageChanged(currentStage.name, newStage.name),
|
|
823
903
|
fromStageId: currentStageId,
|
|
@@ -866,7 +946,7 @@ var CallLogService = class {
|
|
|
866
946
|
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
867
947
|
const previousAgentId = callLog.agentId?.toString();
|
|
868
948
|
const assignmentEntry = {
|
|
869
|
-
entryId:
|
|
949
|
+
entryId: crypto6.randomUUID(),
|
|
870
950
|
type: TimelineEntryType.Assignment,
|
|
871
951
|
content: previousAgentId ? SYSTEM_TIMELINE_FN.callReassigned(previousAgentId, agentId) : SYSTEM_TIMELINE_FN.callAssigned(agentId),
|
|
872
952
|
toAgentId: agentId,
|
|
@@ -894,88 +974,13 @@ var CallLogService = class {
|
|
|
894
974
|
}
|
|
895
975
|
return updated;
|
|
896
976
|
}
|
|
897
|
-
async list(filter = {}) {
|
|
898
|
-
const query = {};
|
|
899
|
-
if (filter.pipelineId) query.pipelineId = filter.pipelineId;
|
|
900
|
-
if (filter.currentStageId) query.currentStageId = filter.currentStageId;
|
|
901
|
-
if (filter.agentId) query.agentId = filter.agentId;
|
|
902
|
-
if (filter.tags && filter.tags.length > 0) query.tags = { $in: filter.tags };
|
|
903
|
-
if (filter.category) query.category = filter.category;
|
|
904
|
-
if (filter.isClosed !== void 0) query.isClosed = filter.isClosed;
|
|
905
|
-
if (filter.contactExternalId) query["contactRef.externalId"] = filter.contactExternalId;
|
|
906
|
-
if (filter.contactName) query["contactRef.displayName"] = { $regex: filter.contactName, $options: "i" };
|
|
907
|
-
if (filter.contactPhone) query["contactRef.phone"] = { $regex: `^${filter.contactPhone}` };
|
|
908
|
-
if (filter.contactEmail) query["contactRef.email"] = filter.contactEmail;
|
|
909
|
-
if (filter.priority) query.priority = filter.priority;
|
|
910
|
-
if (filter.direction) query.direction = filter.direction;
|
|
911
|
-
if (filter.dateRange?.from || filter.dateRange?.to) {
|
|
912
|
-
const dateFilter = {};
|
|
913
|
-
if (filter.dateRange.from) dateFilter.$gte = new Date(filter.dateRange.from);
|
|
914
|
-
if (filter.dateRange.to) dateFilter.$lte = new Date(filter.dateRange.to);
|
|
915
|
-
query.callDate = dateFilter;
|
|
916
|
-
}
|
|
917
|
-
const page = filter.page ?? 1;
|
|
918
|
-
const limit = filter.limit ?? 20;
|
|
919
|
-
const skip = (page - 1) * limit;
|
|
920
|
-
const sort = filter.sort ?? { callDate: -1 };
|
|
921
|
-
const [callLogs, total] = await Promise.all([
|
|
922
|
-
this.CallLog.find(query).sort(sort).skip(skip).limit(limit),
|
|
923
|
-
this.CallLog.countDocuments(query)
|
|
924
|
-
]);
|
|
925
|
-
return { callLogs, total, page, limit };
|
|
926
|
-
}
|
|
927
|
-
async get(callLogId) {
|
|
928
|
-
const callLog = await this.CallLog.findOne({ callLogId });
|
|
929
|
-
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
930
|
-
return callLog;
|
|
931
|
-
}
|
|
932
|
-
async getByContact(externalId) {
|
|
933
|
-
return this.CallLog.find({ "contactRef.externalId": externalId }).sort({ callDate: -1 });
|
|
934
|
-
}
|
|
935
|
-
async getFollowUpsDue(agentId, dateRange) {
|
|
936
|
-
const now = /* @__PURE__ */ new Date();
|
|
937
|
-
const query = {
|
|
938
|
-
nextFollowUpDate: { $lte: now },
|
|
939
|
-
isClosed: false,
|
|
940
|
-
followUpNotifiedAt: null
|
|
941
|
-
};
|
|
942
|
-
if (agentId) query.agentId = agentId;
|
|
943
|
-
if (dateRange?.from || dateRange?.to) {
|
|
944
|
-
const dateFilter = {};
|
|
945
|
-
if (dateRange.from) dateFilter.$gte = new Date(dateRange.from);
|
|
946
|
-
if (dateRange.to) dateFilter.$lte = new Date(dateRange.to);
|
|
947
|
-
query.nextFollowUpDate = { ...query.nextFollowUpDate, ...dateFilter };
|
|
948
|
-
}
|
|
949
|
-
return this.CallLog.find(query).sort({ nextFollowUpDate: 1 });
|
|
950
|
-
}
|
|
951
|
-
async bulkChangeStage(callLogIds, newStageId, agentId) {
|
|
952
|
-
const succeeded = [];
|
|
953
|
-
const failed = [];
|
|
954
|
-
for (const callLogId of callLogIds) {
|
|
955
|
-
try {
|
|
956
|
-
await this.changeStage(callLogId, newStageId, agentId);
|
|
957
|
-
succeeded.push(callLogId);
|
|
958
|
-
} catch (err) {
|
|
959
|
-
failed.push({
|
|
960
|
-
callLogId,
|
|
961
|
-
error: err instanceof Error ? err.message : String(err)
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
this.logger.info("Bulk stage change completed", {
|
|
966
|
-
total: callLogIds.length,
|
|
967
|
-
succeeded: succeeded.length,
|
|
968
|
-
failed: failed.length
|
|
969
|
-
});
|
|
970
|
-
return { succeeded, failed, total: callLogIds.length };
|
|
971
|
-
}
|
|
972
977
|
async close(callLogId, agentId) {
|
|
973
978
|
const callLog = await this.CallLog.findOne({ callLogId });
|
|
974
979
|
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
975
980
|
if (callLog.isClosed) throw new CallLogClosedError(callLogId, "close");
|
|
976
981
|
const now = /* @__PURE__ */ new Date();
|
|
977
982
|
const closeEntry = {
|
|
978
|
-
entryId:
|
|
983
|
+
entryId: crypto6.randomUUID(),
|
|
979
984
|
type: TimelineEntryType.System,
|
|
980
985
|
content: SYSTEM_TIMELINE.CallClosed,
|
|
981
986
|
authorId: agentId,
|
|
@@ -1007,7 +1012,7 @@ var CallLogService = class {
|
|
|
1007
1012
|
if (!callLog.isClosed) throw new CallLogClosedError(callLogId, "reopen");
|
|
1008
1013
|
const now = /* @__PURE__ */ new Date();
|
|
1009
1014
|
const reopenEntry = {
|
|
1010
|
-
entryId:
|
|
1015
|
+
entryId: crypto6.randomUUID(),
|
|
1011
1016
|
type: TimelineEntryType.System,
|
|
1012
1017
|
content: SYSTEM_TIMELINE.CallReopened,
|
|
1013
1018
|
authorId: agentId,
|
|
@@ -1030,6 +1035,72 @@ var CallLogService = class {
|
|
|
1030
1035
|
this.logger.info("Call log reopened", { callLogId, agentId });
|
|
1031
1036
|
return updated;
|
|
1032
1037
|
}
|
|
1038
|
+
async bulkChangeStage(callLogIds, newStageId, agentId) {
|
|
1039
|
+
const succeeded = [];
|
|
1040
|
+
const failed = [];
|
|
1041
|
+
for (const callLogId of callLogIds) {
|
|
1042
|
+
try {
|
|
1043
|
+
await this.changeStage(callLogId, newStageId, agentId);
|
|
1044
|
+
succeeded.push(callLogId);
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
failed.push({
|
|
1047
|
+
callLogId,
|
|
1048
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
this.logger.info("Bulk stage change completed", {
|
|
1053
|
+
total: callLogIds.length,
|
|
1054
|
+
succeeded: succeeded.length,
|
|
1055
|
+
failed: failed.length
|
|
1056
|
+
});
|
|
1057
|
+
return { succeeded, failed, total: callLogIds.length };
|
|
1058
|
+
}
|
|
1059
|
+
async getFollowUpsDue(agentId, dateRange) {
|
|
1060
|
+
const now = /* @__PURE__ */ new Date();
|
|
1061
|
+
const query = {
|
|
1062
|
+
nextFollowUpDate: { $lte: now },
|
|
1063
|
+
isClosed: false,
|
|
1064
|
+
followUpNotifiedAt: null,
|
|
1065
|
+
isDeleted: { $ne: true }
|
|
1066
|
+
};
|
|
1067
|
+
if (agentId) query.agentId = agentId;
|
|
1068
|
+
if (dateRange?.from || dateRange?.to) {
|
|
1069
|
+
const dateFilter = {};
|
|
1070
|
+
if (dateRange.from) dateFilter.$gte = new Date(dateRange.from);
|
|
1071
|
+
if (dateRange.to) dateFilter.$lte = new Date(dateRange.to);
|
|
1072
|
+
query.nextFollowUpDate = { ...query.nextFollowUpDate, ...dateFilter };
|
|
1073
|
+
}
|
|
1074
|
+
return this.CallLog.find(query).sort({ nextFollowUpDate: 1 });
|
|
1075
|
+
}
|
|
1076
|
+
async softDelete(callLogId, agentId, agentName) {
|
|
1077
|
+
const callLog = await this.CallLog.findOne({ callLogId });
|
|
1078
|
+
if (!callLog) throw new CallLogNotFoundError(callLogId);
|
|
1079
|
+
const now = /* @__PURE__ */ new Date();
|
|
1080
|
+
const deleteEntry = {
|
|
1081
|
+
entryId: crypto6.randomUUID(),
|
|
1082
|
+
type: TimelineEntryType.System,
|
|
1083
|
+
content: agentName ? SYSTEM_TIMELINE_FN.callDeleted(agentName) : SYSTEM_TIMELINE_FN.callDeleted(agentId ?? "unknown"),
|
|
1084
|
+
authorId: agentId,
|
|
1085
|
+
createdAt: now
|
|
1086
|
+
};
|
|
1087
|
+
const updated = await this.CallLog.findOneAndUpdate(
|
|
1088
|
+
{ callLogId },
|
|
1089
|
+
{
|
|
1090
|
+
$set: { isDeleted: true, deletedAt: now },
|
|
1091
|
+
$push: {
|
|
1092
|
+
timeline: {
|
|
1093
|
+
$each: [deleteEntry],
|
|
1094
|
+
$slice: -this.options.maxTimelineEntries
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
{ new: true }
|
|
1099
|
+
);
|
|
1100
|
+
if (!updated) throw new CallLogNotFoundError(callLogId);
|
|
1101
|
+
this.logger.info("Call log soft deleted", { callLogId });
|
|
1102
|
+
return updated;
|
|
1103
|
+
}
|
|
1033
1104
|
};
|
|
1034
1105
|
|
|
1035
1106
|
// src/services/analytics.service.ts
|
|
@@ -1060,6 +1131,7 @@ var AnalyticsService = class {
|
|
|
1060
1131
|
const now = /* @__PURE__ */ new Date();
|
|
1061
1132
|
const matchStage = {
|
|
1062
1133
|
agentId,
|
|
1134
|
+
isDeleted: { $ne: true },
|
|
1063
1135
|
...this.buildDateMatch(dateRange)
|
|
1064
1136
|
};
|
|
1065
1137
|
const pipeline = [
|
|
@@ -1152,7 +1224,10 @@ var AnalyticsService = class {
|
|
|
1152
1224
|
}
|
|
1153
1225
|
async getAgentLeaderboard(dateRange) {
|
|
1154
1226
|
const now = /* @__PURE__ */ new Date();
|
|
1155
|
-
const matchStage =
|
|
1227
|
+
const matchStage = {
|
|
1228
|
+
isDeleted: { $ne: true },
|
|
1229
|
+
...this.buildDateMatch(dateRange)
|
|
1230
|
+
};
|
|
1156
1231
|
const pipeline = [
|
|
1157
1232
|
{ $match: matchStage },
|
|
1158
1233
|
{
|
|
@@ -1208,103 +1283,12 @@ var AnalyticsService = class {
|
|
|
1208
1283
|
closeRate: stat.totalCalls > 0 ? Math.round(stat.callsClosed / stat.totalCalls * 1e4) / 100 : 0
|
|
1209
1284
|
})));
|
|
1210
1285
|
}
|
|
1211
|
-
async
|
|
1212
|
-
const
|
|
1213
|
-
const pipelineName = pipeline?.name ?? pipelineId;
|
|
1214
|
-
const stages = pipeline?.stages ?? [];
|
|
1215
|
-
const matchStage = {
|
|
1216
|
-
pipelineId,
|
|
1217
|
-
...this.buildDateMatch(dateRange)
|
|
1218
|
-
};
|
|
1219
|
-
const [totalResult, stageAgg] = await Promise.all([
|
|
1220
|
-
this.CallLog.countDocuments(matchStage),
|
|
1221
|
-
this.CallLog.aggregate([
|
|
1222
|
-
{ $match: matchStage },
|
|
1223
|
-
{ $unwind: { path: "$stageHistory", preserveNullAndEmptyArrays: false } },
|
|
1224
|
-
{
|
|
1225
|
-
$group: {
|
|
1226
|
-
_id: "$stageHistory.toStageId",
|
|
1227
|
-
count: { $sum: 1 },
|
|
1228
|
-
totalTimeMs: { $sum: "$stageHistory.timeInStageMs" }
|
|
1229
|
-
}
|
|
1230
|
-
},
|
|
1231
|
-
{
|
|
1232
|
-
$project: {
|
|
1233
|
-
stageId: "$_id",
|
|
1234
|
-
count: 1,
|
|
1235
|
-
totalTimeMs: 1
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
])
|
|
1239
|
-
]);
|
|
1240
|
-
const totalCalls = totalResult;
|
|
1241
|
-
const stageMap = new Map(stageAgg.map((s) => [s.stageId, s]));
|
|
1242
|
-
let bottleneckStage = null;
|
|
1243
|
-
let maxAvgTime = 0;
|
|
1244
|
-
const stageStats = stages.map((s) => {
|
|
1245
|
-
const agg = stageMap.get(s.stageId);
|
|
1246
|
-
const count = agg?.count ?? 0;
|
|
1247
|
-
const avgTimeMs = count > 0 ? Math.round((agg?.totalTimeMs ?? 0) / count) : 0;
|
|
1248
|
-
const conversionRate = totalCalls > 0 ? Math.round(count / totalCalls * 1e4) / 100 : 0;
|
|
1249
|
-
if (avgTimeMs > maxAvgTime) {
|
|
1250
|
-
maxAvgTime = avgTimeMs;
|
|
1251
|
-
bottleneckStage = s.stageId;
|
|
1252
|
-
}
|
|
1253
|
-
return {
|
|
1254
|
-
stageId: s.stageId,
|
|
1255
|
-
stageName: s.name,
|
|
1256
|
-
count,
|
|
1257
|
-
avgTimeMs,
|
|
1258
|
-
conversionRate
|
|
1259
|
-
};
|
|
1260
|
-
});
|
|
1261
|
-
this.logger.info("Pipeline stats computed", { pipelineId, totalCalls });
|
|
1262
|
-
return {
|
|
1263
|
-
pipelineId,
|
|
1264
|
-
pipelineName,
|
|
1265
|
-
totalCalls,
|
|
1266
|
-
stages: stageStats,
|
|
1267
|
-
bottleneckStage
|
|
1268
|
-
};
|
|
1269
|
-
}
|
|
1270
|
-
async getPipelineFunnel(pipelineId, dateRange) {
|
|
1271
|
-
const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
|
|
1272
|
-
const pipelineName = pipeline?.name ?? pipelineId;
|
|
1273
|
-
const stages = pipeline?.stages ?? [];
|
|
1286
|
+
async getTeamStats(teamId, dateRange = {}) {
|
|
1287
|
+
const now = /* @__PURE__ */ new Date();
|
|
1274
1288
|
const matchStage = {
|
|
1275
|
-
|
|
1289
|
+
isDeleted: { $ne: true },
|
|
1276
1290
|
...this.buildDateMatch(dateRange)
|
|
1277
1291
|
};
|
|
1278
|
-
const [enteredAgg, exitedAgg] = await Promise.all([
|
|
1279
|
-
this.CallLog.aggregate([
|
|
1280
|
-
{ $match: matchStage },
|
|
1281
|
-
{ $unwind: "$stageHistory" },
|
|
1282
|
-
{ $group: { _id: "$stageHistory.toStageId", count: { $sum: 1 } } }
|
|
1283
|
-
]),
|
|
1284
|
-
this.CallLog.aggregate([
|
|
1285
|
-
{ $match: matchStage },
|
|
1286
|
-
{ $unwind: "$stageHistory" },
|
|
1287
|
-
{ $group: { _id: "$stageHistory.fromStageId", count: { $sum: 1 } } }
|
|
1288
|
-
])
|
|
1289
|
-
]);
|
|
1290
|
-
const enteredMap = new Map(enteredAgg.map((r) => [String(r._id), r.count]));
|
|
1291
|
-
const exitedMap = new Map(exitedAgg.map((r) => [String(r._id), r.count]));
|
|
1292
|
-
const funnelStages = stages.map((s) => {
|
|
1293
|
-
const entered = enteredMap.get(s.stageId) ?? 0;
|
|
1294
|
-
const exited = exitedMap.get(s.stageId) ?? 0;
|
|
1295
|
-
return {
|
|
1296
|
-
stageId: s.stageId,
|
|
1297
|
-
stageName: s.name,
|
|
1298
|
-
entered,
|
|
1299
|
-
exited,
|
|
1300
|
-
dropOff: Math.max(0, entered - exited)
|
|
1301
|
-
};
|
|
1302
|
-
});
|
|
1303
|
-
return { pipelineId, pipelineName, stages: funnelStages };
|
|
1304
|
-
}
|
|
1305
|
-
async getTeamStats(teamId, dateRange = {}) {
|
|
1306
|
-
const now = /* @__PURE__ */ new Date();
|
|
1307
|
-
const matchStage = this.buildDateMatch(dateRange);
|
|
1308
1292
|
const pipeline = [
|
|
1309
1293
|
{ $match: matchStage },
|
|
1310
1294
|
{
|
|
@@ -1361,8 +1345,12 @@ var AnalyticsService = class {
|
|
|
1361
1345
|
const totalCalls = agentStats.reduce((sum, a) => sum + a.totalCalls, 0);
|
|
1362
1346
|
return { teamId: teamId ?? null, agentStats, totalCalls };
|
|
1363
1347
|
}
|
|
1364
|
-
async getDailyReport(dateRange) {
|
|
1365
|
-
const matchStage =
|
|
1348
|
+
async getDailyReport(dateRange, agentId) {
|
|
1349
|
+
const matchStage = {
|
|
1350
|
+
isDeleted: { $ne: true },
|
|
1351
|
+
...this.buildDateMatch(dateRange)
|
|
1352
|
+
};
|
|
1353
|
+
if (agentId) matchStage["agentId"] = agentId;
|
|
1366
1354
|
const [dailyAgg, directionAgg, pipelineAgg, agentAgg] = await Promise.all([
|
|
1367
1355
|
this.CallLog.aggregate([
|
|
1368
1356
|
{ $match: matchStage },
|
|
@@ -1442,13 +1430,14 @@ var AnalyticsService = class {
|
|
|
1442
1430
|
const now = /* @__PURE__ */ new Date();
|
|
1443
1431
|
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1444
1432
|
const [openCalls, closedToday, overdueFollowUps, callsToday] = await Promise.all([
|
|
1445
|
-
this.CallLog.countDocuments({ isClosed: false }),
|
|
1446
|
-
this.CallLog.countDocuments({ closedAt: { $gte: midnight } }),
|
|
1433
|
+
this.CallLog.countDocuments({ isClosed: false, isDeleted: { $ne: true } }),
|
|
1434
|
+
this.CallLog.countDocuments({ closedAt: { $gte: midnight }, isDeleted: { $ne: true } }),
|
|
1447
1435
|
this.CallLog.countDocuments({
|
|
1448
1436
|
nextFollowUpDate: { $lt: now },
|
|
1449
|
-
isClosed: false
|
|
1437
|
+
isClosed: false,
|
|
1438
|
+
isDeleted: { $ne: true }
|
|
1450
1439
|
}),
|
|
1451
|
-
this.CallLog.countDocuments({ callDate: { $gte: midnight } })
|
|
1440
|
+
this.CallLog.countDocuments({ callDate: { $gte: midnight }, isDeleted: { $ne: true } })
|
|
1452
1441
|
]);
|
|
1453
1442
|
return {
|
|
1454
1443
|
openCalls,
|
|
@@ -1462,7 +1451,7 @@ var AnalyticsService = class {
|
|
|
1462
1451
|
const now = /* @__PURE__ */ new Date();
|
|
1463
1452
|
const from = new Date(now.getTime() - weeks * 7 * 24 * 60 * 60 * 1e3);
|
|
1464
1453
|
const results = await this.CallLog.aggregate([
|
|
1465
|
-
{ $match: { callDate: { $gte: from, $lte: now } } },
|
|
1454
|
+
{ $match: { callDate: { $gte: from, $lte: now }, isDeleted: { $ne: true } } },
|
|
1466
1455
|
{
|
|
1467
1456
|
$group: {
|
|
1468
1457
|
_id: {
|
|
@@ -1489,8 +1478,11 @@ var AnalyticsService = class {
|
|
|
1489
1478
|
return results;
|
|
1490
1479
|
}
|
|
1491
1480
|
async getOverallReport(dateRange) {
|
|
1492
|
-
const matchStage =
|
|
1493
|
-
|
|
1481
|
+
const matchStage = {
|
|
1482
|
+
isDeleted: { $ne: true },
|
|
1483
|
+
...this.buildDateMatch(dateRange)
|
|
1484
|
+
};
|
|
1485
|
+
const [summaryAgg, tagAgg, categoryAgg, peakHoursAgg, followUpAgg, channelAgg, outcomeAgg] = await Promise.all([
|
|
1494
1486
|
this.CallLog.aggregate([
|
|
1495
1487
|
{ $match: matchStage },
|
|
1496
1488
|
{
|
|
@@ -1535,14 +1527,14 @@ var AnalyticsService = class {
|
|
|
1535
1527
|
_id: null,
|
|
1536
1528
|
total: { $sum: 1 },
|
|
1537
1529
|
withFollowUp: {
|
|
1538
|
-
$sum: { $cond: [{ $
|
|
1530
|
+
$sum: { $cond: [{ $eq: ["$isFollowUp", true] }, 1, 0] }
|
|
1539
1531
|
},
|
|
1540
1532
|
completed: {
|
|
1541
1533
|
$sum: {
|
|
1542
1534
|
$cond: [
|
|
1543
1535
|
{
|
|
1544
1536
|
$and: [
|
|
1545
|
-
{ $
|
|
1537
|
+
{ $eq: ["$isFollowUp", true] },
|
|
1546
1538
|
{ $eq: ["$isClosed", true] }
|
|
1547
1539
|
]
|
|
1548
1540
|
},
|
|
@@ -1553,11 +1545,31 @@ var AnalyticsService = class {
|
|
|
1553
1545
|
}
|
|
1554
1546
|
}
|
|
1555
1547
|
}
|
|
1548
|
+
]),
|
|
1549
|
+
this.CallLog.aggregate([
|
|
1550
|
+
{ $match: matchStage },
|
|
1551
|
+
{ $group: { _id: "$channel", count: { $sum: 1 } } },
|
|
1552
|
+
{ $sort: { count: -1 } }
|
|
1553
|
+
]),
|
|
1554
|
+
this.CallLog.aggregate([
|
|
1555
|
+
{ $match: matchStage },
|
|
1556
|
+
{ $group: { _id: "$outcome", count: { $sum: 1 } } },
|
|
1557
|
+
{ $sort: { count: -1 } }
|
|
1556
1558
|
])
|
|
1557
1559
|
]);
|
|
1558
1560
|
const summary = summaryAgg[0] ?? { totalCalls: 0, closedCalls: 0, avgTimeToCloseMs: 0 };
|
|
1559
|
-
const followUp = followUpAgg[0] ?? { withFollowUp: 0, completed: 0 };
|
|
1561
|
+
const followUp = followUpAgg[0] ?? { total: 0, withFollowUp: 0, completed: 0 };
|
|
1560
1562
|
const followUpComplianceRate = followUp.withFollowUp > 0 ? Math.round(followUp.completed / followUp.withFollowUp * 1e4) / 100 : 0;
|
|
1563
|
+
const followUpCalls = followUp.withFollowUp;
|
|
1564
|
+
const followUpRatio = followUp.total > 0 ? Math.round(followUp.withFollowUp / followUp.total * 1e4) / 100 : 0;
|
|
1565
|
+
const channelDistribution = channelAgg.map((r) => ({
|
|
1566
|
+
channel: r._id != null ? String(r._id) : "unknown",
|
|
1567
|
+
count: r.count
|
|
1568
|
+
}));
|
|
1569
|
+
const outcomeDistribution = outcomeAgg.map((r) => ({
|
|
1570
|
+
outcome: r._id != null ? String(r._id) : "unknown",
|
|
1571
|
+
count: r.count
|
|
1572
|
+
}));
|
|
1561
1573
|
return {
|
|
1562
1574
|
totalCalls: summary.totalCalls,
|
|
1563
1575
|
closedCalls: summary.closedCalls,
|
|
@@ -1565,7 +1577,180 @@ var AnalyticsService = class {
|
|
|
1565
1577
|
followUpComplianceRate,
|
|
1566
1578
|
tagDistribution: tagAgg.map((r) => ({ tag: String(r._id), count: r.count })),
|
|
1567
1579
|
categoryDistribution: categoryAgg.map((r) => ({ category: String(r._id), count: r.count })),
|
|
1568
|
-
peakCallHours: peakHoursAgg.map((r) => ({ hour: Number(r._id), count: r.count }))
|
|
1580
|
+
peakCallHours: peakHoursAgg.map((r) => ({ hour: Number(r._id), count: r.count })),
|
|
1581
|
+
channelDistribution,
|
|
1582
|
+
outcomeDistribution,
|
|
1583
|
+
followUpCalls,
|
|
1584
|
+
followUpRatio
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// src/services/pipeline-analytics.service.ts
|
|
1590
|
+
var PipelineAnalyticsService = class {
|
|
1591
|
+
constructor(CallLog, Pipeline, logger, resolveAgent, tenantId) {
|
|
1592
|
+
this.CallLog = CallLog;
|
|
1593
|
+
this.Pipeline = Pipeline;
|
|
1594
|
+
this.logger = logger;
|
|
1595
|
+
this.resolveAgent = resolveAgent;
|
|
1596
|
+
this.tenantId = tenantId;
|
|
1597
|
+
}
|
|
1598
|
+
buildDateMatch(dateRange, field = "callDate") {
|
|
1599
|
+
if (!dateRange.from && !dateRange.to) return {};
|
|
1600
|
+
const dateFilter = {};
|
|
1601
|
+
if (dateRange.from) dateFilter.$gte = new Date(dateRange.from);
|
|
1602
|
+
if (dateRange.to) dateFilter.$lte = new Date(dateRange.to);
|
|
1603
|
+
return { [field]: dateFilter };
|
|
1604
|
+
}
|
|
1605
|
+
async getPipelineStats(pipelineId, dateRange) {
|
|
1606
|
+
const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
|
|
1607
|
+
const pipelineName = pipeline?.name ?? pipelineId;
|
|
1608
|
+
const stages = pipeline?.stages ?? [];
|
|
1609
|
+
const matchStage = {
|
|
1610
|
+
pipelineId,
|
|
1611
|
+
isDeleted: { $ne: true },
|
|
1612
|
+
...this.buildDateMatch(dateRange)
|
|
1613
|
+
};
|
|
1614
|
+
const [totalResult, stageAgg] = await Promise.all([
|
|
1615
|
+
this.CallLog.countDocuments(matchStage),
|
|
1616
|
+
this.CallLog.aggregate([
|
|
1617
|
+
{ $match: matchStage },
|
|
1618
|
+
{ $unwind: { path: "$stageHistory", preserveNullAndEmptyArrays: false } },
|
|
1619
|
+
{
|
|
1620
|
+
$group: {
|
|
1621
|
+
_id: "$stageHistory.toStageId",
|
|
1622
|
+
count: { $sum: 1 },
|
|
1623
|
+
totalTimeMs: { $sum: "$stageHistory.timeInStageMs" }
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
$project: {
|
|
1628
|
+
stageId: "$_id",
|
|
1629
|
+
count: 1,
|
|
1630
|
+
totalTimeMs: 1
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
])
|
|
1634
|
+
]);
|
|
1635
|
+
const totalCalls = totalResult;
|
|
1636
|
+
const stageMap = new Map(stageAgg.map((s) => [s.stageId, s]));
|
|
1637
|
+
let bottleneckStage = null;
|
|
1638
|
+
let maxAvgTime = 0;
|
|
1639
|
+
const stageStats = stages.map((s) => {
|
|
1640
|
+
const agg = stageMap.get(s.stageId);
|
|
1641
|
+
const count = agg?.count ?? 0;
|
|
1642
|
+
const avgTimeMs = count > 0 ? Math.round((agg?.totalTimeMs ?? 0) / count) : 0;
|
|
1643
|
+
const conversionRate = totalCalls > 0 ? Math.round(count / totalCalls * 1e4) / 100 : 0;
|
|
1644
|
+
if (avgTimeMs > maxAvgTime) {
|
|
1645
|
+
maxAvgTime = avgTimeMs;
|
|
1646
|
+
bottleneckStage = s.stageId;
|
|
1647
|
+
}
|
|
1648
|
+
return {
|
|
1649
|
+
stageId: s.stageId,
|
|
1650
|
+
stageName: s.name,
|
|
1651
|
+
count,
|
|
1652
|
+
avgTimeMs,
|
|
1653
|
+
conversionRate
|
|
1654
|
+
};
|
|
1655
|
+
});
|
|
1656
|
+
this.logger.info("Pipeline stats computed", { pipelineId, totalCalls });
|
|
1657
|
+
return {
|
|
1658
|
+
pipelineId,
|
|
1659
|
+
pipelineName,
|
|
1660
|
+
totalCalls,
|
|
1661
|
+
stages: stageStats,
|
|
1662
|
+
bottleneckStage
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
async getPipelineFunnel(pipelineId, dateRange) {
|
|
1666
|
+
const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
|
|
1667
|
+
const pipelineName = pipeline?.name ?? pipelineId;
|
|
1668
|
+
const stages = pipeline?.stages ?? [];
|
|
1669
|
+
const matchStage = {
|
|
1670
|
+
pipelineId,
|
|
1671
|
+
isDeleted: { $ne: true },
|
|
1672
|
+
...this.buildDateMatch(dateRange)
|
|
1673
|
+
};
|
|
1674
|
+
const [enteredAgg, exitedAgg] = await Promise.all([
|
|
1675
|
+
this.CallLog.aggregate([
|
|
1676
|
+
{ $match: matchStage },
|
|
1677
|
+
{ $unwind: "$stageHistory" },
|
|
1678
|
+
{ $group: { _id: "$stageHistory.toStageId", count: { $sum: 1 } } }
|
|
1679
|
+
]),
|
|
1680
|
+
this.CallLog.aggregate([
|
|
1681
|
+
{ $match: matchStage },
|
|
1682
|
+
{ $unwind: "$stageHistory" },
|
|
1683
|
+
{ $group: { _id: "$stageHistory.fromStageId", count: { $sum: 1 } } }
|
|
1684
|
+
])
|
|
1685
|
+
]);
|
|
1686
|
+
const enteredMap = new Map(enteredAgg.map((r) => [String(r._id), r.count]));
|
|
1687
|
+
const exitedMap = new Map(exitedAgg.map((r) => [String(r._id), r.count]));
|
|
1688
|
+
const funnelStages = stages.map((s) => {
|
|
1689
|
+
const entered = enteredMap.get(s.stageId) ?? 0;
|
|
1690
|
+
const exited = exitedMap.get(s.stageId) ?? 0;
|
|
1691
|
+
return {
|
|
1692
|
+
stageId: s.stageId,
|
|
1693
|
+
stageName: s.name,
|
|
1694
|
+
entered,
|
|
1695
|
+
exited,
|
|
1696
|
+
dropOff: Math.max(0, entered - exited)
|
|
1697
|
+
};
|
|
1698
|
+
});
|
|
1699
|
+
return { pipelineId, pipelineName, stages: funnelStages };
|
|
1700
|
+
}
|
|
1701
|
+
async getChannelDistribution(dateRange) {
|
|
1702
|
+
const matchStage = {
|
|
1703
|
+
isDeleted: { $ne: true },
|
|
1704
|
+
...this.buildDateMatch(dateRange)
|
|
1705
|
+
};
|
|
1706
|
+
const results = await this.CallLog.aggregate([
|
|
1707
|
+
{ $match: matchStage },
|
|
1708
|
+
{ $group: { _id: "$channel", count: { $sum: 1 } } },
|
|
1709
|
+
{ $sort: { count: -1 } }
|
|
1710
|
+
]);
|
|
1711
|
+
return results.map((r) => ({
|
|
1712
|
+
channel: r._id != null ? String(r._id) : "unknown",
|
|
1713
|
+
count: r.count
|
|
1714
|
+
}));
|
|
1715
|
+
}
|
|
1716
|
+
async getOutcomeDistribution(dateRange) {
|
|
1717
|
+
const matchStage = {
|
|
1718
|
+
isDeleted: { $ne: true },
|
|
1719
|
+
...this.buildDateMatch(dateRange)
|
|
1720
|
+
};
|
|
1721
|
+
const results = await this.CallLog.aggregate([
|
|
1722
|
+
{ $match: matchStage },
|
|
1723
|
+
{ $group: { _id: "$outcome", count: { $sum: 1 } } },
|
|
1724
|
+
{ $sort: { count: -1 } }
|
|
1725
|
+
]);
|
|
1726
|
+
return results.map((r) => ({
|
|
1727
|
+
outcome: r._id != null ? String(r._id) : "unknown",
|
|
1728
|
+
count: r.count
|
|
1729
|
+
}));
|
|
1730
|
+
}
|
|
1731
|
+
async getFollowUpStats(dateRange) {
|
|
1732
|
+
const matchStage = {
|
|
1733
|
+
isDeleted: { $ne: true },
|
|
1734
|
+
...this.buildDateMatch(dateRange)
|
|
1735
|
+
};
|
|
1736
|
+
const results = await this.CallLog.aggregate([
|
|
1737
|
+
{ $match: matchStage },
|
|
1738
|
+
{
|
|
1739
|
+
$group: {
|
|
1740
|
+
_id: null,
|
|
1741
|
+
total: { $sum: 1 },
|
|
1742
|
+
followUpCalls: {
|
|
1743
|
+
$sum: { $cond: [{ $eq: ["$isFollowUp", true] }, 1, 0] }
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
]);
|
|
1748
|
+
const stat = results[0] ?? { total: 0, followUpCalls: 0 };
|
|
1749
|
+
const followUpRatio = stat.total > 0 ? Math.round(stat.followUpCalls / stat.total * 1e4) / 100 : 0;
|
|
1750
|
+
return {
|
|
1751
|
+
followUpCalls: stat.followUpCalls,
|
|
1752
|
+
totalCalls: stat.total,
|
|
1753
|
+
followUpRatio
|
|
1569
1754
|
};
|
|
1570
1755
|
}
|
|
1571
1756
|
};
|
|
@@ -1573,9 +1758,9 @@ var AnalyticsService = class {
|
|
|
1573
1758
|
// src/services/export.service.ts
|
|
1574
1759
|
var CSV_HEADER = "callLogId,contactName,contactPhone,contactEmail,direction,pipelineId,currentStageId,priority,agentId,callDate,isClosed,tags";
|
|
1575
1760
|
var ExportService = class {
|
|
1576
|
-
constructor(CallLog,
|
|
1761
|
+
constructor(CallLog, pipelineAnalytics, logger) {
|
|
1577
1762
|
this.CallLog = CallLog;
|
|
1578
|
-
this.
|
|
1763
|
+
this.pipelineAnalytics = pipelineAnalytics;
|
|
1579
1764
|
this.logger = logger;
|
|
1580
1765
|
}
|
|
1581
1766
|
async exportCallLog(callLogId, format) {
|
|
@@ -1617,7 +1802,7 @@ var ExportService = class {
|
|
|
1617
1802
|
);
|
|
1618
1803
|
}
|
|
1619
1804
|
async exportPipelineReport(pipelineId, dateRange, format) {
|
|
1620
|
-
const report = await this.
|
|
1805
|
+
const report = await this.pipelineAnalytics.getPipelineStats(pipelineId, dateRange);
|
|
1621
1806
|
if (format === "csv") {
|
|
1622
1807
|
const header = "pipelineId,pipelineName,totalCalls,stageId,stageName,count,avgTimeMs,conversionRate,bottleneckStage";
|
|
1623
1808
|
const rows = [header];
|
|
@@ -1798,17 +1983,25 @@ function createPipelineRoutes(pipeline, logger) {
|
|
|
1798
1983
|
});
|
|
1799
1984
|
return router;
|
|
1800
1985
|
}
|
|
1801
|
-
function
|
|
1986
|
+
function getScopedAgentId(req, enableAgentScoping) {
|
|
1987
|
+
if (!enableAgentScoping) return void 0;
|
|
1988
|
+
const user = req.user;
|
|
1989
|
+
if (!user) return void 0;
|
|
1990
|
+
if (user.role === "owner") return void 0;
|
|
1991
|
+
return user.adminUserId;
|
|
1992
|
+
}
|
|
1993
|
+
function createCallLogRoutes(services, logger, enableAgentScoping = false) {
|
|
1802
1994
|
const router = Router();
|
|
1803
|
-
const { callLogs, timeline } = services;
|
|
1995
|
+
const { callLogs, lifecycle, timeline } = services;
|
|
1804
1996
|
router.get("/follow-ups", async (req, res) => {
|
|
1805
1997
|
try {
|
|
1806
|
-
const
|
|
1998
|
+
const scopedAgentId = getScopedAgentId(req, enableAgentScoping);
|
|
1999
|
+
const agentId = scopedAgentId ?? req.query["agentId"];
|
|
1807
2000
|
const dateRange = {
|
|
1808
2001
|
from: req.query["from"],
|
|
1809
2002
|
to: req.query["to"]
|
|
1810
2003
|
};
|
|
1811
|
-
const result = await
|
|
2004
|
+
const result = await lifecycle.getFollowUpsDue(agentId, dateRange);
|
|
1812
2005
|
sendSuccess(res, result);
|
|
1813
2006
|
} catch (error) {
|
|
1814
2007
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -1819,7 +2012,7 @@ function createCallLogRoutes(services, logger) {
|
|
|
1819
2012
|
router.put("/-/bulk/stage", async (req, res) => {
|
|
1820
2013
|
try {
|
|
1821
2014
|
const { callLogIds, newStageId, agentId } = req.body;
|
|
1822
|
-
const result = await
|
|
2015
|
+
const result = await lifecycle.bulkChangeStage(callLogIds, newStageId, agentId);
|
|
1823
2016
|
sendSuccess(res, result);
|
|
1824
2017
|
} catch (error) {
|
|
1825
2018
|
if (error instanceof AlxCallLogError) {
|
|
@@ -1837,7 +2030,12 @@ function createCallLogRoutes(services, logger) {
|
|
|
1837
2030
|
const filter = {};
|
|
1838
2031
|
if (query["pipelineId"]) filter["pipelineId"] = query["pipelineId"];
|
|
1839
2032
|
if (query["currentStageId"]) filter["currentStageId"] = query["currentStageId"];
|
|
1840
|
-
|
|
2033
|
+
const scopedAgentId = getScopedAgentId(req, enableAgentScoping);
|
|
2034
|
+
if (scopedAgentId) {
|
|
2035
|
+
filter["agentId"] = scopedAgentId;
|
|
2036
|
+
} else if (query["agentId"]) {
|
|
2037
|
+
filter["agentId"] = query["agentId"];
|
|
2038
|
+
}
|
|
1841
2039
|
if (query["category"]) filter["category"] = query["category"];
|
|
1842
2040
|
if (query["isClosed"] !== void 0) filter["isClosed"] = query["isClosed"] === "true";
|
|
1843
2041
|
if (query["contactExternalId"]) filter["contactExternalId"] = query["contactExternalId"];
|
|
@@ -1846,6 +2044,14 @@ function createCallLogRoutes(services, logger) {
|
|
|
1846
2044
|
if (query["contactEmail"]) filter["contactEmail"] = query["contactEmail"];
|
|
1847
2045
|
if (query["priority"]) filter["priority"] = query["priority"];
|
|
1848
2046
|
if (query["direction"]) filter["direction"] = query["direction"];
|
|
2047
|
+
const channel = query["channel"];
|
|
2048
|
+
const outcome = query["outcome"];
|
|
2049
|
+
const isFollowUp = query["isFollowUp"] !== void 0 ? query["isFollowUp"] === "true" : void 0;
|
|
2050
|
+
const includeDeleted = query["includeDeleted"] === "true";
|
|
2051
|
+
if (channel) filter["channel"] = channel;
|
|
2052
|
+
if (outcome) filter["outcome"] = outcome;
|
|
2053
|
+
if (isFollowUp !== void 0) filter["isFollowUp"] = isFollowUp;
|
|
2054
|
+
if (includeDeleted) filter["includeDeleted"] = true;
|
|
1849
2055
|
if (query["page"]) filter["page"] = parseInt(query["page"], 10);
|
|
1850
2056
|
if (query["limit"]) filter["limit"] = parseInt(query["limit"], 10);
|
|
1851
2057
|
if (query["from"] || query["to"]) {
|
|
@@ -1901,10 +2107,26 @@ function createCallLogRoutes(services, logger) {
|
|
|
1901
2107
|
sendError(res, message, 500);
|
|
1902
2108
|
}
|
|
1903
2109
|
});
|
|
2110
|
+
router.delete("/:id", async (req, res) => {
|
|
2111
|
+
try {
|
|
2112
|
+
const agentId = req.user?.adminUserId;
|
|
2113
|
+
const agentName = req.user?.displayName;
|
|
2114
|
+
const result = await lifecycle.softDelete(req.params["id"], agentId, agentName);
|
|
2115
|
+
sendSuccess(res, result);
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
if (error instanceof AlxCallLogError) {
|
|
2118
|
+
sendError(res, error.message, 404);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2122
|
+
logger.error("Failed to soft delete call log", { id: req.params["id"], error: message });
|
|
2123
|
+
sendError(res, message, 500);
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
1904
2126
|
router.put("/:id/stage", async (req, res) => {
|
|
1905
2127
|
try {
|
|
1906
2128
|
const { newStageId, agentId } = req.body;
|
|
1907
|
-
const result = await
|
|
2129
|
+
const result = await lifecycle.changeStage(req.params["id"], newStageId, agentId);
|
|
1908
2130
|
sendSuccess(res, result);
|
|
1909
2131
|
} catch (error) {
|
|
1910
2132
|
if (error instanceof AlxCallLogError) {
|
|
@@ -1919,7 +2141,7 @@ function createCallLogRoutes(services, logger) {
|
|
|
1919
2141
|
router.put("/:id/assign", async (req, res) => {
|
|
1920
2142
|
try {
|
|
1921
2143
|
const { agentId, assignedBy } = req.body;
|
|
1922
|
-
const result = await
|
|
2144
|
+
const result = await lifecycle.assign(req.params["id"], agentId, assignedBy);
|
|
1923
2145
|
sendSuccess(res, result);
|
|
1924
2146
|
} catch (error) {
|
|
1925
2147
|
if (error instanceof AlxCallLogError) {
|
|
@@ -1934,7 +2156,7 @@ function createCallLogRoutes(services, logger) {
|
|
|
1934
2156
|
router.put("/:id/close", async (req, res) => {
|
|
1935
2157
|
try {
|
|
1936
2158
|
const { agentId } = req.body;
|
|
1937
|
-
const result = await
|
|
2159
|
+
const result = await lifecycle.close(req.params["id"], agentId);
|
|
1938
2160
|
sendSuccess(res, result);
|
|
1939
2161
|
} catch (error) {
|
|
1940
2162
|
if (error instanceof AlxCallLogError) {
|
|
@@ -1949,7 +2171,7 @@ function createCallLogRoutes(services, logger) {
|
|
|
1949
2171
|
router.put("/:id/reopen", async (req, res) => {
|
|
1950
2172
|
try {
|
|
1951
2173
|
const { agentId } = req.body;
|
|
1952
|
-
const result = await
|
|
2174
|
+
const result = await lifecycle.reopen(req.params["id"], agentId);
|
|
1953
2175
|
sendSuccess(res, result);
|
|
1954
2176
|
} catch (error) {
|
|
1955
2177
|
if (error instanceof AlxCallLogError) {
|
|
@@ -2029,8 +2251,15 @@ function createContactRoutes(services, logger) {
|
|
|
2029
2251
|
});
|
|
2030
2252
|
return router;
|
|
2031
2253
|
}
|
|
2032
|
-
function createAnalyticsRoutes(analytics, logger) {
|
|
2254
|
+
function createAnalyticsRoutes(analytics, pipelineAnalytics, logger, enableAgentScoping = false) {
|
|
2033
2255
|
const router = Router();
|
|
2256
|
+
function getScopedAgentId2(req) {
|
|
2257
|
+
if (!enableAgentScoping) return void 0;
|
|
2258
|
+
const user = req.user;
|
|
2259
|
+
if (!user) return void 0;
|
|
2260
|
+
if (user.role === "owner") return void 0;
|
|
2261
|
+
return user.adminUserId;
|
|
2262
|
+
}
|
|
2034
2263
|
function parseDateRange(query) {
|
|
2035
2264
|
return {
|
|
2036
2265
|
from: query["from"],
|
|
@@ -2049,6 +2278,11 @@ function createAnalyticsRoutes(analytics, logger) {
|
|
|
2049
2278
|
});
|
|
2050
2279
|
router.get("/agent/:agentId", async (req, res) => {
|
|
2051
2280
|
try {
|
|
2281
|
+
const scopedAgentId = getScopedAgentId2(req);
|
|
2282
|
+
if (scopedAgentId && scopedAgentId !== req.params["agentId"]) {
|
|
2283
|
+
sendError(res, "Forbidden: you may only view your own agent stats", 403);
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2052
2286
|
const dateRange = parseDateRange(req.query);
|
|
2053
2287
|
const result = await analytics.getAgentStats(req.params["agentId"], dateRange);
|
|
2054
2288
|
sendSuccess(res, result);
|
|
@@ -2076,7 +2310,7 @@ function createAnalyticsRoutes(analytics, logger) {
|
|
|
2076
2310
|
router.get("/pipeline/:id", async (req, res) => {
|
|
2077
2311
|
try {
|
|
2078
2312
|
const dateRange = parseDateRange(req.query);
|
|
2079
|
-
const result = await
|
|
2313
|
+
const result = await pipelineAnalytics.getPipelineStats(req.params["id"], dateRange);
|
|
2080
2314
|
sendSuccess(res, result);
|
|
2081
2315
|
} catch (error) {
|
|
2082
2316
|
if (error instanceof AlxCallLogError) {
|
|
@@ -2091,7 +2325,7 @@ function createAnalyticsRoutes(analytics, logger) {
|
|
|
2091
2325
|
router.get("/pipeline/:id/funnel", async (req, res) => {
|
|
2092
2326
|
try {
|
|
2093
2327
|
const dateRange = parseDateRange(req.query);
|
|
2094
|
-
const result = await
|
|
2328
|
+
const result = await pipelineAnalytics.getPipelineFunnel(req.params["id"], dateRange);
|
|
2095
2329
|
sendSuccess(res, result);
|
|
2096
2330
|
} catch (error) {
|
|
2097
2331
|
if (error instanceof AlxCallLogError) {
|
|
@@ -2103,6 +2337,39 @@ function createAnalyticsRoutes(analytics, logger) {
|
|
|
2103
2337
|
sendError(res, message, 500);
|
|
2104
2338
|
}
|
|
2105
2339
|
});
|
|
2340
|
+
router.get("/channel-distribution", async (req, res) => {
|
|
2341
|
+
try {
|
|
2342
|
+
const dateRange = parseDateRange(req.query);
|
|
2343
|
+
const result = await pipelineAnalytics.getChannelDistribution(dateRange);
|
|
2344
|
+
sendSuccess(res, result);
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2347
|
+
logger.error("Failed to get channel distribution", { error: message });
|
|
2348
|
+
sendError(res, message, 500);
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
router.get("/outcome-distribution", async (req, res) => {
|
|
2352
|
+
try {
|
|
2353
|
+
const dateRange = parseDateRange(req.query);
|
|
2354
|
+
const result = await pipelineAnalytics.getOutcomeDistribution(dateRange);
|
|
2355
|
+
sendSuccess(res, result);
|
|
2356
|
+
} catch (error) {
|
|
2357
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2358
|
+
logger.error("Failed to get outcome distribution", { error: message });
|
|
2359
|
+
sendError(res, message, 500);
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
router.get("/follow-up-stats", async (req, res) => {
|
|
2363
|
+
try {
|
|
2364
|
+
const dateRange = parseDateRange(req.query);
|
|
2365
|
+
const result = await pipelineAnalytics.getFollowUpStats(dateRange);
|
|
2366
|
+
sendSuccess(res, result);
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2369
|
+
logger.error("Failed to get follow-up stats", { error: message });
|
|
2370
|
+
sendError(res, message, 500);
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2106
2373
|
router.get("/team", async (req, res) => {
|
|
2107
2374
|
try {
|
|
2108
2375
|
const { teamId } = req.query;
|
|
@@ -2117,8 +2384,9 @@ function createAnalyticsRoutes(analytics, logger) {
|
|
|
2117
2384
|
});
|
|
2118
2385
|
router.get("/daily", async (req, res) => {
|
|
2119
2386
|
try {
|
|
2387
|
+
const scopedAgentId = getScopedAgentId2(req);
|
|
2120
2388
|
const dateRange = parseDateRange(req.query);
|
|
2121
|
-
const result = await analytics.getDailyReport(dateRange);
|
|
2389
|
+
const result = await analytics.getDailyReport(dateRange, scopedAgentId);
|
|
2122
2390
|
sendSuccess(res, result);
|
|
2123
2391
|
} catch (error) {
|
|
2124
2392
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -2243,7 +2511,7 @@ function createSettingsRoutes(settings, exportSvc, logger) {
|
|
|
2243
2511
|
// src/routes/index.ts
|
|
2244
2512
|
function createRoutes(services, options) {
|
|
2245
2513
|
const router = Router();
|
|
2246
|
-
const { logger, authenticateRequest } = options;
|
|
2514
|
+
const { logger, authenticateRequest, enableAgentScoping = false } = options;
|
|
2247
2515
|
let authMiddleware;
|
|
2248
2516
|
if (authenticateRequest) {
|
|
2249
2517
|
authMiddleware = async (req, res, next) => {
|
|
@@ -2263,9 +2531,9 @@ function createRoutes(services, options) {
|
|
|
2263
2531
|
}
|
|
2264
2532
|
const protectedRouter = Router();
|
|
2265
2533
|
protectedRouter.use("/pipelines", createPipelineRoutes(services.pipelines, logger));
|
|
2266
|
-
protectedRouter.use("/calls", createCallLogRoutes({ callLogs: services.callLogs, timeline: services.timeline }, logger));
|
|
2534
|
+
protectedRouter.use("/calls", createCallLogRoutes({ callLogs: services.callLogs, lifecycle: services.lifecycle, timeline: services.timeline }, logger, enableAgentScoping));
|
|
2267
2535
|
protectedRouter.use("/contacts", createContactRoutes({ callLogs: services.callLogs, timeline: services.timeline }, logger));
|
|
2268
|
-
protectedRouter.use("/analytics", createAnalyticsRoutes(services.analytics, logger));
|
|
2536
|
+
protectedRouter.use("/analytics", createAnalyticsRoutes(services.analytics, services.pipelineAnalytics, logger, enableAgentScoping));
|
|
2269
2537
|
protectedRouter.use("/", createSettingsRoutes(services.settings, services.export, logger));
|
|
2270
2538
|
if (authMiddleware) {
|
|
2271
2539
|
router.use(authMiddleware, protectedRouter);
|
|
@@ -2320,7 +2588,7 @@ var FollowUpWorker = class {
|
|
|
2320
2588
|
$set: { followUpNotifiedAt: /* @__PURE__ */ new Date() },
|
|
2321
2589
|
$push: {
|
|
2322
2590
|
timeline: {
|
|
2323
|
-
entryId:
|
|
2591
|
+
entryId: crypto6.randomUUID(),
|
|
2324
2592
|
type: TimelineEntryType.FollowUpCompleted,
|
|
2325
2593
|
content: SYSTEM_TIMELINE.FollowUpCompleted,
|
|
2326
2594
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -2373,7 +2641,8 @@ var CallLogEngineConfigSchema = z.object({
|
|
|
2373
2641
|
}).optional(),
|
|
2374
2642
|
options: z.object({
|
|
2375
2643
|
maxTimelineEntries: z.number().int().positive().optional(),
|
|
2376
|
-
followUpCheckIntervalMs: z.number().int().positive().optional()
|
|
2644
|
+
followUpCheckIntervalMs: z.number().int().positive().optional(),
|
|
2645
|
+
enableAgentScoping: z.boolean().optional()
|
|
2377
2646
|
}).optional()
|
|
2378
2647
|
});
|
|
2379
2648
|
function createCallLogEngine(config) {
|
|
@@ -2403,14 +2672,30 @@ function createCallLogEngine(config) {
|
|
|
2403
2672
|
config.hooks ?? {},
|
|
2404
2673
|
resolvedOptions
|
|
2405
2674
|
);
|
|
2675
|
+
const callLogLifecycleService = new CallLogLifecycleService(
|
|
2676
|
+
CallLog,
|
|
2677
|
+
Pipeline,
|
|
2678
|
+
timelineService,
|
|
2679
|
+
logger,
|
|
2680
|
+
config.hooks ?? {},
|
|
2681
|
+
resolvedOptions
|
|
2682
|
+
);
|
|
2406
2683
|
const analyticsService = new AnalyticsService(CallLog, Pipeline, logger, config.agents?.resolveAgent);
|
|
2407
|
-
const
|
|
2684
|
+
const pipelineAnalyticsService = new PipelineAnalyticsService(
|
|
2685
|
+
CallLog,
|
|
2686
|
+
Pipeline,
|
|
2687
|
+
logger,
|
|
2688
|
+
config.agents?.resolveAgent
|
|
2689
|
+
);
|
|
2690
|
+
const exportService = new ExportService(CallLog, pipelineAnalyticsService, logger);
|
|
2408
2691
|
const routes = createRoutes(
|
|
2409
2692
|
{
|
|
2410
2693
|
pipelines: pipelineService,
|
|
2411
2694
|
callLogs: callLogService,
|
|
2695
|
+
lifecycle: callLogLifecycleService,
|
|
2412
2696
|
timeline: timelineService,
|
|
2413
2697
|
analytics: analyticsService,
|
|
2698
|
+
pipelineAnalytics: pipelineAnalyticsService,
|
|
2414
2699
|
settings: settingsService,
|
|
2415
2700
|
export: exportService
|
|
2416
2701
|
},
|
|
@@ -2422,7 +2707,8 @@ function createCallLogEngine(config) {
|
|
|
2422
2707
|
if (!token) return null;
|
|
2423
2708
|
return config.adapters.authenticateAgent(token);
|
|
2424
2709
|
} : void 0,
|
|
2425
|
-
logger
|
|
2710
|
+
logger,
|
|
2711
|
+
enableAgentScoping: resolvedOptions.enableAgentScoping
|
|
2426
2712
|
}
|
|
2427
2713
|
);
|
|
2428
2714
|
const followUpWorker = new FollowUpWorker({
|
|
@@ -2439,8 +2725,10 @@ function createCallLogEngine(config) {
|
|
|
2439
2725
|
return {
|
|
2440
2726
|
pipelines: pipelineService,
|
|
2441
2727
|
callLogs: callLogService,
|
|
2728
|
+
lifecycle: callLogLifecycleService,
|
|
2442
2729
|
timeline: timelineService,
|
|
2443
2730
|
analytics: analyticsService,
|
|
2731
|
+
pipelineAnalytics: pipelineAnalyticsService,
|
|
2444
2732
|
settings: settingsService,
|
|
2445
2733
|
export: exportService,
|
|
2446
2734
|
routes,
|
|
@@ -2449,6 +2737,6 @@ function createCallLogEngine(config) {
|
|
|
2449
2737
|
};
|
|
2450
2738
|
}
|
|
2451
2739
|
|
|
2452
|
-
export { AGENT_CALL_DEFAULTS, AgentCapacityError, AlxCallLogError, AnalyticsService, AuthFailedError, CALL_LOG_DEFAULTS, CallLogClosedError, CallLogNotFoundError, CallLogSchema, CallLogService, CallLogSettingsSchema, ContactNotFoundError, ContactRefSchema, ERROR_CODE, ERROR_MESSAGE, ExportService, FollowUpWorker, InvalidConfigError, InvalidPipelineError, PIPELINE_DEFAULTS, PipelineNotFoundError, PipelineSchema, PipelineService, PipelineStageSchema, SYSTEM_TIMELINE, SYSTEM_TIMELINE_FN, SettingsService, StageChangeSchema, StageInUseError, StageNotFoundError, TimelineEntrySchema, TimelineService, createCallLogEngine, createCallLogModel, createCallLogSettingsModel, createPipelineModel, createRoutes, validatePipelineStages };
|
|
2740
|
+
export { AGENT_CALL_DEFAULTS, AgentCapacityError, AlxCallLogError, AnalyticsService, AuthFailedError, CALL_LOG_DEFAULTS, CallLogClosedError, CallLogLifecycleService, CallLogNotFoundError, CallLogSchema, CallLogService, CallLogSettingsSchema, ContactNotFoundError, ContactRefSchema, ERROR_CODE, ERROR_MESSAGE, ExportService, FollowUpWorker, InvalidConfigError, InvalidPipelineError, PIPELINE_DEFAULTS, PipelineAnalyticsService, PipelineNotFoundError, PipelineSchema, PipelineService, PipelineStageSchema, SYSTEM_TIMELINE, SYSTEM_TIMELINE_FN, SettingsService, StageChangeSchema, StageInUseError, StageNotFoundError, TimelineEntrySchema, TimelineService, createCallLogEngine, createCallLogModel, createCallLogSettingsModel, createPipelineModel, createRoutes, validatePipelineStages };
|
|
2453
2741
|
//# sourceMappingURL=index.mjs.map
|
|
2454
2742
|
//# sourceMappingURL=index.mjs.map
|