@talkpilot/core-db 1.3.4 → 1.3.5
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/.claude/settings.local.json +5 -1
- package/DEVELOPMENT.md +28 -0
- package/README.md +21 -0
- 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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/municipal/index.d.ts +1 -0
- package/dist/municipal/index.d.ts.map +1 -1
- package/dist/municipal/index.js +1 -0
- package/dist/municipal/index.js.map +1 -1
- package/dist/municipal/muniIssues/index.d.ts +7 -0
- package/dist/municipal/muniIssues/index.d.ts.map +1 -0
- package/dist/municipal/muniIssues/index.js +29 -0
- package/dist/municipal/muniIssues/index.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.constants.d.ts +14 -0
- package/dist/municipal/muniIssues/muniIssues.constants.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.constants.js +17 -0
- package/dist/municipal/muniIssues/muniIssues.constants.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.getters.d.ts +7 -0
- package/dist/municipal/muniIssues/muniIssues.getters.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.getters.js +24 -0
- package/dist/municipal/muniIssues/muniIssues.getters.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.schema.d.ts +4 -0
- package/dist/municipal/muniIssues/muniIssues.schema.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.schema.js +63 -0
- package/dist/municipal/muniIssues/muniIssues.schema.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.setters.d.ts +4 -0
- package/dist/municipal/muniIssues/muniIssues.setters.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.setters.js +22 -0
- package/dist/municipal/muniIssues/muniIssues.setters.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.setup.d.ts +3 -0
- package/dist/municipal/muniIssues/muniIssues.setup.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.setup.js +61 -0
- package/dist/municipal/muniIssues/muniIssues.setup.js.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.types.d.ts +30 -0
- package/dist/municipal/muniIssues/muniIssues.types.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.types.js +3 -0
- package/dist/municipal/muniIssues/muniIssues.types.js.map +1 -0
- 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/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 +2 -0
- package/dist/test-utils/factories/index.d.ts.map +1 -1
- package/dist/test-utils/factories/index.js +2 -0
- package/dist/test-utils/factories/index.js.map +1 -1
- package/dist/test-utils/factories/municipal/muniIssues.d.ts +10 -0
- package/dist/test-utils/factories/municipal/muniIssues.d.ts.map +1 -0
- package/dist/test-utils/factories/municipal/muniIssues.js +30 -0
- package/dist/test-utils/factories/municipal/muniIssues.js.map +1 -0
- 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/websitalk/scans/scans.types.js +3 -0
- 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 +10 -0
- package/src/municipal/index.ts +1 -0
- package/src/municipal/muniIssues/__tests__/muniIssues.setters.spec.ts +92 -0
- package/src/municipal/muniIssues/__tests__/muniIssues.setup.spec.ts +92 -0
- package/src/municipal/muniIssues/index.ts +17 -0
- package/src/municipal/muniIssues/muniIssues.constants.ts +18 -0
- package/src/municipal/muniIssues/muniIssues.getters.ts +24 -0
- package/src/municipal/muniIssues/muniIssues.schema.ts +66 -0
- package/src/municipal/muniIssues/muniIssues.setters.ts +24 -0
- package/src/municipal/muniIssues/muniIssues.setup.ts +65 -0
- package/src/municipal/muniIssues/muniIssues.types.ts +37 -0
- package/src/talkpilot/calls/__tests__/calls.dashboard.spec.ts +111 -8
- package/src/talkpilot/calls/dashboard/calls.dashboard.ts +197 -148
- package/src/talkpilot/calls/dashboard/calls.dashboard.types.ts +12 -25
- 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 +2 -0
- package/src/test-utils/factories/municipal/muniIssues.ts +32 -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
|
@@ -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
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MongoMemoryServer } from "mongodb-memory-server";
|
|
2
2
|
import { MongoClient, Db } from "mongodb";
|
|
3
|
-
import { setDb, setMunicipalDataDb } from "../index";
|
|
3
|
+
import { setDb, setMunicipalDataDb, setWebsitalkDb } from "../index";
|
|
4
4
|
|
|
5
5
|
let mongoServer: MongoMemoryServer;
|
|
6
6
|
let client: MongoClient;
|
|
@@ -13,8 +13,10 @@ export async function initTestDb(): Promise<Db> {
|
|
|
13
13
|
await client.connect();
|
|
14
14
|
const talkpilotDb = client.db();
|
|
15
15
|
const municipalDb = client.db("municipal-data");
|
|
16
|
+
const websitalkDb = client.db("website-talk");
|
|
16
17
|
setDb(talkpilotDb);
|
|
17
18
|
setMunicipalDataDb(municipalDb);
|
|
19
|
+
setWebsitalkDb(websitalkDb);
|
|
18
20
|
return talkpilotDb;
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -8,5 +8,7 @@ export * from "./talkpilot/phone_numbers";
|
|
|
8
8
|
export * from "./talkpilot/sessions";
|
|
9
9
|
export * from "./municipal/cities";
|
|
10
10
|
export * from "./municipal/departmentsSubjects";
|
|
11
|
+
export * from "./municipal/muniIssues";
|
|
11
12
|
export * from "./municipal/streets";
|
|
12
13
|
export * from "./municipal/tickets";
|
|
14
|
+
export * from "./websitalk/scans";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Factory } from "fishery";
|
|
2
|
+
import { faker } from "@faker-js/faker";
|
|
3
|
+
import type { CreateMuniIssueInput } from "../../../municipal";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Test factory for `createMuniIssue` input.
|
|
7
|
+
* `jira.key` uses `sequence` so each build gets a unique key (the real key
|
|
8
|
+
* comes from Jira in production).
|
|
9
|
+
*/
|
|
10
|
+
export const muniIssueInputFactory = Factory.define<CreateMuniIssueInput>(
|
|
11
|
+
({ sequence }) => ({
|
|
12
|
+
callSid: `CA${faker.string.alphanumeric(10)}`,
|
|
13
|
+
clientId: faker.string.uuid(),
|
|
14
|
+
cityName: faker.location.city(),
|
|
15
|
+
issueContent: {
|
|
16
|
+
issuerName: faker.person.fullName(),
|
|
17
|
+
issuerRole: faker.person.jobTitle(),
|
|
18
|
+
issueDescription: faker.lorem.sentence(),
|
|
19
|
+
desiredBehavior: faker.lorem.sentence(),
|
|
20
|
+
},
|
|
21
|
+
jira: {
|
|
22
|
+
key: `CP-${sequence}`,
|
|
23
|
+
syncStatus: "created",
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export function createMuniIssueInput(
|
|
29
|
+
overrides?: Partial<CreateMuniIssueInput>,
|
|
30
|
+
): CreateMuniIssueInput {
|
|
31
|
+
return muniIssueInputFactory.build(overrides);
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Factory } from "fishery";
|
|
2
|
+
import { faker } from "@faker-js/faker";
|
|
3
|
+
import type { Scan } from "../../../websitalk";
|
|
4
|
+
|
|
5
|
+
export const scanFactory = Factory.define<Scan>(() => ({
|
|
6
|
+
clientId: faker.string.uuid(),
|
|
7
|
+
baseUrl: faker.internet.url(),
|
|
8
|
+
status: "CHECKING",
|
|
9
|
+
activeTasksCount: 0,
|
|
10
|
+
pagesScanned: 0,
|
|
11
|
+
pagesSkipped: 0,
|
|
12
|
+
skipped: [],
|
|
13
|
+
siteChrome: {
|
|
14
|
+
url: faker.internet.url(),
|
|
15
|
+
sourceType: "site_chrome",
|
|
16
|
+
},
|
|
17
|
+
createdAt: new Date(),
|
|
18
|
+
updatedAt: new Date(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
export function createScanFixture(overrides?: Partial<Scan>): Scan {
|
|
22
|
+
return scanFactory.build(overrides);
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Db, ObjectId as MongoObjectId } from "mongodb";
|
|
2
|
+
|
|
3
|
+
export * from "./scans";
|
|
4
|
+
export { websitalkMongodbClient } from "./mongodb-client";
|
|
5
|
+
|
|
6
|
+
let db: Db;
|
|
7
|
+
export const setDb = (d: Db) => {
|
|
8
|
+
db = d;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getDb = (): Db => {
|
|
12
|
+
if (!db) throw new Error("Website Talk DB not initialised");
|
|
13
|
+
return db;
|
|
14
|
+
};
|
|
15
|
+
export const ObjectId = MongoObjectId;
|