@talkpilot/core-db 1.1.18 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/development.mdc +65 -65
- package/DEVELOPMENT.md +98 -98
- package/README.md +160 -160
- package/dist/talkpilot/calls/calls.getters.d.ts +2 -1
- package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.getters.js +176 -0
- package/dist/talkpilot/calls/calls.getters.js.map +1 -1
- package/dist/talkpilot/calls/calls.types.d.ts +49 -2
- package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.types.js.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts +1 -0
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.js +13 -0
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.js.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.types.d.ts +2 -0
- package/dist/talkpilot/clientsConfig/clientsConfig.types.d.ts.map +1 -1
- package/jest.config.js +19 -19
- package/package.json +45 -45
- package/src/__tests__/setup.ts +20 -20
- package/src/connection.ts +42 -42
- package/src/index.ts +16 -16
- package/src/municipal/__tests__/validation.spec.ts +62 -62
- package/src/municipal/cities/cities.getters.ts +50 -50
- package/src/municipal/cities/cities.types.ts +11 -11
- package/src/municipal/cities/index.ts +2 -2
- package/src/municipal/departmentsSubjects/departmentsSubjects.getters.ts +282 -282
- package/src/municipal/departmentsSubjects/departmentsSubjects.types.ts +72 -72
- package/src/municipal/departmentsSubjects/index.ts +9 -9
- package/src/municipal/index.ts +21 -21
- package/src/municipal/mongodb-client.ts +61 -61
- package/src/municipal/streets/index.ts +2 -2
- package/src/municipal/streets/streets.getters.ts +125 -125
- package/src/municipal/streets/streets.types.ts +18 -18
- package/src/municipal/systemInstructions/__tests__/getters.spec.ts +113 -113
- package/src/municipal/systemInstructions/__tests__/setters.spec.ts +274 -274
- package/src/municipal/systemInstructions/index.ts +7 -7
- package/src/municipal/systemInstructions/instructions.getters.ts +57 -57
- package/src/municipal/systemInstructions/instructions.setters.ts +119 -119
- package/src/municipal/systemInstructions/instructions.types.ts +30 -30
- package/src/municipal/tickets/__tests__/tickets.getters.spec.ts +66 -66
- package/src/municipal/tickets/index.ts +2 -2
- package/src/municipal/tickets/tickets.getters.ts +261 -261
- package/src/municipal/tickets/tickets.types.ts +43 -43
- package/src/municipal/utils/types.ts +11 -11
- package/src/talkpilot/__tests__/db.spec.ts +38 -38
- package/src/talkpilot/__tests__/mongodb-client.spec.ts +18 -18
- package/src/talkpilot/__tests__/validation.spec.ts +68 -68
- package/src/talkpilot/agents/__tests__/agents.getters.spec.ts +29 -29
- package/src/talkpilot/agents/agents.getters.ts +34 -34
- package/src/talkpilot/agents/agents.types.ts +14 -14
- package/src/talkpilot/agents/index.ts +2 -2
- package/src/talkpilot/backgroundToolResults/__tests__/backgroundToolResults.getters.spec.ts +147 -147
- package/src/talkpilot/backgroundToolResults/backgroundToolResults.getters.ts +65 -65
- package/src/talkpilot/backgroundToolResults/backgroundToolResults.types.ts +23 -23
- package/src/talkpilot/backgroundToolResults/index.ts +2 -2
- package/src/talkpilot/calls/__tests__/callStats.utils.spec.ts +128 -128
- package/src/talkpilot/calls/__tests__/calls.spec.ts +252 -252
- package/src/talkpilot/calls/calls.getters.ts +446 -248
- package/src/talkpilot/calls/calls.types.ts +171 -116
- package/src/talkpilot/calls/index.ts +2 -2
- package/src/talkpilot/clientAudioBuffers/__tests__/clientAudioBuffer.getters.spec.ts +160 -160
- package/src/talkpilot/clientAudioBuffers/clientAudioBuffer.getters.ts +117 -117
- package/src/talkpilot/clientAudioBuffers/clientsAudioBuffers.types.ts +25 -25
- package/src/talkpilot/clientAudioBuffers/index.ts +2 -2
- package/src/talkpilot/clients/clients.getters.ts +16 -16
- package/src/talkpilot/clients/clients.types.ts +14 -14
- package/src/talkpilot/clients/index.ts +2 -2
- package/src/talkpilot/clientsConfig/__tests__/clientsConfig.spec.ts +106 -106
- package/src/talkpilot/clientsConfig/clientsConfig.getters.ts +44 -22
- package/src/talkpilot/clientsConfig/clientsConfig.types.ts +94 -92
- package/src/talkpilot/clientsConfig/index.ts +2 -2
- package/src/talkpilot/flows/__tests__/flows.schema.spec.ts +67 -67
- package/src/talkpilot/flows/flows.getter.ts +14 -14
- package/src/talkpilot/flows/flows.schema.ts +153 -153
- package/src/talkpilot/flows/flows.types.ts +184 -184
- package/src/talkpilot/flows/index.ts +2 -2
- package/src/talkpilot/groups/__tests__/groups.spec.ts +90 -90
- package/src/talkpilot/groups/__tests__/phone.utils.spec.ts +32 -32
- package/src/talkpilot/groups/groups.getters.ts +30 -30
- package/src/talkpilot/groups/groups.types.ts +29 -29
- package/src/talkpilot/groups/index.ts +3 -3
- package/src/talkpilot/groups/phone.utils.ts +46 -46
- package/src/talkpilot/index.ts +29 -29
- package/src/talkpilot/leads/index.ts +2 -2
- package/src/talkpilot/leads/leads.getter.ts +6 -6
- package/src/talkpilot/leads/leads.schema.ts +33 -33
- package/src/talkpilot/leads/leads.types.ts +20 -20
- package/src/talkpilot/mongodb-client.ts +78 -78
- package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +247 -247
- package/src/talkpilot/phone_numbers/index.ts +2 -2
- package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +154 -154
- package/src/talkpilot/phone_numbers/phone_numbers.schema.ts +17 -17
- package/src/talkpilot/phone_numbers/phone_numbers.types.ts +30 -30
- package/src/talkpilot/plans/__tests__/plans.spec.ts +70 -70
- package/src/talkpilot/plans/index.ts +2 -2
- package/src/talkpilot/plans/plans.getters.ts +132 -132
- package/src/talkpilot/plans/plans.types.ts +89 -89
- package/src/talkpilot/results/index.ts +7 -7
- package/src/talkpilot/results/results.getter.ts +35 -35
- package/src/talkpilot/results/results.schema.ts +25 -25
- package/src/talkpilot/results/results.types.ts +34 -34
- package/src/talkpilot/retry_analyze/__tests__/retryAnalyze.getters.spec.ts +156 -156
- package/src/talkpilot/retry_analyze/index.ts +2 -2
- package/src/talkpilot/retry_analyze/retryAnalyze.getters.ts +75 -75
- package/src/talkpilot/retry_analyze/retryAnalyze.types.ts +13 -13
- package/src/talkpilot/sessions/__tests__/sessions.spec.ts +147 -147
- package/src/talkpilot/sessions/index.ts +2 -2
- package/src/talkpilot/sessions/sessions.getter.ts +92 -92
- package/src/talkpilot/sessions/sessions.schema.ts +34 -34
- package/src/talkpilot/sessions/sessions.types.ts +30 -30
- package/src/talkpilot/subscriptions/__tests__/subscriptions.getters.utils.spec.ts +45 -45
- package/src/talkpilot/subscriptions/index.ts +3 -3
- package/src/talkpilot/subscriptions/subscriptions.getters.ts +146 -146
- package/src/talkpilot/subscriptions/subscriptions.getters.utils.ts +33 -33
- package/src/talkpilot/subscriptions/subscriptions.types.ts +66 -66
- package/src/talkpilot/utils/__tests__/query.utils.spec.ts +49 -49
- package/src/talkpilot/utils/query.utils.ts +21 -21
- package/src/test-utils/db-utils.ts +24 -24
- package/src/test-utils/factories/index.ts +12 -12
- package/src/test-utils/factories/municipal/cities.ts +16 -16
- package/src/test-utils/factories/municipal/departmentsSubjects.ts +37 -37
- package/src/test-utils/factories/municipal/streets.ts +22 -22
- package/src/test-utils/factories/municipal/tickets.ts +39 -39
- package/src/test-utils/factories/talkpilot/agents.ts +19 -19
- package/src/test-utils/factories/talkpilot/calls.ts +37 -37
- package/src/test-utils/factories/talkpilot/clientAudioBuffers.ts +20 -20
- package/src/test-utils/factories/talkpilot/clientsConfig.ts +18 -18
- package/src/test-utils/factories/talkpilot/flows.ts +33 -33
- package/src/test-utils/factories/talkpilot/groups.ts +33 -33
- package/src/test-utils/factories/talkpilot/phone_numbers.ts +22 -22
- package/src/test-utils/factories/talkpilot/sessions.ts +35 -35
- package/src/utils/validation.ts +23 -23
- package/tsconfig.json +23 -23
|
@@ -1,261 +1,261 @@
|
|
|
1
|
-
import { CityName, getDb, ObjectId, Ticket } from "../index";
|
|
2
|
-
import type { SubjectStatsItem } from "./tickets.types";
|
|
3
|
-
import { Collection, Filter, ObjectId as MongoObjectId } from "mongodb";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Generate a new ticket ID as a string
|
|
7
|
-
* @returns A new ObjectId as a string
|
|
8
|
-
*/
|
|
9
|
-
export const generateTicketId = (): string => {
|
|
10
|
-
return new ObjectId().toString();
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const getTicketsCollection = (): Collection<Ticket> => {
|
|
14
|
-
return getDb().collection<Ticket>("tickets");
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const findTickets = async (
|
|
18
|
-
filter: Filter<Ticket> = {},
|
|
19
|
-
): Promise<Ticket[]> => {
|
|
20
|
-
return await getTicketsCollection().find(filter).toArray();
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const findTicketsByCallSid = async (
|
|
24
|
-
callSid: string,
|
|
25
|
-
): Promise<Ticket[]> => {
|
|
26
|
-
return await getTicketsCollection().find({ callSid }).toArray();
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const getTicketById = async (
|
|
30
|
-
ticketId: string,
|
|
31
|
-
): Promise<Ticket | null> => {
|
|
32
|
-
const ticket = await getTicketsCollection().findOne({
|
|
33
|
-
_id: new ObjectId(ticketId),
|
|
34
|
-
});
|
|
35
|
-
return ticket ? ticket : null;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const createTicket = async (
|
|
39
|
-
ticketData: Omit<Ticket, "_id" | "createdAt" | "updatedAt">,
|
|
40
|
-
ticketId?: string,
|
|
41
|
-
): Promise<MongoObjectId> => {
|
|
42
|
-
const ticket: Ticket = {
|
|
43
|
-
_id: ticketId ? new ObjectId(ticketId) : new ObjectId(),
|
|
44
|
-
...ticketData,
|
|
45
|
-
createdAt: new Date(),
|
|
46
|
-
updatedAt: new Date(),
|
|
47
|
-
};
|
|
48
|
-
const { insertedId } = await getTicketsCollection().insertOne(ticket);
|
|
49
|
-
return insertedId;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export const updateTicket = async (
|
|
53
|
-
ticketId: string,
|
|
54
|
-
data: Partial<Omit<Ticket, "_id" | "createdAt" | "updatedAt">>,
|
|
55
|
-
): Promise<Ticket | null> => {
|
|
56
|
-
const result = await getTicketsCollection().findOneAndUpdate(
|
|
57
|
-
{ _id: new ObjectId(ticketId) },
|
|
58
|
-
{ $set: { ...data, updatedAt: new Date() } },
|
|
59
|
-
{ returnDocument: "after" },
|
|
60
|
-
);
|
|
61
|
-
return result || null;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export const deleteTicket = async (ticketId: string): Promise<boolean> => {
|
|
65
|
-
const result = await getTicketsCollection().deleteOne({
|
|
66
|
-
_id: new ObjectId(ticketId),
|
|
67
|
-
});
|
|
68
|
-
return result.deletedCount > 0;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Count tickets by city and date range (createdAt converted to given timezone).
|
|
73
|
-
* Used as "open" tickets count when status is not available.
|
|
74
|
-
*/
|
|
75
|
-
export async function getTicketsCountByCityAndDateRange(
|
|
76
|
-
cityName: string,
|
|
77
|
-
startStr: string,
|
|
78
|
-
endStr: string,
|
|
79
|
-
timezone: string,
|
|
80
|
-
): Promise<number> {
|
|
81
|
-
const doc = await getTicketsCollection()
|
|
82
|
-
.aggregate<{ n: number }>([
|
|
83
|
-
{ $match: { cityName, callSid: { $exists: true, $nin: [null, ""] } } },
|
|
84
|
-
{
|
|
85
|
-
$addFields: {
|
|
86
|
-
dateLocal: {
|
|
87
|
-
$dateToString: { format: "%Y-%m-%d", date: "$createdAt", timezone },
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{ $match: { dateLocal: { $gte: startStr, $lte: endStr } } },
|
|
92
|
-
{ $count: "n" },
|
|
93
|
-
])
|
|
94
|
-
.next();
|
|
95
|
-
return doc?.n ?? 0;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Count of tickets by department (subject) from departmentsSubjects. Different departments (subject_id) are grouped, and sub-subjects are unified under their department.
|
|
100
|
-
* Date filter: createdAt in [startStr, endStr] (in the given timezone). Fallback to "Unclassified" when no match is found in the lookup.
|
|
101
|
-
*/
|
|
102
|
-
export async function getTicketsSubjectStats(
|
|
103
|
-
cityName: string,
|
|
104
|
-
startStr: string,
|
|
105
|
-
endStr: string,
|
|
106
|
-
timezone: string,
|
|
107
|
-
): Promise<SubjectStatsItem[]> {
|
|
108
|
-
const coll = getTicketsCollection();
|
|
109
|
-
const rows = await coll
|
|
110
|
-
.aggregate<{ _id: string; subject: string; count: number }>([
|
|
111
|
-
{ $match: { cityName } },
|
|
112
|
-
{
|
|
113
|
-
$addFields: {
|
|
114
|
-
dateLocal: {
|
|
115
|
-
$dateToString: { format: "%Y-%m-%d", date: "$createdAt", timezone },
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
{ $match: { dateLocal: { $gte: startStr, $lte: endStr } } },
|
|
120
|
-
{
|
|
121
|
-
$addFields: {
|
|
122
|
-
effectiveSubjectId: {
|
|
123
|
-
$ifNull: [
|
|
124
|
-
"$externalCallFields.event_subject_id",
|
|
125
|
-
{
|
|
126
|
-
$ifNull: [
|
|
127
|
-
"$externalCallFields.event_sub_subject_id",
|
|
128
|
-
"$externalCallFields.event_sub_subject_id2",
|
|
129
|
-
],
|
|
130
|
-
},
|
|
131
|
-
],
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
$lookup: {
|
|
137
|
-
from: "departmentsSubjects",
|
|
138
|
-
let: { sid: "$effectiveSubjectId", c: cityName },
|
|
139
|
-
pipeline: [
|
|
140
|
-
{
|
|
141
|
-
$match: {
|
|
142
|
-
$expr: {
|
|
143
|
-
$and: [
|
|
144
|
-
{ $eq: ["$cityName", "$$c"] },
|
|
145
|
-
{
|
|
146
|
-
$or: [
|
|
147
|
-
{ $eq: ["$subject_id", { $ifNull: ["$$sid", ""] }] },
|
|
148
|
-
{
|
|
149
|
-
$eq: ["$sub_subject_id", { $ifNull: ["$$sid", ""] }],
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
$eq: ["$sub_subject_id2", { $ifNull: ["$$sid", ""] }],
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
{ $limit: 1 },
|
|
161
|
-
],
|
|
162
|
-
as: "subj",
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
$addFields: {
|
|
167
|
-
subject_id: {
|
|
168
|
-
$cond: {
|
|
169
|
-
if: { $gt: [{ $size: "$subj" }, 0] },
|
|
170
|
-
then: {
|
|
171
|
-
$ifNull: [{ $arrayElemAt: ["$subj.subject_id", 0] }, ""],
|
|
172
|
-
},
|
|
173
|
-
else: "",
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
subject: {
|
|
177
|
-
$cond: {
|
|
178
|
-
if: { $gt: [{ $size: "$subj" }, 0] },
|
|
179
|
-
then: {
|
|
180
|
-
$ifNull: [
|
|
181
|
-
{ $arrayElemAt: ["$subj.subjectName", 0] },
|
|
182
|
-
"Unclassified",
|
|
183
|
-
],
|
|
184
|
-
},
|
|
185
|
-
else: "Unclassified",
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
$group: {
|
|
192
|
-
_id: "$subject_id",
|
|
193
|
-
subject: { $first: "$subject" },
|
|
194
|
-
count: { $sum: 1 },
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
{ $sort: { count: -1 } },
|
|
198
|
-
])
|
|
199
|
-
.toArray();
|
|
200
|
-
|
|
201
|
-
const total = rows.reduce((s: number, r: any) => s + r.count, 0);
|
|
202
|
-
return rows.map((r: any) => ({
|
|
203
|
-
subject_name: r.subject,
|
|
204
|
-
subject_id: r._id,
|
|
205
|
-
count: r.count,
|
|
206
|
-
percentage: total > 0 ? Math.round((r.count / total) * 100) : 0,
|
|
207
|
-
}));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export const findTicketByQuery = async (query: Partial<Ticket>) => {
|
|
211
|
-
return await getTicketsCollection().findOne(query);
|
|
212
|
-
};
|
|
213
|
-
/**
|
|
214
|
-
* Update ticket files by external call number
|
|
215
|
-
*/
|
|
216
|
-
export const updateTicketFilesByCallNumber = async (params: {
|
|
217
|
-
cityName: CityName;
|
|
218
|
-
callNumber: string;
|
|
219
|
-
image1?: string;
|
|
220
|
-
image2?: string;
|
|
221
|
-
image3?: string;
|
|
222
|
-
}): Promise<boolean> => {
|
|
223
|
-
const { cityName, callNumber, image1, image2, image3 } = params;
|
|
224
|
-
|
|
225
|
-
const update: any = {};
|
|
226
|
-
if (image1) update["externalCallFields.image1"] = image1;
|
|
227
|
-
if (image2) update["externalCallFields.image2"] = image2;
|
|
228
|
-
if (image3) update["externalCallFields.image3"] = image3;
|
|
229
|
-
|
|
230
|
-
const result = await getTicketsCollection().updateOne(
|
|
231
|
-
{
|
|
232
|
-
cityName: cityName,
|
|
233
|
-
"externalCallFields.call_number": callNumber,
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
$set: {
|
|
237
|
-
...update,
|
|
238
|
-
updatedAt: new Date(),
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
return result.matchedCount > 0;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Remove guestJwt from ticket by external call number
|
|
248
|
-
*/
|
|
249
|
-
export const clearGuestJwtByCallNumber = async (params: {
|
|
250
|
-
cityName: CityName;
|
|
251
|
-
callNumber: string;
|
|
252
|
-
}): Promise<boolean> => {
|
|
253
|
-
const { cityName, callNumber } = params;
|
|
254
|
-
|
|
255
|
-
const result = await getTicketsCollection().updateOne(
|
|
256
|
-
{ cityName, "externalCallFields.call_number": callNumber },
|
|
257
|
-
{ $unset: { guestJwt: "" }, $set: { updatedAt: new Date() } },
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
return result.matchedCount > 0;
|
|
261
|
-
};
|
|
1
|
+
import { CityName, getDb, ObjectId, Ticket } from "../index";
|
|
2
|
+
import type { SubjectStatsItem } from "./tickets.types";
|
|
3
|
+
import { Collection, Filter, ObjectId as MongoObjectId } from "mongodb";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a new ticket ID as a string
|
|
7
|
+
* @returns A new ObjectId as a string
|
|
8
|
+
*/
|
|
9
|
+
export const generateTicketId = (): string => {
|
|
10
|
+
return new ObjectId().toString();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getTicketsCollection = (): Collection<Ticket> => {
|
|
14
|
+
return getDb().collection<Ticket>("tickets");
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const findTickets = async (
|
|
18
|
+
filter: Filter<Ticket> = {},
|
|
19
|
+
): Promise<Ticket[]> => {
|
|
20
|
+
return await getTicketsCollection().find(filter).toArray();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const findTicketsByCallSid = async (
|
|
24
|
+
callSid: string,
|
|
25
|
+
): Promise<Ticket[]> => {
|
|
26
|
+
return await getTicketsCollection().find({ callSid }).toArray();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getTicketById = async (
|
|
30
|
+
ticketId: string,
|
|
31
|
+
): Promise<Ticket | null> => {
|
|
32
|
+
const ticket = await getTicketsCollection().findOne({
|
|
33
|
+
_id: new ObjectId(ticketId),
|
|
34
|
+
});
|
|
35
|
+
return ticket ? ticket : null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const createTicket = async (
|
|
39
|
+
ticketData: Omit<Ticket, "_id" | "createdAt" | "updatedAt">,
|
|
40
|
+
ticketId?: string,
|
|
41
|
+
): Promise<MongoObjectId> => {
|
|
42
|
+
const ticket: Ticket = {
|
|
43
|
+
_id: ticketId ? new ObjectId(ticketId) : new ObjectId(),
|
|
44
|
+
...ticketData,
|
|
45
|
+
createdAt: new Date(),
|
|
46
|
+
updatedAt: new Date(),
|
|
47
|
+
};
|
|
48
|
+
const { insertedId } = await getTicketsCollection().insertOne(ticket);
|
|
49
|
+
return insertedId;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const updateTicket = async (
|
|
53
|
+
ticketId: string,
|
|
54
|
+
data: Partial<Omit<Ticket, "_id" | "createdAt" | "updatedAt">>,
|
|
55
|
+
): Promise<Ticket | null> => {
|
|
56
|
+
const result = await getTicketsCollection().findOneAndUpdate(
|
|
57
|
+
{ _id: new ObjectId(ticketId) },
|
|
58
|
+
{ $set: { ...data, updatedAt: new Date() } },
|
|
59
|
+
{ returnDocument: "after" },
|
|
60
|
+
);
|
|
61
|
+
return result || null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const deleteTicket = async (ticketId: string): Promise<boolean> => {
|
|
65
|
+
const result = await getTicketsCollection().deleteOne({
|
|
66
|
+
_id: new ObjectId(ticketId),
|
|
67
|
+
});
|
|
68
|
+
return result.deletedCount > 0;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Count tickets by city and date range (createdAt converted to given timezone).
|
|
73
|
+
* Used as "open" tickets count when status is not available.
|
|
74
|
+
*/
|
|
75
|
+
export async function getTicketsCountByCityAndDateRange(
|
|
76
|
+
cityName: string,
|
|
77
|
+
startStr: string,
|
|
78
|
+
endStr: string,
|
|
79
|
+
timezone: string,
|
|
80
|
+
): Promise<number> {
|
|
81
|
+
const doc = await getTicketsCollection()
|
|
82
|
+
.aggregate<{ n: number }>([
|
|
83
|
+
{ $match: { cityName, callSid: { $exists: true, $nin: [null, ""] } } },
|
|
84
|
+
{
|
|
85
|
+
$addFields: {
|
|
86
|
+
dateLocal: {
|
|
87
|
+
$dateToString: { format: "%Y-%m-%d", date: "$createdAt", timezone },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{ $match: { dateLocal: { $gte: startStr, $lte: endStr } } },
|
|
92
|
+
{ $count: "n" },
|
|
93
|
+
])
|
|
94
|
+
.next();
|
|
95
|
+
return doc?.n ?? 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Count of tickets by department (subject) from departmentsSubjects. Different departments (subject_id) are grouped, and sub-subjects are unified under their department.
|
|
100
|
+
* Date filter: createdAt in [startStr, endStr] (in the given timezone). Fallback to "Unclassified" when no match is found in the lookup.
|
|
101
|
+
*/
|
|
102
|
+
export async function getTicketsSubjectStats(
|
|
103
|
+
cityName: string,
|
|
104
|
+
startStr: string,
|
|
105
|
+
endStr: string,
|
|
106
|
+
timezone: string,
|
|
107
|
+
): Promise<SubjectStatsItem[]> {
|
|
108
|
+
const coll = getTicketsCollection();
|
|
109
|
+
const rows = await coll
|
|
110
|
+
.aggregate<{ _id: string; subject: string; count: number }>([
|
|
111
|
+
{ $match: { cityName } },
|
|
112
|
+
{
|
|
113
|
+
$addFields: {
|
|
114
|
+
dateLocal: {
|
|
115
|
+
$dateToString: { format: "%Y-%m-%d", date: "$createdAt", timezone },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{ $match: { dateLocal: { $gte: startStr, $lte: endStr } } },
|
|
120
|
+
{
|
|
121
|
+
$addFields: {
|
|
122
|
+
effectiveSubjectId: {
|
|
123
|
+
$ifNull: [
|
|
124
|
+
"$externalCallFields.event_subject_id",
|
|
125
|
+
{
|
|
126
|
+
$ifNull: [
|
|
127
|
+
"$externalCallFields.event_sub_subject_id",
|
|
128
|
+
"$externalCallFields.event_sub_subject_id2",
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
$lookup: {
|
|
137
|
+
from: "departmentsSubjects",
|
|
138
|
+
let: { sid: "$effectiveSubjectId", c: cityName },
|
|
139
|
+
pipeline: [
|
|
140
|
+
{
|
|
141
|
+
$match: {
|
|
142
|
+
$expr: {
|
|
143
|
+
$and: [
|
|
144
|
+
{ $eq: ["$cityName", "$$c"] },
|
|
145
|
+
{
|
|
146
|
+
$or: [
|
|
147
|
+
{ $eq: ["$subject_id", { $ifNull: ["$$sid", ""] }] },
|
|
148
|
+
{
|
|
149
|
+
$eq: ["$sub_subject_id", { $ifNull: ["$$sid", ""] }],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
$eq: ["$sub_subject_id2", { $ifNull: ["$$sid", ""] }],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{ $limit: 1 },
|
|
161
|
+
],
|
|
162
|
+
as: "subj",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
$addFields: {
|
|
167
|
+
subject_id: {
|
|
168
|
+
$cond: {
|
|
169
|
+
if: { $gt: [{ $size: "$subj" }, 0] },
|
|
170
|
+
then: {
|
|
171
|
+
$ifNull: [{ $arrayElemAt: ["$subj.subject_id", 0] }, ""],
|
|
172
|
+
},
|
|
173
|
+
else: "",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
subject: {
|
|
177
|
+
$cond: {
|
|
178
|
+
if: { $gt: [{ $size: "$subj" }, 0] },
|
|
179
|
+
then: {
|
|
180
|
+
$ifNull: [
|
|
181
|
+
{ $arrayElemAt: ["$subj.subjectName", 0] },
|
|
182
|
+
"Unclassified",
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
else: "Unclassified",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
$group: {
|
|
192
|
+
_id: "$subject_id",
|
|
193
|
+
subject: { $first: "$subject" },
|
|
194
|
+
count: { $sum: 1 },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{ $sort: { count: -1 } },
|
|
198
|
+
])
|
|
199
|
+
.toArray();
|
|
200
|
+
|
|
201
|
+
const total = rows.reduce((s: number, r: any) => s + r.count, 0);
|
|
202
|
+
return rows.map((r: any) => ({
|
|
203
|
+
subject_name: r.subject,
|
|
204
|
+
subject_id: r._id,
|
|
205
|
+
count: r.count,
|
|
206
|
+
percentage: total > 0 ? Math.round((r.count / total) * 100) : 0,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const findTicketByQuery = async (query: Partial<Ticket>) => {
|
|
211
|
+
return await getTicketsCollection().findOne(query);
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Update ticket files by external call number
|
|
215
|
+
*/
|
|
216
|
+
export const updateTicketFilesByCallNumber = async (params: {
|
|
217
|
+
cityName: CityName;
|
|
218
|
+
callNumber: string;
|
|
219
|
+
image1?: string;
|
|
220
|
+
image2?: string;
|
|
221
|
+
image3?: string;
|
|
222
|
+
}): Promise<boolean> => {
|
|
223
|
+
const { cityName, callNumber, image1, image2, image3 } = params;
|
|
224
|
+
|
|
225
|
+
const update: any = {};
|
|
226
|
+
if (image1) update["externalCallFields.image1"] = image1;
|
|
227
|
+
if (image2) update["externalCallFields.image2"] = image2;
|
|
228
|
+
if (image3) update["externalCallFields.image3"] = image3;
|
|
229
|
+
|
|
230
|
+
const result = await getTicketsCollection().updateOne(
|
|
231
|
+
{
|
|
232
|
+
cityName: cityName,
|
|
233
|
+
"externalCallFields.call_number": callNumber,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
$set: {
|
|
237
|
+
...update,
|
|
238
|
+
updatedAt: new Date(),
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return result.matchedCount > 0;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Remove guestJwt from ticket by external call number
|
|
248
|
+
*/
|
|
249
|
+
export const clearGuestJwtByCallNumber = async (params: {
|
|
250
|
+
cityName: CityName;
|
|
251
|
+
callNumber: string;
|
|
252
|
+
}): Promise<boolean> => {
|
|
253
|
+
const { cityName, callNumber } = params;
|
|
254
|
+
|
|
255
|
+
const result = await getTicketsCollection().updateOne(
|
|
256
|
+
{ cityName, "externalCallFields.call_number": callNumber },
|
|
257
|
+
{ $unset: { guestJwt: "" }, $set: { updatedAt: new Date() } },
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
return result.matchedCount > 0;
|
|
261
|
+
};
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import { ObjectId, WithId } from "mongodb";
|
|
2
|
-
|
|
3
|
-
import { CityName } from "../utils/types";
|
|
4
|
-
|
|
5
|
-
export type Ticket = {
|
|
6
|
-
_id: ObjectId;
|
|
7
|
-
createdAt: Date;
|
|
8
|
-
updatedAt: Date;
|
|
9
|
-
callSid?: string;
|
|
10
|
-
cityName: CityName;
|
|
11
|
-
externalCallFields: {
|
|
12
|
-
// Request fields
|
|
13
|
-
first_name?: string;
|
|
14
|
-
last_name?: string;
|
|
15
|
-
event_description?: string;
|
|
16
|
-
street?: string;
|
|
17
|
-
house_number?: string;
|
|
18
|
-
event_subject_id?: string;
|
|
19
|
-
event_sub_subject_id?: string;
|
|
20
|
-
event_sub_subject_id2?: string | null;
|
|
21
|
-
// Response fields (only if external call was successful)
|
|
22
|
-
call_number?: string;
|
|
23
|
-
status?: number; // Response status from Bina API (1 = success)
|
|
24
|
-
error?: string; // Error message from Bina API if any
|
|
25
|
-
image1?: string;
|
|
26
|
-
image2?: string;
|
|
27
|
-
image3?: string;
|
|
28
|
-
file1?: string;
|
|
29
|
-
file2?: string;
|
|
30
|
-
file3?: string;
|
|
31
|
-
};
|
|
32
|
-
guestJwt?: string;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type TicketDoc = WithId<Ticket>;
|
|
36
|
-
|
|
37
|
-
/** Call count by department (subject). */
|
|
38
|
-
export type SubjectStatsItem = {
|
|
39
|
-
subject_name: string;
|
|
40
|
-
subject_id: string;
|
|
41
|
-
count: number;
|
|
42
|
-
percentage: number;
|
|
43
|
-
};
|
|
1
|
+
import { ObjectId, WithId } from "mongodb";
|
|
2
|
+
|
|
3
|
+
import { CityName } from "../utils/types";
|
|
4
|
+
|
|
5
|
+
export type Ticket = {
|
|
6
|
+
_id: ObjectId;
|
|
7
|
+
createdAt: Date;
|
|
8
|
+
updatedAt: Date;
|
|
9
|
+
callSid?: string;
|
|
10
|
+
cityName: CityName;
|
|
11
|
+
externalCallFields: {
|
|
12
|
+
// Request fields
|
|
13
|
+
first_name?: string;
|
|
14
|
+
last_name?: string;
|
|
15
|
+
event_description?: string;
|
|
16
|
+
street?: string;
|
|
17
|
+
house_number?: string;
|
|
18
|
+
event_subject_id?: string;
|
|
19
|
+
event_sub_subject_id?: string;
|
|
20
|
+
event_sub_subject_id2?: string | null;
|
|
21
|
+
// Response fields (only if external call was successful)
|
|
22
|
+
call_number?: string;
|
|
23
|
+
status?: number; // Response status from Bina API (1 = success)
|
|
24
|
+
error?: string; // Error message from Bina API if any
|
|
25
|
+
image1?: string;
|
|
26
|
+
image2?: string;
|
|
27
|
+
image3?: string;
|
|
28
|
+
file1?: string;
|
|
29
|
+
file2?: string;
|
|
30
|
+
file3?: string;
|
|
31
|
+
};
|
|
32
|
+
guestJwt?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type TicketDoc = WithId<Ticket>;
|
|
36
|
+
|
|
37
|
+
/** Call count by department (subject). */
|
|
38
|
+
export type SubjectStatsItem = {
|
|
39
|
+
subject_name: string;
|
|
40
|
+
subject_id: string;
|
|
41
|
+
count: number;
|
|
42
|
+
percentage: number;
|
|
43
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* City name type for municipal data
|
|
3
|
-
*/
|
|
4
|
-
export type CityName =
|
|
5
|
-
| "ashdod"
|
|
6
|
-
| "maltar"
|
|
7
|
-
| "billit"
|
|
8
|
-
| "hashkelon"
|
|
9
|
-
| "eilat"
|
|
10
|
-
| "tests"
|
|
11
|
-
| string;
|
|
1
|
+
/**
|
|
2
|
+
* City name type for municipal data
|
|
3
|
+
*/
|
|
4
|
+
export type CityName =
|
|
5
|
+
| "ashdod"
|
|
6
|
+
| "maltar"
|
|
7
|
+
| "billit"
|
|
8
|
+
| "hashkelon"
|
|
9
|
+
| "eilat"
|
|
10
|
+
| "tests"
|
|
11
|
+
| string;
|