@talkpilot/core-db 1.3.5 → 1.3.7

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