@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/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 crypto5 from 'crypto';
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: () => crypto5.randomUUID() },
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: () => crypto5.randomUUID() },
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: () => crypto5.randomUUID() },
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: () => crypto5.randomUUID() },
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: crypto5.randomUUID()
475
+ stageId: crypto6.randomUUID()
464
476
  }));
465
477
  validatePipelineStages(stages);
466
- const pipelineId = crypto5.randomUUID();
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: crypto5.randomUUID()
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: crypto5.randomUUID(),
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: crypto5.randomUUID(),
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 = crypto5.randomUUID();
740
+ const callLogId = crypto6.randomUUID();
729
741
  const initialEntry = {
730
- entryId: crypto5.randomUUID(),
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
- timeline: [initialEntry],
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: crypto5.randomUUID(),
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: crypto5.randomUUID(),
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: crypto5.randomUUID(),
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: crypto5.randomUUID(),
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: crypto5.randomUUID(),
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 = this.buildDateMatch(dateRange);
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 getPipelineStats(pipelineId, dateRange) {
1212
- const pipeline = await this.Pipeline.findOne({ pipelineId, isDeleted: false });
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
- pipelineId,
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 = this.buildDateMatch(dateRange);
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 = this.buildDateMatch(dateRange);
1493
- const [summaryAgg, tagAgg, categoryAgg, peakHoursAgg, followUpAgg] = await Promise.all([
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: [{ $ne: ["$nextFollowUpDate", null] }, 1, 0] }
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
- { $ne: ["$nextFollowUpDate", null] },
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, analytics, logger) {
1761
+ constructor(CallLog, pipelineAnalytics, logger) {
1577
1762
  this.CallLog = CallLog;
1578
- this.analytics = analytics;
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.analytics.getPipelineStats(pipelineId, dateRange);
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 createCallLogRoutes(services, logger) {
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 { agentId } = req.query;
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 callLogs.getFollowUpsDue(agentId, dateRange);
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 callLogs.bulkChangeStage(callLogIds, newStageId, agentId);
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
- if (query["agentId"]) filter["agentId"] = query["agentId"];
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 callLogs.changeStage(req.params["id"], newStageId, agentId);
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 callLogs.assign(req.params["id"], agentId, assignedBy);
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 callLogs.close(req.params["id"], agentId);
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 callLogs.reopen(req.params["id"], agentId);
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 analytics.getPipelineStats(req.params["id"], dateRange);
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 analytics.getPipelineFunnel(req.params["id"], dateRange);
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: crypto5.randomUUID(),
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 exportService = new ExportService(CallLog, analyticsService, logger);
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