@talkpilot/core-db 1.3.19 → 1.3.21

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.
Files changed (202) hide show
  1. package/.claude/settings.local.json +13 -0
  2. package/.cursor/rules/development.mdc +65 -65
  3. package/DEVELOPMENT.md +141 -141
  4. package/README.md +500 -465
  5. package/dist/talkpilot/calls/calls.constants.d.ts +4 -0
  6. package/dist/talkpilot/calls/calls.constants.d.ts.map +1 -1
  7. package/dist/talkpilot/calls/calls.constants.js +11 -1
  8. package/dist/talkpilot/calls/calls.constants.js.map +1 -1
  9. package/dist/talkpilot/calls/calls.getters.d.ts +3 -0
  10. package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
  11. package/dist/talkpilot/calls/calls.getters.js +12 -1
  12. package/dist/talkpilot/calls/calls.getters.js.map +1 -1
  13. package/dist/talkpilot/calls/calls.types.d.ts +9 -0
  14. package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
  15. package/dist/talkpilot/leads/leads.setter.d.ts +4 -0
  16. package/dist/talkpilot/leads/leads.setter.d.ts.map +1 -0
  17. package/dist/talkpilot/leads/leads.setter.js +16 -0
  18. package/dist/talkpilot/leads/leads.setter.js.map +1 -0
  19. package/jest.config.js +20 -20
  20. package/package.json +46 -46
  21. package/src/__tests__/setup.ts +20 -20
  22. package/src/connection.ts +54 -54
  23. package/src/index.ts +27 -27
  24. package/src/municipal/__tests__/validation.spec.ts +62 -62
  25. package/src/municipal/cities/cities.getters.ts +50 -50
  26. package/src/municipal/cities/cities.types.ts +11 -11
  27. package/src/municipal/cities/index.ts +2 -2
  28. package/src/municipal/departmentsSubjects/departmentsSubjects.getters.ts +282 -282
  29. package/src/municipal/departmentsSubjects/departmentsSubjects.types.ts +72 -72
  30. package/src/municipal/departmentsSubjects/index.ts +9 -9
  31. package/src/municipal/index.ts +22 -22
  32. package/src/municipal/mongodb-client.ts +61 -61
  33. package/src/municipal/muniIssues/__tests__/muniIssues.setters.spec.ts +94 -94
  34. package/src/municipal/muniIssues/__tests__/muniIssues.setup.spec.ts +92 -92
  35. package/src/municipal/muniIssues/index.ts +17 -17
  36. package/src/municipal/muniIssues/muniIssues.constants.ts +18 -18
  37. package/src/municipal/muniIssues/muniIssues.getters.ts +24 -24
  38. package/src/municipal/muniIssues/muniIssues.schema.ts +66 -66
  39. package/src/municipal/muniIssues/muniIssues.setters.ts +24 -24
  40. package/src/municipal/muniIssues/muniIssues.setup.ts +65 -65
  41. package/src/municipal/muniIssues/muniIssues.types.ts +37 -37
  42. package/src/municipal/streets/index.ts +2 -2
  43. package/src/municipal/streets/streets.getters.ts +125 -125
  44. package/src/municipal/streets/streets.types.ts +18 -18
  45. package/src/municipal/systemInstructions/__tests__/getters.spec.ts +113 -113
  46. package/src/municipal/systemInstructions/__tests__/setters.spec.ts +274 -274
  47. package/src/municipal/systemInstructions/index.ts +7 -7
  48. package/src/municipal/systemInstructions/instructions.getters.ts +57 -57
  49. package/src/municipal/systemInstructions/instructions.setters.ts +119 -119
  50. package/src/municipal/systemInstructions/instructions.types.ts +30 -30
  51. package/src/municipal/tickets/__tests__/tickets.getters.spec.ts +30 -30
  52. package/src/municipal/tickets/__tests__/tickets.statistics.spec.ts +99 -99
  53. package/src/municipal/tickets/index.ts +9 -9
  54. package/src/municipal/tickets/tickets.constants.ts +8 -8
  55. package/src/municipal/tickets/tickets.getters.ts +121 -121
  56. package/src/municipal/tickets/tickets.statistics.aggregation.ts +110 -110
  57. package/src/municipal/tickets/tickets.statistics.getters.ts +145 -145
  58. package/src/municipal/tickets/tickets.types.ts +54 -54
  59. package/src/municipal/utils/types.ts +11 -11
  60. package/src/talkpilot/__tests__/db.spec.ts +38 -38
  61. package/src/talkpilot/__tests__/mongodb-client.spec.ts +18 -18
  62. package/src/talkpilot/__tests__/validation.spec.ts +68 -68
  63. package/src/talkpilot/agents/__tests__/agents.getters.spec.ts +29 -29
  64. package/src/talkpilot/agents/agents.getters.ts +34 -34
  65. package/src/talkpilot/agents/agents.types.ts +14 -14
  66. package/src/talkpilot/agents/index.ts +2 -2
  67. package/src/talkpilot/backgroundToolResults/__tests__/backgroundToolResults.getters.spec.ts +147 -147
  68. package/src/talkpilot/backgroundToolResults/backgroundToolResults.getters.ts +65 -65
  69. package/src/talkpilot/backgroundToolResults/backgroundToolResults.types.ts +23 -23
  70. package/src/talkpilot/backgroundToolResults/index.ts +2 -2
  71. package/src/talkpilot/calls/__tests__/callStats.utils.spec.ts +128 -128
  72. package/src/talkpilot/calls/__tests__/calls.dashboard.spec.ts +229 -229
  73. package/src/talkpilot/calls/__tests__/calls.spec.ts +471 -283
  74. package/src/talkpilot/calls/__tests__/calls.statistics.spec.ts +483 -483
  75. package/src/talkpilot/calls/calls.constants.ts +35 -23
  76. package/src/talkpilot/calls/calls.getters.ts +277 -258
  77. package/src/talkpilot/calls/calls.statistics.getters.ts +668 -668
  78. package/src/talkpilot/calls/calls.statistics.types.ts +48 -48
  79. package/src/talkpilot/calls/calls.types.ts +141 -130
  80. package/src/talkpilot/calls/dashboard/calls.dashboard.ts +295 -295
  81. package/src/talkpilot/calls/dashboard/calls.dashboard.types.ts +57 -57
  82. package/src/talkpilot/calls/index.ts +6 -6
  83. package/src/talkpilot/clientAudioBuffers/__tests__/clientAudioBuffer.getters.spec.ts +160 -160
  84. package/src/talkpilot/clientAudioBuffers/clientAudioBuffer.getters.ts +117 -117
  85. package/src/talkpilot/clientAudioBuffers/clientsAudioBuffers.types.ts +25 -25
  86. package/src/talkpilot/clientAudioBuffers/index.ts +2 -2
  87. package/src/talkpilot/clients/clients.getters.ts +16 -16
  88. package/src/talkpilot/clients/clients.types.ts +14 -14
  89. package/src/talkpilot/clients/index.ts +2 -2
  90. package/src/talkpilot/clientsConfig/__tests__/clientsConfig.getters.spec.ts +105 -105
  91. package/src/talkpilot/clientsConfig/__tests__/clientsConfig.spec.ts +186 -186
  92. package/src/talkpilot/clientsConfig/clientsConfig.getters.ts +79 -79
  93. package/src/talkpilot/clientsConfig/clientsConfig.types.ts +127 -127
  94. package/src/talkpilot/clientsConfig/index.ts +2 -2
  95. package/src/talkpilot/contextNotes/__tests__/contextNotes.getters.spec.ts +217 -217
  96. package/src/talkpilot/contextNotes/contextNotes.getters.ts +85 -85
  97. package/src/talkpilot/contextNotes/contextNotes.types.ts +20 -20
  98. package/src/talkpilot/contextNotes/index.ts +2 -2
  99. package/src/talkpilot/flows/__tests__/flows.schema.spec.ts +99 -99
  100. package/src/talkpilot/flows/flows.getter.ts +14 -14
  101. package/src/talkpilot/flows/flows.schema.ts +155 -155
  102. package/src/talkpilot/flows/flows.types.ts +189 -189
  103. package/src/talkpilot/flows/index.ts +2 -2
  104. package/src/talkpilot/groups/__tests__/groups.spec.ts +90 -90
  105. package/src/talkpilot/groups/__tests__/phone.utils.spec.ts +32 -32
  106. package/src/talkpilot/groups/groups.getters.ts +30 -30
  107. package/src/talkpilot/groups/groups.types.ts +29 -29
  108. package/src/talkpilot/groups/index.ts +3 -3
  109. package/src/talkpilot/groups/phone.utils.ts +46 -46
  110. package/src/talkpilot/index.ts +31 -31
  111. package/src/talkpilot/leads/index.ts +2 -2
  112. package/src/talkpilot/leads/leads.getter.ts +6 -6
  113. package/src/talkpilot/leads/leads.schema.ts +33 -33
  114. package/src/talkpilot/leads/leads.types.ts +20 -20
  115. package/src/talkpilot/mongodb-client.ts +78 -78
  116. package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +252 -252
  117. package/src/talkpilot/phone_numbers/index.ts +2 -2
  118. package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +171 -171
  119. package/src/talkpilot/phone_numbers/phone_numbers.schema.ts +17 -17
  120. package/src/talkpilot/phone_numbers/phone_numbers.types.ts +30 -30
  121. package/src/talkpilot/plans/__tests__/plans.spec.ts +70 -70
  122. package/src/talkpilot/plans/index.ts +2 -2
  123. package/src/talkpilot/plans/plans.getters.ts +132 -132
  124. package/src/talkpilot/plans/plans.types.ts +89 -89
  125. package/src/talkpilot/products/__tests__/products.getters.spec.ts +44 -44
  126. package/src/talkpilot/products/index.ts +2 -2
  127. package/src/talkpilot/products/products.getters.ts +12 -12
  128. package/src/talkpilot/products/products.types.ts +9 -9
  129. package/src/talkpilot/results/index.ts +7 -7
  130. package/src/talkpilot/results/results.getter.ts +39 -39
  131. package/src/talkpilot/results/results.schema.ts +25 -25
  132. package/src/talkpilot/results/results.types.ts +34 -34
  133. package/src/talkpilot/retry_analyze/__tests__/retryAnalyze.getters.spec.ts +156 -156
  134. package/src/talkpilot/retry_analyze/index.ts +2 -2
  135. package/src/talkpilot/retry_analyze/retryAnalyze.getters.ts +84 -84
  136. package/src/talkpilot/retry_analyze/retryAnalyze.types.ts +13 -13
  137. package/src/talkpilot/sessions/__tests__/sessions.spec.ts +147 -147
  138. package/src/talkpilot/sessions/index.ts +2 -2
  139. package/src/talkpilot/sessions/sessions.getter.ts +92 -92
  140. package/src/talkpilot/sessions/sessions.schema.ts +34 -34
  141. package/src/talkpilot/sessions/sessions.types.ts +30 -30
  142. package/src/talkpilot/subscriptions/__tests__/subscriptions.getters.utils.spec.ts +45 -45
  143. package/src/talkpilot/subscriptions/index.ts +3 -3
  144. package/src/talkpilot/subscriptions/subscriptions.getters.ts +146 -146
  145. package/src/talkpilot/subscriptions/subscriptions.getters.utils.ts +33 -33
  146. package/src/talkpilot/subscriptions/subscriptions.types.ts +66 -66
  147. package/src/talkpilot/utils/__tests__/query.utils.spec.ts +49 -49
  148. package/src/talkpilot/utils/query.utils.ts +21 -21
  149. package/src/test-utils/db-utils.ts +26 -26
  150. package/src/test-utils/factories/index.ts +16 -16
  151. package/src/test-utils/factories/municipal/cities.ts +16 -16
  152. package/src/test-utils/factories/municipal/departmentsSubjects.ts +37 -37
  153. package/src/test-utils/factories/municipal/muniIssues.ts +32 -32
  154. package/src/test-utils/factories/municipal/streets.ts +22 -22
  155. package/src/test-utils/factories/municipal/tickets.ts +39 -39
  156. package/src/test-utils/factories/talkpilot/agents.ts +19 -19
  157. package/src/test-utils/factories/talkpilot/calls.ts +37 -37
  158. package/src/test-utils/factories/talkpilot/clientAudioBuffers.ts +20 -20
  159. package/src/test-utils/factories/talkpilot/clientsConfig.ts +18 -18
  160. package/src/test-utils/factories/talkpilot/contextNotes.ts +40 -40
  161. package/src/test-utils/factories/talkpilot/flows.ts +33 -33
  162. package/src/test-utils/factories/talkpilot/groups.ts +33 -33
  163. package/src/test-utils/factories/talkpilot/phone_numbers.ts +22 -22
  164. package/src/test-utils/factories/talkpilot/sessions.ts +35 -35
  165. package/src/test-utils/factories/websitalk/scans.ts +24 -24
  166. package/src/test-utils/factories/websitalk/websiteUrls.ts +17 -17
  167. package/src/utils/date.utils.ts +126 -126
  168. package/src/utils/shared.types.ts +4 -4
  169. package/src/utils/validation.ts +23 -23
  170. package/src/websitalk/index.ts +16 -16
  171. package/src/websitalk/mongodb-client.ts +61 -61
  172. package/src/websitalk/scans/__tests__/scans.spec.ts +313 -313
  173. package/src/websitalk/scans/index.ts +4 -4
  174. package/src/websitalk/scans/scans.constants.ts +15 -15
  175. package/src/websitalk/scans/scans.getters.ts +160 -160
  176. package/src/websitalk/scans/scans.types.ts +45 -45
  177. package/src/websitalk/scans/scans.utils.ts +5 -5
  178. package/src/websitalk/websiteUrls/__tests__/websiteUrls.spec.ts +77 -77
  179. package/src/websitalk/websiteUrls/index.ts +2 -2
  180. package/src/websitalk/websiteUrls/websiteUrls.getters.ts +65 -65
  181. package/src/websitalk/websiteUrls/websiteUrls.types.ts +13 -13
  182. package/tsconfig.json +23 -23
  183. package/dist/talkpilot/flows/flows.setter.d.ts +0 -13
  184. package/dist/talkpilot/flows/flows.setter.d.ts.map +0 -1
  185. package/dist/talkpilot/flows/flows.setter.js +0 -90
  186. package/dist/talkpilot/flows/flows.setter.js.map +0 -1
  187. package/dist/websitalk/websitalk.setup.d.ts +0 -3
  188. package/dist/websitalk/websitalk.setup.d.ts.map +0 -1
  189. package/dist/websitalk/websitalk.setup.js +0 -34
  190. package/dist/websitalk/websitalk.setup.js.map +0 -1
  191. package/dist/websitalk/websiteUrls/websiteUrls.constants.d.ts +0 -2
  192. package/dist/websitalk/websiteUrls/websiteUrls.constants.d.ts.map +0 -1
  193. package/dist/websitalk/websiteUrls/websiteUrls.constants.js +0 -5
  194. package/dist/websitalk/websiteUrls/websiteUrls.constants.js.map +0 -1
  195. package/dist/websitalk/websiteUrls/websiteUrls.context.d.ts +0 -11
  196. package/dist/websitalk/websiteUrls/websiteUrls.context.d.ts.map +0 -1
  197. package/dist/websitalk/websiteUrls/websiteUrls.context.js +0 -35
  198. package/dist/websitalk/websiteUrls/websiteUrls.context.js.map +0 -1
  199. package/dist/websitalk/websiteUrls/websiteUrls.setters.d.ts +0 -7
  200. package/dist/websitalk/websiteUrls/websiteUrls.setters.d.ts.map +0 -1
  201. package/dist/websitalk/websiteUrls/websiteUrls.setters.js +0 -29
  202. package/dist/websitalk/websiteUrls/websiteUrls.setters.js.map +0 -1
@@ -1,295 +1,295 @@
1
- import dayjs from "dayjs";
2
- import { getCallsCollection } from "../calls.getters";
3
- import { IN_CALL_STATUSES } from "../calls.constants";
4
- import { getClientConfig } from "../../clientsConfig";
5
- import utc from "dayjs/plugin/utc";
6
- import timezonePlugin from "dayjs/plugin/timezone";
7
- import isoWeek from "dayjs/plugin/isoWeek";
8
- import quarterOfYear from "dayjs/plugin/quarterOfYear";
9
- import {
10
- DashboardAggregationResult,
11
- DashboardDailyTrendMetric,
12
- DashboardReportQuery,
13
- DashboardReportResponse,
14
- DashboardSummaryMetrics,
15
- DashboardVolumeGranularity,
16
- } from "./calls.dashboard.types";
17
-
18
- export type {
19
- DashboardVolumeGranularity,
20
- DashboardReportQuery,
21
- DashboardReportResponse,
22
- } from "./calls.dashboard.types";
23
-
24
- const DEFAULT_KPI_DATA = {
25
- completedCount: 0,
26
- inCallCount: 0,
27
- answeredDuration: 0,
28
- };
29
-
30
- dayjs.extend(utc);
31
- dayjs.extend(timezonePlugin);
32
- dayjs.extend(isoWeek);
33
- dayjs.extend(quarterOfYear);
34
-
35
- const BUCKET_UNIT: Record<DashboardVolumeGranularity, string> = {
36
- hour: "hour",
37
- day: "day",
38
- week: "isoWeek",
39
- month: "month",
40
- quarter: "quarter",
41
- year: "year",
42
- };
43
-
44
- function formatBucketKey(
45
- bucketStart: dayjs.Dayjs,
46
- granularity: DashboardVolumeGranularity,
47
- ): string {
48
- switch (granularity) {
49
- case "hour":
50
- return bucketStart.format("YYYY-MM-DDTHH:00");
51
- case "day":
52
- case "week":
53
- return bucketStart.format("YYYY-MM-DD");
54
- case "month":
55
- return bucketStart.format("YYYY-MM");
56
- case "quarter":
57
- return `${bucketStart.format("YYYY")}-Q${bucketStart.quarter()}`;
58
- case "year":
59
- return bucketStart.format("YYYY");
60
- }
61
- }
62
-
63
- function generateBucketKeys(
64
- startDateObj: Date,
65
- endDateObj: Date,
66
- granularity: DashboardVolumeGranularity,
67
- timezone: string,
68
- ): string[] {
69
- const unitName = BUCKET_UNIT[granularity];
70
- const startOfUnit = unitName as dayjs.OpUnitType;
71
- const stepUnit = (
72
- unitName === "isoWeek" ? "week" : unitName
73
- ) as dayjs.ManipulateType;
74
- const end = dayjs(endDateObj).tz(timezone);
75
- let cursor = dayjs(startDateObj).tz(timezone).startOf(startOfUnit);
76
-
77
- const keys: string[] = [];
78
- while (cursor.isBefore(end) || cursor.isSame(end, startOfUnit)) {
79
- keys.push(formatBucketKey(cursor, granularity));
80
- cursor = cursor.add(1, stepUnit);
81
- }
82
- return keys;
83
- }
84
-
85
- function fillVolumeGaps(
86
- bucketKeys: string[],
87
- volumeDataRaw: { date: string; completed: number }[],
88
- ): DashboardDailyTrendMetric[] {
89
- const completedByKey = new Map(
90
- volumeDataRaw.map((d) => [d.date, d.completed]),
91
- );
92
- return bucketKeys.map((date) => ({
93
- date,
94
- completed: completedByKey.get(date) ?? 0,
95
- }));
96
- }
97
-
98
- function resolveVolumeGranularity(
99
- startDateObj: Date,
100
- endDateObj: Date,
101
- ): DashboardVolumeGranularity {
102
- const spanDays = dayjs(endDateObj).diff(dayjs(startDateObj), "day");
103
-
104
- if (spanDays <= 2) return "hour";
105
- if (spanDays <= 60) return "day";
106
- if (spanDays <= 365) return "week";
107
- if (spanDays <= 730) return "month";
108
- if (spanDays <= 1460) return "quarter";
109
- return "year";
110
- }
111
-
112
- const VOLUME_DATE_FORMATS: Partial<Record<DashboardVolumeGranularity, string>> =
113
- {
114
- hour: "%Y-%m-%dT%H:00",
115
- day: "%Y-%m-%d",
116
- month: "%Y-%m",
117
- year: "%Y",
118
- };
119
-
120
- function buildVolumeBucketIdExpression(
121
- timezone: string,
122
- granularity: DashboardVolumeGranularity,
123
- ) {
124
- if (granularity === "week") {
125
- return {
126
- $dateToString: {
127
- format: "%Y-%m-%d",
128
- date: {
129
- $dateTrunc: {
130
- date: "$createdAt",
131
- unit: "week",
132
- timezone,
133
- startOfWeek: "monday",
134
- },
135
- },
136
- timezone,
137
- },
138
- };
139
- }
140
-
141
- if (granularity === "quarter") {
142
- return {
143
- $concat: [
144
- { $toString: { $year: { date: "$createdAt", timezone } } },
145
- "-Q",
146
- {
147
- $toString: {
148
- $add: [
149
- {
150
- $trunc: {
151
- $divide: [
152
- {
153
- $subtract: [
154
- { $month: { date: "$createdAt", timezone } },
155
- 1,
156
- ],
157
- },
158
- 3,
159
- ],
160
- },
161
- },
162
- 1,
163
- ],
164
- },
165
- },
166
- ],
167
- };
168
- }
169
-
170
- return {
171
- $dateToString: {
172
- format: VOLUME_DATE_FORMATS[granularity],
173
- date: "$createdAt",
174
- timezone,
175
- },
176
- };
177
- }
178
-
179
- function buildKpisPipeline() {
180
- return [
181
- {
182
- $group: {
183
- _id: null,
184
- completedCount: {
185
- $sum: { $cond: [{ $eq: ["$status", "completed"] }, 1, 0] },
186
- },
187
- inCallCount: {
188
- $sum: {
189
- $cond: [{ $in: ["$status", [...IN_CALL_STATUSES]] }, 1, 0],
190
- },
191
- },
192
- answeredDuration: {
193
- $sum: {
194
- $cond: [{ $eq: ["$status", "completed"] }, "$callLength", 0],
195
- },
196
- },
197
- },
198
- },
199
- ];
200
- }
201
-
202
- function buildVolumeDataPipeline(
203
- timezone: string,
204
- granularity: DashboardVolumeGranularity,
205
- ) {
206
- return [
207
- {
208
- $group: {
209
- _id: buildVolumeBucketIdExpression(timezone, granularity),
210
- completed: {
211
- $sum: { $cond: [{ $eq: ["$status", "completed"] }, 1, 0] },
212
- },
213
- },
214
- },
215
- { $sort: { _id: 1 } },
216
- ];
217
- }
218
-
219
- export async function getDashboardStats(
220
- params: DashboardReportQuery,
221
- ): Promise<DashboardReportResponse> {
222
- const {
223
- clientId,
224
- startDate,
225
- endDate,
226
- granularity: requestedGranularity,
227
- } = params;
228
- const clientConfig = await getClientConfig(clientId);
229
- const timezone = clientConfig?.timezone ?? "UTC";
230
-
231
- const startDateObj = dayjs(startDate).tz(timezone).startOf("day").toDate();
232
- const endDateObj = dayjs(endDate).tz(timezone).endOf("day").toDate();
233
-
234
- const granularity =
235
- requestedGranularity ?? resolveVolumeGranularity(startDateObj, endDateObj);
236
-
237
- const pipeline = [
238
- {
239
- $match: {
240
- clientId,
241
- createdAt: { $gte: startDateObj, $lte: endDateObj },
242
- },
243
- },
244
- {
245
- $facet: {
246
- kpis: buildKpisPipeline(),
247
- volumeData: buildVolumeDataPipeline(timezone, granularity),
248
- },
249
- },
250
- ];
251
-
252
- const callsCollection = getCallsCollection();
253
- const [aggregatedResult] = await callsCollection
254
- .aggregate<DashboardAggregationResult>(pipeline)
255
- .toArray();
256
-
257
- const kpiData = aggregatedResult?.kpis?.[0] ?? DEFAULT_KPI_DATA;
258
- const volumeDataRaw = aggregatedResult?.volumeData ?? [];
259
-
260
- const completedCount = kpiData.completedCount;
261
- const inCallCount = kpiData.inCallCount;
262
- const totalCalls = completedCount + inCallCount;
263
- const answeredDuration = kpiData.answeredDuration ?? 0;
264
-
265
- const kpis: DashboardSummaryMetrics = {
266
- totalCalls,
267
- avgDurationSeconds:
268
- completedCount > 0
269
- ? Math.round(answeredDuration / completedCount)
270
- : 0,
271
- timeSavedMinutes: Math.round(answeredDuration / 60),
272
- completedCount,
273
- inCallCount,
274
- };
275
-
276
- const bucketKeys = generateBucketKeys(
277
- startDateObj,
278
- endDateObj,
279
- granularity,
280
- timezone,
281
- );
282
- const volumeData: DashboardDailyTrendMetric[] = fillVolumeGaps(
283
- bucketKeys,
284
- volumeDataRaw.map((d) => ({ date: d._id, completed: d.completed })),
285
- );
286
-
287
- const response: DashboardReportResponse = {
288
- kpis,
289
- charts: {
290
- volumeData,
291
- volumeGranularity: granularity,
292
- },
293
- };
294
- return response;
295
- }
1
+ import dayjs from "dayjs";
2
+ import { getCallsCollection } from "../calls.getters";
3
+ import { IN_CALL_STATUSES } from "../calls.constants";
4
+ import { getClientConfig } from "../../clientsConfig";
5
+ import utc from "dayjs/plugin/utc";
6
+ import timezonePlugin from "dayjs/plugin/timezone";
7
+ import isoWeek from "dayjs/plugin/isoWeek";
8
+ import quarterOfYear from "dayjs/plugin/quarterOfYear";
9
+ import {
10
+ DashboardAggregationResult,
11
+ DashboardDailyTrendMetric,
12
+ DashboardReportQuery,
13
+ DashboardReportResponse,
14
+ DashboardSummaryMetrics,
15
+ DashboardVolumeGranularity,
16
+ } from "./calls.dashboard.types";
17
+
18
+ export type {
19
+ DashboardVolumeGranularity,
20
+ DashboardReportQuery,
21
+ DashboardReportResponse,
22
+ } from "./calls.dashboard.types";
23
+
24
+ const DEFAULT_KPI_DATA = {
25
+ completedCount: 0,
26
+ inCallCount: 0,
27
+ answeredDuration: 0,
28
+ };
29
+
30
+ dayjs.extend(utc);
31
+ dayjs.extend(timezonePlugin);
32
+ dayjs.extend(isoWeek);
33
+ dayjs.extend(quarterOfYear);
34
+
35
+ const BUCKET_UNIT: Record<DashboardVolumeGranularity, string> = {
36
+ hour: "hour",
37
+ day: "day",
38
+ week: "isoWeek",
39
+ month: "month",
40
+ quarter: "quarter",
41
+ year: "year",
42
+ };
43
+
44
+ function formatBucketKey(
45
+ bucketStart: dayjs.Dayjs,
46
+ granularity: DashboardVolumeGranularity,
47
+ ): string {
48
+ switch (granularity) {
49
+ case "hour":
50
+ return bucketStart.format("YYYY-MM-DDTHH:00");
51
+ case "day":
52
+ case "week":
53
+ return bucketStart.format("YYYY-MM-DD");
54
+ case "month":
55
+ return bucketStart.format("YYYY-MM");
56
+ case "quarter":
57
+ return `${bucketStart.format("YYYY")}-Q${bucketStart.quarter()}`;
58
+ case "year":
59
+ return bucketStart.format("YYYY");
60
+ }
61
+ }
62
+
63
+ function generateBucketKeys(
64
+ startDateObj: Date,
65
+ endDateObj: Date,
66
+ granularity: DashboardVolumeGranularity,
67
+ timezone: string,
68
+ ): string[] {
69
+ const unitName = BUCKET_UNIT[granularity];
70
+ const startOfUnit = unitName as dayjs.OpUnitType;
71
+ const stepUnit = (
72
+ unitName === "isoWeek" ? "week" : unitName
73
+ ) as dayjs.ManipulateType;
74
+ const end = dayjs(endDateObj).tz(timezone);
75
+ let cursor = dayjs(startDateObj).tz(timezone).startOf(startOfUnit);
76
+
77
+ const keys: string[] = [];
78
+ while (cursor.isBefore(end) || cursor.isSame(end, startOfUnit)) {
79
+ keys.push(formatBucketKey(cursor, granularity));
80
+ cursor = cursor.add(1, stepUnit);
81
+ }
82
+ return keys;
83
+ }
84
+
85
+ function fillVolumeGaps(
86
+ bucketKeys: string[],
87
+ volumeDataRaw: { date: string; completed: number }[],
88
+ ): DashboardDailyTrendMetric[] {
89
+ const completedByKey = new Map(
90
+ volumeDataRaw.map((d) => [d.date, d.completed]),
91
+ );
92
+ return bucketKeys.map((date) => ({
93
+ date,
94
+ completed: completedByKey.get(date) ?? 0,
95
+ }));
96
+ }
97
+
98
+ function resolveVolumeGranularity(
99
+ startDateObj: Date,
100
+ endDateObj: Date,
101
+ ): DashboardVolumeGranularity {
102
+ const spanDays = dayjs(endDateObj).diff(dayjs(startDateObj), "day");
103
+
104
+ if (spanDays <= 2) return "hour";
105
+ if (spanDays <= 60) return "day";
106
+ if (spanDays <= 365) return "week";
107
+ if (spanDays <= 730) return "month";
108
+ if (spanDays <= 1460) return "quarter";
109
+ return "year";
110
+ }
111
+
112
+ const VOLUME_DATE_FORMATS: Partial<Record<DashboardVolumeGranularity, string>> =
113
+ {
114
+ hour: "%Y-%m-%dT%H:00",
115
+ day: "%Y-%m-%d",
116
+ month: "%Y-%m",
117
+ year: "%Y",
118
+ };
119
+
120
+ function buildVolumeBucketIdExpression(
121
+ timezone: string,
122
+ granularity: DashboardVolumeGranularity,
123
+ ) {
124
+ if (granularity === "week") {
125
+ return {
126
+ $dateToString: {
127
+ format: "%Y-%m-%d",
128
+ date: {
129
+ $dateTrunc: {
130
+ date: "$createdAt",
131
+ unit: "week",
132
+ timezone,
133
+ startOfWeek: "monday",
134
+ },
135
+ },
136
+ timezone,
137
+ },
138
+ };
139
+ }
140
+
141
+ if (granularity === "quarter") {
142
+ return {
143
+ $concat: [
144
+ { $toString: { $year: { date: "$createdAt", timezone } } },
145
+ "-Q",
146
+ {
147
+ $toString: {
148
+ $add: [
149
+ {
150
+ $trunc: {
151
+ $divide: [
152
+ {
153
+ $subtract: [
154
+ { $month: { date: "$createdAt", timezone } },
155
+ 1,
156
+ ],
157
+ },
158
+ 3,
159
+ ],
160
+ },
161
+ },
162
+ 1,
163
+ ],
164
+ },
165
+ },
166
+ ],
167
+ };
168
+ }
169
+
170
+ return {
171
+ $dateToString: {
172
+ format: VOLUME_DATE_FORMATS[granularity],
173
+ date: "$createdAt",
174
+ timezone,
175
+ },
176
+ };
177
+ }
178
+
179
+ function buildKpisPipeline() {
180
+ return [
181
+ {
182
+ $group: {
183
+ _id: null,
184
+ completedCount: {
185
+ $sum: { $cond: [{ $eq: ["$status", "completed"] }, 1, 0] },
186
+ },
187
+ inCallCount: {
188
+ $sum: {
189
+ $cond: [{ $in: ["$status", [...IN_CALL_STATUSES]] }, 1, 0],
190
+ },
191
+ },
192
+ answeredDuration: {
193
+ $sum: {
194
+ $cond: [{ $eq: ["$status", "completed"] }, "$callLength", 0],
195
+ },
196
+ },
197
+ },
198
+ },
199
+ ];
200
+ }
201
+
202
+ function buildVolumeDataPipeline(
203
+ timezone: string,
204
+ granularity: DashboardVolumeGranularity,
205
+ ) {
206
+ return [
207
+ {
208
+ $group: {
209
+ _id: buildVolumeBucketIdExpression(timezone, granularity),
210
+ completed: {
211
+ $sum: { $cond: [{ $eq: ["$status", "completed"] }, 1, 0] },
212
+ },
213
+ },
214
+ },
215
+ { $sort: { _id: 1 } },
216
+ ];
217
+ }
218
+
219
+ export async function getDashboardStats(
220
+ params: DashboardReportQuery,
221
+ ): Promise<DashboardReportResponse> {
222
+ const {
223
+ clientId,
224
+ startDate,
225
+ endDate,
226
+ granularity: requestedGranularity,
227
+ } = params;
228
+ const clientConfig = await getClientConfig(clientId);
229
+ const timezone = clientConfig?.timezone ?? "UTC";
230
+
231
+ const startDateObj = dayjs(startDate).tz(timezone).startOf("day").toDate();
232
+ const endDateObj = dayjs(endDate).tz(timezone).endOf("day").toDate();
233
+
234
+ const granularity =
235
+ requestedGranularity ?? resolveVolumeGranularity(startDateObj, endDateObj);
236
+
237
+ const pipeline = [
238
+ {
239
+ $match: {
240
+ clientId,
241
+ createdAt: { $gte: startDateObj, $lte: endDateObj },
242
+ },
243
+ },
244
+ {
245
+ $facet: {
246
+ kpis: buildKpisPipeline(),
247
+ volumeData: buildVolumeDataPipeline(timezone, granularity),
248
+ },
249
+ },
250
+ ];
251
+
252
+ const callsCollection = getCallsCollection();
253
+ const [aggregatedResult] = await callsCollection
254
+ .aggregate<DashboardAggregationResult>(pipeline)
255
+ .toArray();
256
+
257
+ const kpiData = aggregatedResult?.kpis?.[0] ?? DEFAULT_KPI_DATA;
258
+ const volumeDataRaw = aggregatedResult?.volumeData ?? [];
259
+
260
+ const completedCount = kpiData.completedCount;
261
+ const inCallCount = kpiData.inCallCount;
262
+ const totalCalls = completedCount + inCallCount;
263
+ const answeredDuration = kpiData.answeredDuration ?? 0;
264
+
265
+ const kpis: DashboardSummaryMetrics = {
266
+ totalCalls,
267
+ avgDurationSeconds:
268
+ completedCount > 0
269
+ ? Math.round(answeredDuration / completedCount)
270
+ : 0,
271
+ timeSavedMinutes: Math.round(answeredDuration / 60),
272
+ completedCount,
273
+ inCallCount,
274
+ };
275
+
276
+ const bucketKeys = generateBucketKeys(
277
+ startDateObj,
278
+ endDateObj,
279
+ granularity,
280
+ timezone,
281
+ );
282
+ const volumeData: DashboardDailyTrendMetric[] = fillVolumeGaps(
283
+ bucketKeys,
284
+ volumeDataRaw.map((d) => ({ date: d._id, completed: d.completed })),
285
+ );
286
+
287
+ const response: DashboardReportResponse = {
288
+ kpis,
289
+ charts: {
290
+ volumeData,
291
+ volumeGranularity: granularity,
292
+ },
293
+ };
294
+ return response;
295
+ }