@talkpilot/core-db 1.3.0 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -30
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +10 -0
- package/dist/connection.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/municipal/tickets/index.d.ts +1 -2
- package/dist/municipal/tickets/index.d.ts.map +1 -1
- package/dist/municipal/tickets/index.js +0 -1
- package/dist/municipal/tickets/index.js.map +1 -1
- package/dist/municipal/tickets/tickets.getters.d.ts +11 -0
- package/dist/municipal/tickets/tickets.getters.d.ts.map +1 -1
- package/dist/municipal/tickets/tickets.getters.js +128 -0
- package/dist/municipal/tickets/tickets.getters.js.map +1 -1
- package/dist/municipal/tickets/tickets.types.d.ts +5 -10
- package/dist/municipal/tickets/tickets.types.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.types.d.ts +2 -1
- package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.types.js +3 -0
- package/dist/talkpilot/calls/calls.types.js.map +1 -1
- package/dist/talkpilot/calls/dashboard/calls.dashboard.d.ts +1 -33
- package/dist/talkpilot/calls/dashboard/calls.dashboard.d.ts.map +1 -1
- package/dist/talkpilot/calls/dashboard/calls.dashboard.js +146 -131
- package/dist/talkpilot/calls/dashboard/calls.dashboard.js.map +1 -1
- package/dist/talkpilot/calls/dashboard/calls.dashboard.types.d.ts +6 -27
- package/dist/talkpilot/calls/dashboard/calls.dashboard.types.d.ts.map +1 -1
- package/dist/talkpilot/calls/index.d.ts +0 -3
- package/dist/talkpilot/calls/index.d.ts.map +1 -1
- package/dist/talkpilot/calls/index.js +0 -3
- package/dist/talkpilot/calls/index.js.map +1 -1
- package/dist/test-utils/db-utils.d.ts.map +1 -1
- package/dist/test-utils/db-utils.js +2 -0
- package/dist/test-utils/db-utils.js.map +1 -1
- package/dist/test-utils/factories/index.d.ts +1 -0
- package/dist/test-utils/factories/index.d.ts.map +1 -1
- package/dist/test-utils/factories/index.js +1 -0
- package/dist/test-utils/factories/index.js.map +1 -1
- package/dist/test-utils/factories/websitalk/scans.d.ts +5 -0
- package/dist/test-utils/factories/websitalk/scans.d.ts.map +1 -0
- package/dist/test-utils/factories/websitalk/scans.js +25 -0
- package/dist/test-utils/factories/websitalk/scans.js.map +1 -0
- package/dist/websitalk/index.d.ts +7 -0
- package/dist/websitalk/index.d.ts.map +1 -0
- package/dist/websitalk/index.js +34 -0
- package/dist/websitalk/index.js.map +1 -0
- package/dist/websitalk/mongodb-client.d.ts +13 -0
- package/dist/websitalk/mongodb-client.d.ts.map +1 -0
- package/dist/websitalk/mongodb-client.js +56 -0
- package/dist/websitalk/mongodb-client.js.map +1 -0
- package/dist/websitalk/scans/index.d.ts +3 -0
- package/dist/websitalk/scans/index.d.ts.map +1 -0
- package/dist/websitalk/scans/index.js +19 -0
- package/dist/websitalk/scans/index.js.map +1 -0
- package/dist/websitalk/scans/scans.getters.d.ts +12 -0
- package/dist/websitalk/scans/scans.getters.d.ts.map +1 -0
- package/dist/websitalk/scans/scans.getters.js +74 -0
- package/dist/websitalk/scans/scans.getters.js.map +1 -0
- package/dist/websitalk/scans/scans.types.d.ts +45 -0
- package/dist/websitalk/scans/scans.types.d.ts.map +1 -0
- package/dist/{talkpilot/calls/calls.statistics.types.js → websitalk/scans/scans.types.js} +1 -1
- package/dist/websitalk/scans/scans.types.js.map +1 -0
- package/package.json +1 -1
- package/src/connection.ts +12 -0
- package/src/index.ts +9 -0
- package/src/municipal/tickets/__tests__/tickets.getters.spec.ts +37 -1
- package/src/municipal/tickets/index.ts +1 -2
- package/src/municipal/tickets/tickets.getters.ts +140 -0
- package/src/municipal/tickets/tickets.types.ts +9 -14
- package/src/talkpilot/calls/__tests__/calls.dashboard.spec.ts +111 -8
- package/src/talkpilot/calls/calls.types.ts +2 -4
- package/src/talkpilot/calls/dashboard/calls.dashboard.ts +197 -148
- package/src/talkpilot/calls/dashboard/calls.dashboard.types.ts +12 -25
- package/src/talkpilot/calls/index.ts +0 -3
- package/src/talkpilot/clientsConfig/__tests__/clientsConfig.spec.ts +0 -7
- package/src/test-utils/db-utils.ts +3 -1
- package/src/test-utils/factories/index.ts +1 -0
- package/src/test-utils/factories/websitalk/scans.ts +23 -0
- package/src/websitalk/index.ts +15 -0
- package/src/websitalk/mongodb-client.ts +61 -0
- package/src/websitalk/scans/__tests__/scans.spec.ts +218 -0
- package/src/websitalk/scans/index.ts +2 -0
- package/src/websitalk/scans/scans.getters.ts +113 -0
- package/src/websitalk/scans/scans.types.ts +53 -0
- package/dist/municipal/tickets/tickets.constants.d.ts +0 -7
- package/dist/municipal/tickets/tickets.constants.d.ts.map +0 -1
- package/dist/municipal/tickets/tickets.constants.js +0 -10
- package/dist/municipal/tickets/tickets.constants.js.map +0 -1
- package/dist/municipal/tickets/tickets.deprecated.getters.d.ts +0 -12
- package/dist/municipal/tickets/tickets.deprecated.getters.d.ts.map +0 -1
- package/dist/municipal/tickets/tickets.deprecated.getters.js +0 -131
- package/dist/municipal/tickets/tickets.deprecated.getters.js.map +0 -1
- package/dist/municipal/tickets/tickets.statistics.aggregation.d.ts +0 -45
- package/dist/municipal/tickets/tickets.statistics.aggregation.d.ts.map +0 -1
- package/dist/municipal/tickets/tickets.statistics.aggregation.js +0 -98
- package/dist/municipal/tickets/tickets.statistics.aggregation.js.map +0 -1
- package/dist/municipal/tickets/tickets.statistics.dates.d.ts +0 -7
- package/dist/municipal/tickets/tickets.statistics.dates.d.ts.map +0 -1
- package/dist/municipal/tickets/tickets.statistics.dates.js +0 -40
- package/dist/municipal/tickets/tickets.statistics.dates.js.map +0 -1
- package/dist/municipal/tickets/tickets.statistics.getters.d.ts +0 -9
- package/dist/municipal/tickets/tickets.statistics.getters.d.ts.map +0 -1
- package/dist/municipal/tickets/tickets.statistics.getters.js +0 -55
- package/dist/municipal/tickets/tickets.statistics.getters.js.map +0 -1
- package/dist/municipal/tickets/tickets.statistics.pipeline.d.ts +0 -53
- package/dist/municipal/tickets/tickets.statistics.pipeline.d.ts.map +0 -1
- package/dist/municipal/tickets/tickets.statistics.pipeline.js +0 -112
- package/dist/municipal/tickets/tickets.statistics.pipeline.js.map +0 -1
- package/dist/municipal/tickets/tickets.statistics.utils.d.ts +0 -7
- package/dist/municipal/tickets/tickets.statistics.utils.d.ts.map +0 -1
- package/dist/municipal/tickets/tickets.statistics.utils.js +0 -40
- package/dist/municipal/tickets/tickets.statistics.utils.js.map +0 -1
- package/dist/talkpilot/calls/calls.constants.d.ts +0 -17
- package/dist/talkpilot/calls/calls.constants.d.ts.map +0 -1
- package/dist/talkpilot/calls/calls.constants.js +0 -20
- package/dist/talkpilot/calls/calls.constants.js.map +0 -1
- package/dist/talkpilot/calls/calls.statistics.getters.d.ts +0 -19
- package/dist/talkpilot/calls/calls.statistics.getters.d.ts.map +0 -1
- package/dist/talkpilot/calls/calls.statistics.getters.js +0 -375
- package/dist/talkpilot/calls/calls.statistics.getters.js.map +0 -1
- package/dist/talkpilot/calls/calls.statistics.ticketScope.d.ts +0 -12
- package/dist/talkpilot/calls/calls.statistics.ticketScope.d.ts.map +0 -1
- package/dist/talkpilot/calls/calls.statistics.ticketScope.js +0 -37
- package/dist/talkpilot/calls/calls.statistics.ticketScope.js.map +0 -1
- package/dist/talkpilot/calls/calls.statistics.tickets.d.ts +0 -17
- package/dist/talkpilot/calls/calls.statistics.tickets.d.ts.map +0 -1
- package/dist/talkpilot/calls/calls.statistics.tickets.js +0 -33
- package/dist/talkpilot/calls/calls.statistics.tickets.js.map +0 -1
- package/dist/talkpilot/calls/calls.statistics.types.d.ts +0 -39
- package/dist/talkpilot/calls/calls.statistics.types.d.ts.map +0 -1
- package/dist/talkpilot/calls/calls.statistics.types.js.map +0 -1
- package/dist/utils/date.utils.d.ts +0 -49
- package/dist/utils/date.utils.d.ts.map +0 -1
- package/dist/utils/date.utils.js +0 -103
- package/dist/utils/date.utils.js.map +0 -1
- package/dist/utils/statistics.aggregation.d.ts +0 -20
- package/dist/utils/statistics.aggregation.d.ts.map +0 -1
- package/dist/utils/statistics.aggregation.js +0 -43
- package/dist/utils/statistics.aggregation.js.map +0 -1
- package/src/municipal/tickets/__tests__/tickets.statistics.spec.ts +0 -104
- package/src/municipal/tickets/tickets.constants.ts +0 -8
- package/src/municipal/tickets/tickets.statistics.aggregation.ts +0 -113
- package/src/municipal/tickets/tickets.statistics.getters.ts +0 -93
- package/src/talkpilot/calls/__tests__/calls.statistics.spec.ts +0 -281
- package/src/talkpilot/calls/calls.constants.ts +0 -20
- package/src/talkpilot/calls/calls.statistics.getters.ts +0 -525
- package/src/talkpilot/calls/calls.statistics.types.ts +0 -44
- package/src/utils/date.utils.ts +0 -116
|
@@ -25,8 +25,8 @@ describe("getDashboardStats", () => {
|
|
|
25
25
|
|
|
26
26
|
const params = {
|
|
27
27
|
clientId,
|
|
28
|
-
startDate: "2026-05-
|
|
29
|
-
endDate: "2026-05-
|
|
28
|
+
startDate: "2026-05-01T00:00:00.000Z",
|
|
29
|
+
endDate: "2026-05-31T23:59:59.999Z",
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const result = await getDashboardStats(params);
|
|
@@ -35,12 +35,115 @@ describe("getDashboardStats", () => {
|
|
|
35
35
|
expect(result.kpis.totalCalls).toBe(0);
|
|
36
36
|
expect(result.kpis.completedCount).toBe(0);
|
|
37
37
|
expect(result.kpis.avgDurationSeconds).toBe(0);
|
|
38
|
-
expect(result.
|
|
39
|
-
expect(result.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
expect(result.kpis.busyCount).toBe(0);
|
|
39
|
+
expect(result.kpis.timeSavedMinutes).toBe(0);
|
|
40
|
+
// ~30 day range -> daily buckets, gap-filled with zeros across the whole range
|
|
41
|
+
expect(result.charts.volumeGranularity).toBe("day");
|
|
42
|
+
expect(result.charts.volumeData.length).toBeGreaterThan(0);
|
|
43
|
+
expect(result.charts.volumeData.every((d) => d.completed === 0)).toBe(true);
|
|
44
|
+
expect(result.charts.volumeData[0].date).toBe("2026-05-01");
|
|
45
|
+
expect(
|
|
46
|
+
result.charts.volumeData[result.charts.volumeData.length - 1].date,
|
|
47
|
+
).toBe("2026-05-31");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should pick a bucket size that keeps the volume chart readable across range lengths", async () => {
|
|
51
|
+
const clientId = "client-dash-456";
|
|
52
|
+
|
|
53
|
+
await getClientsConfigCollection().insertOne({
|
|
54
|
+
clientId,
|
|
55
|
+
timezone: "UTC",
|
|
56
|
+
products: {},
|
|
57
|
+
} as any);
|
|
58
|
+
|
|
59
|
+
const oneDay = await getDashboardStats({
|
|
60
|
+
clientId,
|
|
61
|
+
startDate: "2026-05-01T00:00:00.000Z",
|
|
62
|
+
endDate: "2026-05-01T23:59:59.999Z",
|
|
63
|
+
});
|
|
64
|
+
expect(oneDay.charts.volumeGranularity).toBe("hour");
|
|
65
|
+
|
|
66
|
+
const threeWeeks = await getDashboardStats({
|
|
67
|
+
clientId,
|
|
68
|
+
startDate: "2026-05-01T00:00:00.000Z",
|
|
69
|
+
endDate: "2026-05-21T23:59:59.999Z",
|
|
70
|
+
});
|
|
71
|
+
expect(threeWeeks.charts.volumeGranularity).toBe("day");
|
|
72
|
+
|
|
73
|
+
const fourMonths = await getDashboardStats({
|
|
74
|
+
clientId,
|
|
75
|
+
startDate: "2026-01-01T00:00:00.000Z",
|
|
76
|
+
endDate: "2026-04-30T23:59:59.999Z",
|
|
77
|
+
});
|
|
78
|
+
expect(fourMonths.charts.volumeGranularity).toBe("week");
|
|
79
|
+
|
|
80
|
+
const eighteenMonths = await getDashboardStats({
|
|
81
|
+
clientId,
|
|
82
|
+
startDate: "2025-01-01T00:00:00.000Z",
|
|
83
|
+
endDate: "2026-06-30T23:59:59.999Z",
|
|
84
|
+
});
|
|
85
|
+
expect(eighteenMonths.charts.volumeGranularity).toBe("month");
|
|
86
|
+
|
|
87
|
+
const threeYears = await getDashboardStats({
|
|
88
|
+
clientId,
|
|
89
|
+
startDate: "2023-01-01T00:00:00.000Z",
|
|
90
|
+
endDate: "2026-01-01T23:59:59.999Z",
|
|
91
|
+
});
|
|
92
|
+
expect(threeYears.charts.volumeGranularity).toBe("quarter");
|
|
93
|
+
|
|
94
|
+
const fiveYears = await getDashboardStats({
|
|
95
|
+
clientId,
|
|
96
|
+
startDate: "2021-01-01T00:00:00.000Z",
|
|
97
|
+
endDate: "2026-01-01T23:59:59.999Z",
|
|
98
|
+
});
|
|
99
|
+
expect(fiveYears.charts.volumeGranularity).toBe("year");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should fill gaps with zero-count buckets so the chart has no missing points", async () => {
|
|
103
|
+
const clientId = "client-dash-789";
|
|
104
|
+
|
|
105
|
+
await getClientsConfigCollection().insertOne({
|
|
106
|
+
clientId,
|
|
107
|
+
timezone: "UTC",
|
|
108
|
+
products: {},
|
|
109
|
+
} as any);
|
|
110
|
+
|
|
111
|
+
// Sparse data: only two of the day's hours have calls.
|
|
112
|
+
await getCallsCollection().insertMany([
|
|
113
|
+
{
|
|
114
|
+
clientId,
|
|
115
|
+
status: "completed",
|
|
116
|
+
callLength: 60,
|
|
117
|
+
createdAt: new Date("2026-05-01T09:15:00Z"),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
clientId,
|
|
121
|
+
status: "completed",
|
|
122
|
+
callLength: 90,
|
|
123
|
+
createdAt: new Date("2026-05-01T18:45:00Z"),
|
|
124
|
+
},
|
|
125
|
+
] as any);
|
|
126
|
+
|
|
127
|
+
const result = await getDashboardStats({
|
|
128
|
+
clientId,
|
|
129
|
+
startDate: "2026-05-01T00:00:00.000Z",
|
|
130
|
+
endDate: "2026-05-01T23:59:59.999Z",
|
|
44
131
|
});
|
|
132
|
+
|
|
133
|
+
expect(result.charts.volumeGranularity).toBe("hour");
|
|
134
|
+
// A full day of hourly buckets, with no gaps, sorted chronologically.
|
|
135
|
+
expect(result.charts.volumeData).toHaveLength(24);
|
|
136
|
+
expect(result.charts.volumeData[0].date).toBe("2026-05-01T00:00");
|
|
137
|
+
expect(result.charts.volumeData[23].date).toBe("2026-05-01T23:00");
|
|
138
|
+
|
|
139
|
+
const populated = result.charts.volumeData.filter((d) => d.completed > 0);
|
|
140
|
+
expect(populated).toHaveLength(2);
|
|
141
|
+
expect(populated.map((d) => d.date)).toEqual([
|
|
142
|
+
"2026-05-01T09:00",
|
|
143
|
+
"2026-05-01T18:00",
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
const empty = result.charts.volumeData.filter((d) => d.completed === 0);
|
|
147
|
+
expect(empty).toHaveLength(22);
|
|
45
148
|
});
|
|
46
149
|
});
|
|
@@ -2,10 +2,8 @@ import { ObjectId, Sort, WithId } from "mongodb";
|
|
|
2
2
|
import { TranscriptionSegment } from "../results";
|
|
3
3
|
import { LeadProperty } from "../leads";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
CONFERENCE_ROLE_SUPERVISOR,
|
|
8
|
-
} from "./calls.constants";
|
|
5
|
+
export const CONFERENCE_ROLE_CUSTOMER = "customer" as const;
|
|
6
|
+
export const CONFERENCE_ROLE_SUPERVISOR = "supervisor" as const;
|
|
9
7
|
|
|
10
8
|
export type ConferenceRole =
|
|
11
9
|
| typeof CONFERENCE_ROLE_CUSTOMER
|
|
@@ -3,173 +3,237 @@ import { getCallsCollection } from "../calls.getters";
|
|
|
3
3
|
import { getClientConfig } from "../../clientsConfig";
|
|
4
4
|
import utc from "dayjs/plugin/utc";
|
|
5
5
|
import timezonePlugin from "dayjs/plugin/timezone";
|
|
6
|
+
import isoWeek from "dayjs/plugin/isoWeek";
|
|
7
|
+
import quarterOfYear from "dayjs/plugin/quarterOfYear";
|
|
6
8
|
import {
|
|
7
9
|
DashboardAggregationResult,
|
|
8
10
|
DashboardDailyTrendMetric,
|
|
9
|
-
DashboardHeatmapMetric,
|
|
10
11
|
DashboardReportQuery,
|
|
11
12
|
DashboardReportResponse,
|
|
12
13
|
DashboardSummaryMetrics,
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
DashboardVolumeGranularity,
|
|
15
|
+
} from "./calls.dashboard.types";
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
DashboardVolumeGranularity,
|
|
19
|
+
DashboardReportQuery,
|
|
20
|
+
DashboardReportResponse,
|
|
15
21
|
} from "./calls.dashboard.types";
|
|
16
|
-
import { CallLengthThresholds } from "src/utils/shared.types";
|
|
17
22
|
|
|
18
23
|
const DEFAULT_KPI_DATA = {
|
|
19
|
-
totalCalls: 0,
|
|
20
|
-
totalDuration: 0,
|
|
21
24
|
completedCount: 0,
|
|
22
|
-
failedCount: 0,
|
|
23
|
-
noAnswerCount: 0,
|
|
24
25
|
busyCount: 0,
|
|
26
|
+
answeredDuration: 0,
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
dayjs.extend(utc);
|
|
28
30
|
dayjs.extend(timezonePlugin);
|
|
31
|
+
dayjs.extend(isoWeek);
|
|
32
|
+
dayjs.extend(quarterOfYear);
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
}
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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: {
|
|
61
129
|
date: "$createdAt",
|
|
62
|
-
|
|
130
|
+
unit: "week",
|
|
131
|
+
timezone,
|
|
132
|
+
startOfWeek: "monday",
|
|
63
133
|
},
|
|
64
134
|
},
|
|
65
|
-
|
|
66
|
-
completed: {
|
|
67
|
-
$sum: { $cond: [{ $eq: ["$status", "completed"] }, 1, 0] },
|
|
68
|
-
},
|
|
135
|
+
timezone,
|
|
69
136
|
},
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
];
|
|
73
|
-
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
74
139
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
],
|
|
86
163
|
},
|
|
87
|
-
hour: { $hour: { date: "$createdAt", timezone: timezone } },
|
|
88
164
|
},
|
|
89
|
-
|
|
90
|
-
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
$dateToString: {
|
|
171
|
+
format: VOLUME_DATE_FORMATS[granularity],
|
|
172
|
+
date: "$createdAt",
|
|
173
|
+
timezone,
|
|
91
174
|
},
|
|
92
|
-
|
|
175
|
+
};
|
|
93
176
|
}
|
|
94
177
|
|
|
95
|
-
|
|
96
|
-
thresholds: CallLengthThresholds,
|
|
97
|
-
) {
|
|
98
|
-
const { shortThreshold, mediumThreshold } = thresholds;
|
|
99
|
-
|
|
178
|
+
function buildKpisPipeline() {
|
|
100
179
|
return [
|
|
101
180
|
{
|
|
102
181
|
$group: {
|
|
103
182
|
_id: null,
|
|
104
|
-
|
|
105
|
-
$sum: { $cond: [{ $
|
|
183
|
+
completedCount: {
|
|
184
|
+
$sum: { $cond: [{ $eq: ["$status", "completed"] }, 1, 0] },
|
|
106
185
|
},
|
|
107
|
-
|
|
186
|
+
busyCount: {
|
|
187
|
+
$sum: { $cond: [{ $eq: ["$status", "busy"] }, 1, 0] },
|
|
188
|
+
},
|
|
189
|
+
answeredDuration: {
|
|
108
190
|
$sum: {
|
|
109
191
|
$cond: [
|
|
110
|
-
{
|
|
111
|
-
|
|
112
|
-
{ $gte: ["$callLength", shortThreshold] },
|
|
113
|
-
{ $lte: ["$callLength", mediumThreshold] },
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
1,
|
|
192
|
+
{ $in: ["$status", ["completed", "busy"]] },
|
|
193
|
+
"$callLength",
|
|
117
194
|
0,
|
|
118
195
|
],
|
|
119
196
|
},
|
|
120
197
|
},
|
|
121
|
-
long: {
|
|
122
|
-
$sum: { $cond: [{ $gt: ["$callLength", mediumThreshold] }, 1, 0] },
|
|
123
|
-
},
|
|
124
198
|
},
|
|
125
199
|
},
|
|
126
200
|
];
|
|
127
201
|
}
|
|
128
202
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Array.from(map.entries())
|
|
145
|
-
.sort(([a], [b]) => a - b)
|
|
146
|
-
.map(([h, c]) => ({
|
|
147
|
-
hour: h,
|
|
148
|
-
calls: c,
|
|
149
|
-
}));
|
|
150
|
-
|
|
151
|
-
const heatmap: Record<string, DashboardHeatmapMetric[]> = {};
|
|
152
|
-
for (const [day, hourMap] of Array.from(heatmapMap.entries()).sort()) {
|
|
153
|
-
heatmap[day] = toSortedBuckets(hourMap);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return heatmap;
|
|
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
|
+
];
|
|
157
218
|
}
|
|
158
219
|
|
|
159
220
|
export async function getDashboardStats(
|
|
160
221
|
params: DashboardReportQuery,
|
|
161
222
|
): Promise<DashboardReportResponse> {
|
|
162
|
-
const {
|
|
223
|
+
const {
|
|
224
|
+
clientId,
|
|
225
|
+
startDate,
|
|
226
|
+
endDate,
|
|
227
|
+
granularity: requestedGranularity,
|
|
228
|
+
} = params;
|
|
163
229
|
const clientConfig = await getClientConfig(clientId);
|
|
164
230
|
const timezone = clientConfig?.timezone ?? "UTC";
|
|
165
231
|
|
|
166
|
-
const startDateObj = dayjs.tz(
|
|
167
|
-
const endDateObj = dayjs.tz(
|
|
232
|
+
const startDateObj = dayjs(startDate).tz(timezone).startOf("day").toDate();
|
|
233
|
+
const endDateObj = dayjs(endDate).tz(timezone).endOf("day").toDate();
|
|
168
234
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
mediumThreshold: clientConfig?.callLengthThresholds?.mediumThreshold ?? 180,
|
|
172
|
-
};
|
|
235
|
+
const granularity =
|
|
236
|
+
requestedGranularity ?? resolveVolumeGranularity(startDateObj, endDateObj);
|
|
173
237
|
|
|
174
238
|
const pipeline = [
|
|
175
239
|
{
|
|
@@ -181,9 +245,7 @@ export async function getDashboardStats(
|
|
|
181
245
|
{
|
|
182
246
|
$facet: {
|
|
183
247
|
kpis: buildKpisPipeline(),
|
|
184
|
-
|
|
185
|
-
hourlyData: buildHourlyDataPipeline(timezone),
|
|
186
|
-
callLengthBuckets: buildCallLengthBucketsPipeline(thresholds),
|
|
248
|
+
volumeData: buildVolumeDataPipeline(timezone, granularity),
|
|
187
249
|
},
|
|
188
250
|
},
|
|
189
251
|
];
|
|
@@ -194,49 +256,36 @@ export async function getDashboardStats(
|
|
|
194
256
|
.toArray();
|
|
195
257
|
|
|
196
258
|
const kpiData = aggregatedResult?.kpis?.[0] ?? DEFAULT_KPI_DATA;
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
medium: 0,
|
|
202
|
-
long: 0,
|
|
203
|
-
};
|
|
259
|
+
const volumeDataRaw = aggregatedResult?.volumeData ?? [];
|
|
260
|
+
|
|
261
|
+
const answeredCount = kpiData.completedCount + kpiData.busyCount;
|
|
262
|
+
const answeredDuration = kpiData.answeredDuration ?? 0;
|
|
204
263
|
|
|
205
264
|
const kpis: DashboardSummaryMetrics = {
|
|
206
|
-
totalCalls:
|
|
265
|
+
totalCalls: answeredCount,
|
|
207
266
|
avgDurationSeconds:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
: 0,
|
|
211
|
-
timeSavedMinutes: Math.round((kpiData.totalDuration ?? 0) / 60),
|
|
212
|
-
successRate:
|
|
213
|
-
kpiData.totalCalls > 0
|
|
214
|
-
? Math.round((kpiData.completedCount / kpiData.totalCalls) * 1000) / 10
|
|
215
|
-
: 0,
|
|
267
|
+
answeredCount > 0 ? Math.round(answeredDuration / answeredCount) : 0,
|
|
268
|
+
timeSavedMinutes: Math.round(answeredDuration / 60),
|
|
216
269
|
completedCount: kpiData.completedCount,
|
|
217
|
-
failedCount: kpiData.failedCount,
|
|
218
|
-
noAnswerCount: kpiData.noAnswerCount,
|
|
219
270
|
busyCount: kpiData.busyCount,
|
|
220
271
|
};
|
|
221
272
|
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
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
|
+
);
|
|
229
283
|
|
|
230
284
|
const response: DashboardReportResponse = {
|
|
231
285
|
kpis,
|
|
232
286
|
charts: {
|
|
233
287
|
volumeData,
|
|
234
|
-
|
|
235
|
-
callLengthBuckets: {
|
|
236
|
-
short: callLengthRaw.short,
|
|
237
|
-
medium: callLengthRaw.medium,
|
|
238
|
-
long: callLengthRaw.long,
|
|
239
|
-
},
|
|
288
|
+
volumeGranularity: granularity,
|
|
240
289
|
},
|
|
241
290
|
};
|
|
242
291
|
return response;
|
|
@@ -5,37 +5,35 @@ export type DashboardHeatmapMetric = {
|
|
|
5
5
|
|
|
6
6
|
export type DashboardDailyTrendMetric = {
|
|
7
7
|
date: string;
|
|
8
|
-
count: number;
|
|
9
8
|
completed: number;
|
|
10
9
|
};
|
|
11
10
|
|
|
12
|
-
export type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
export type DashboardVolumeGranularity =
|
|
12
|
+
| "hour"
|
|
13
|
+
| "day"
|
|
14
|
+
| "week"
|
|
15
|
+
| "month"
|
|
16
|
+
| "quarter"
|
|
17
|
+
| "year";
|
|
17
18
|
|
|
18
19
|
export type DashboardSummaryMetrics = {
|
|
19
20
|
totalCalls: number;
|
|
20
21
|
avgDurationSeconds: number;
|
|
21
22
|
timeSavedMinutes: number;
|
|
22
|
-
successRate: number;
|
|
23
23
|
completedCount: number;
|
|
24
|
-
failedCount: number;
|
|
25
|
-
noAnswerCount: number;
|
|
26
24
|
busyCount: number;
|
|
27
25
|
};
|
|
28
26
|
|
|
29
27
|
export type DashboardVisualData = {
|
|
30
28
|
volumeData: DashboardDailyTrendMetric[];
|
|
31
|
-
|
|
32
|
-
callLengthBuckets: DashboardDurationSegments;
|
|
29
|
+
volumeGranularity: DashboardVolumeGranularity;
|
|
33
30
|
};
|
|
34
31
|
|
|
35
32
|
export type DashboardReportQuery = {
|
|
36
33
|
clientId: string;
|
|
37
34
|
startDate: string;
|
|
38
35
|
endDate: string;
|
|
36
|
+
granularity?: DashboardVolumeGranularity;
|
|
39
37
|
};
|
|
40
38
|
|
|
41
39
|
export type DashboardReportResponse = {
|
|
@@ -43,28 +41,17 @@ export type DashboardReportResponse = {
|
|
|
43
41
|
charts: DashboardVisualData;
|
|
44
42
|
};
|
|
45
43
|
type RawKpiResult = {
|
|
46
|
-
totalCalls: number;
|
|
47
|
-
totalDuration: number | null;
|
|
48
44
|
completedCount: number;
|
|
49
|
-
failedCount: number;
|
|
50
|
-
noAnswerCount: number;
|
|
51
45
|
busyCount: number;
|
|
46
|
+
answeredDuration: number | null;
|
|
52
47
|
};
|
|
53
48
|
|
|
54
|
-
export type
|
|
49
|
+
export type RawVolumeAggregationResult = {
|
|
55
50
|
_id: string;
|
|
56
|
-
count: number;
|
|
57
51
|
completed: number;
|
|
58
52
|
};
|
|
59
53
|
|
|
60
|
-
export type RawHourlyAggregationResult = {
|
|
61
|
-
_id: { day: string; hour: number };
|
|
62
|
-
count: number;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
54
|
export interface DashboardAggregationResult {
|
|
66
55
|
kpis: RawKpiResult[];
|
|
67
|
-
|
|
68
|
-
hourlyData: RawHourlyAggregationResult[];
|
|
69
|
-
callLengthBuckets: DashboardDurationSegments[];
|
|
56
|
+
volumeData: RawVolumeAggregationResult[];
|
|
70
57
|
}
|
|
@@ -36,10 +36,6 @@ describe("db.clientsConfig", () => {
|
|
|
36
36
|
timezone: "Asia/Jerusalem",
|
|
37
37
|
language: "he",
|
|
38
38
|
defaultBaseUrl: "https://acme.example",
|
|
39
|
-
scheduler: {
|
|
40
|
-
type: "cron",
|
|
41
|
-
schedule: "0 9 * * 1-5",
|
|
42
|
-
},
|
|
43
39
|
},
|
|
44
40
|
},
|
|
45
41
|
});
|
|
@@ -57,9 +53,6 @@ describe("db.clientsConfig", () => {
|
|
|
57
53
|
timezone: "Asia/Jerusalem",
|
|
58
54
|
language: "he",
|
|
59
55
|
defaultBaseUrl: "https://acme.example",
|
|
60
|
-
scheduler: {
|
|
61
|
-
type: "cron",
|
|
62
|
-
},
|
|
63
56
|
},
|
|
64
57
|
},
|
|
65
58
|
});
|