@consilioweb/payload-support 0.16.0 → 1.1.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/README.md +357 -435
- package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
- package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
- package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
- package/dist/components/TicketConversation/SkeletonText.js +20 -0
- package/dist/components/TicketConversation/constants.cjs +27 -0
- package/dist/components/TicketConversation/constants.js +26 -1
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
- package/dist/components/TicketConversation/index.cjs +10 -147
- package/dist/components/TicketConversation/index.js +4 -141
- package/dist/index.cjs +1763 -677
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1760 -677
- package/dist/views/BillingView/client.cjs +40 -54
- package/dist/views/BillingView/client.js +40 -54
- package/dist/views/ChatView/client.cjs +4 -3
- package/dist/views/ChatView/client.js +4 -3
- package/dist/views/CrmView/client.cjs +7 -7
- package/dist/views/CrmView/client.js +7 -7
- package/dist/views/EmailTrackingView/client.cjs +6 -5
- package/dist/views/EmailTrackingView/client.js +6 -5
- package/dist/views/ImportConversationView/client.cjs +4 -4
- package/dist/views/ImportConversationView/client.js +4 -4
- package/dist/views/LogsView/client.cjs +3 -2
- package/dist/views/LogsView/client.js +3 -2
- package/dist/views/NewTicketView/client.cjs +1 -1
- package/dist/views/NewTicketView/client.js +1 -1
- package/dist/views/PendingEmailsView/client.cjs +7 -6
- package/dist/views/PendingEmailsView/client.js +7 -6
- package/dist/views/SupportDashboardView/client.cjs +19 -22
- package/dist/views/SupportDashboardView/client.js +19 -22
- package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
- package/dist/views/TicketDetailView/NextActionItem.js +34 -0
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
- package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
- package/dist/views/TicketDetailView/client.cjs +59 -190
- package/dist/views/TicketDetailView/client.js +52 -183
- package/dist/views/TicketDetailView/constants.cjs +20 -0
- package/dist/views/TicketDetailView/constants.js +16 -0
- package/dist/views/TicketDetailView/helpers.cjs +23 -0
- package/dist/views/TicketDetailView/helpers.js +20 -0
- package/dist/views/TicketDetailView/types.cjs +2 -0
- package/dist/views/TicketDetailView/types.js +1 -0
- package/dist/views/TicketInboxView/client.cjs +14 -10
- package/dist/views/TicketInboxView/client.js +14 -10
- package/dist/views/TicketingSettingsView/client.cjs +101 -96
- package/dist/views/TicketingSettingsView/client.js +92 -88
- package/dist/views/TimeDashboardView/client.cjs +2 -2
- package/dist/views/TimeDashboardView/client.js +2 -2
- package/dist/views/shared/StatusPill.cjs +23 -12
- package/dist/views/shared/StatusPill.js +23 -12
- package/dist/views/shared/dateLocale.cjs +5 -0
- package/dist/views/shared/dateLocale.js +3 -0
- package/package.json +28 -15
- package/src/__tests__/generateTrackingToken.test.ts +53 -0
- package/src/__tests__/integration/ai-agent.test.ts +46 -0
- package/src/__tests__/integration/auth.test.ts +43 -0
- package/src/__tests__/integration/automation-rules.test.ts +68 -0
- package/src/__tests__/integration/buildTestPayload.ts +52 -0
- package/src/__tests__/integration/channels.test.ts +52 -0
- package/src/__tests__/integration/core-behaviors.test.ts +58 -0
- package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
- package/src/__tests__/integration/digest.test.ts +48 -0
- package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
- package/src/__tests__/integration/isolation.test.ts +81 -0
- package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
- package/src/__tests__/integration/nps.test.ts +37 -0
- package/src/__tests__/integration/per-team-sla.test.ts +44 -0
- package/src/__tests__/integration/push.test.ts +75 -0
- package/src/__tests__/integration/sanitization.test.ts +35 -0
- package/src/__tests__/integration/sla-pause.test.ts +39 -0
- package/src/__tests__/integration/smoke.test.ts +26 -0
- package/src/__tests__/integration/snooze.test.ts +68 -0
- package/src/__tests__/integration/teams.test.ts +38 -0
- package/src/__tests__/rateLimiter.test.ts +89 -0
- package/src/__tests__/sanitizeHtml.test.ts +165 -0
- package/src/__tests__/sla.test.ts +258 -0
- package/src/collections/AutomationRules.ts +66 -0
- package/src/collections/NotificationQueue.ts +30 -0
- package/src/collections/PushSubscription.ts +28 -0
- package/src/collections/SatisfactionSurveys.ts +11 -2
- package/src/collections/SlaPolicies.ts +10 -0
- package/src/collections/SupportClients.ts +58 -1
- package/src/collections/SupportTeam.ts +27 -0
- package/src/collections/TicketActivityLog.ts +9 -4
- package/src/collections/TicketMessages.ts +140 -31
- package/src/collections/Tickets.ts +31 -73
- package/src/collections/index.ts +4 -0
- package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
- package/src/components/TicketConversation/SkeletonText.tsx +23 -0
- package/src/components/TicketConversation/constants.ts +29 -0
- package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
- package/src/components/TicketConversation/index.tsx +4 -152
- package/src/endpoints/admin-chat-stream.ts +3 -4
- package/src/endpoints/admin-chat.ts +11 -20
- package/src/endpoints/admin-stats.ts +77 -35
- package/src/endpoints/ai-agent.ts +36 -0
- package/src/endpoints/apply-macro.ts +8 -14
- package/src/endpoints/auth-2fa.ts +12 -12
- package/src/endpoints/auto-close.ts +6 -10
- package/src/endpoints/billing.ts +17 -4
- package/src/endpoints/bulk-action.ts +7 -12
- package/src/endpoints/channels.ts +89 -0
- package/src/endpoints/chat-stream.ts +3 -4
- package/src/endpoints/chat.ts +7 -12
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/client-intelligence.ts +4 -4
- package/src/endpoints/delete-account.ts +9 -16
- package/src/endpoints/email-stats.ts +2 -2
- package/src/endpoints/escalate.ts +2 -2
- package/src/endpoints/export-csv.ts +2 -2
- package/src/endpoints/export-data.ts +5 -8
- package/src/endpoints/import-conversation.ts +6 -10
- package/src/endpoints/index.ts +22 -1
- package/src/endpoints/invite-collaborator.ts +30 -15
- package/src/endpoints/invoice.ts +127 -0
- package/src/endpoints/kb-search.ts +2 -2
- package/src/endpoints/login.ts +12 -4
- package/src/endpoints/merge-clients.ts +14 -19
- package/src/endpoints/merge-tickets.ts +11 -18
- package/src/endpoints/oauth-google.ts +86 -39
- package/src/endpoints/pending-emails-process.ts +15 -25
- package/src/endpoints/process-digests.ts +93 -0
- package/src/endpoints/process-scheduled.ts +6 -10
- package/src/endpoints/process-snooze.ts +78 -0
- package/src/endpoints/purge-logs.ts +2 -2
- package/src/endpoints/push.ts +49 -0
- package/src/endpoints/resend-notification.ts +3 -4
- package/src/endpoints/round-robin-config.ts +3 -4
- package/src/endpoints/satisfaction.ts +21 -14
- package/src/endpoints/search.ts +5 -8
- package/src/endpoints/seed-kb.ts +3 -4
- package/src/endpoints/send-reminder.ts +6 -9
- package/src/endpoints/settings.ts +5 -2
- package/src/endpoints/signature.ts +3 -4
- package/src/endpoints/sla-check.ts +2 -2
- package/src/endpoints/split-ticket.ts +7 -12
- package/src/endpoints/statuses.ts +2 -2
- package/src/endpoints/ticket-feedback.ts +4 -6
- package/src/endpoints/ticket-synthesis.ts +2 -2
- package/src/endpoints/track-open.ts +16 -28
- package/src/endpoints/transfer-ticket.ts +21 -9
- package/src/endpoints/user-prefs.ts +3 -4
- package/src/hooks/automationRules.ts +84 -0
- package/src/hooks/checkSLA.ts +63 -17
- package/src/plugin.ts +8 -0
- package/src/portal/LiveChat.tsx +2 -63
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
- package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
- package/src/portal/auth/tickets/detail/page.tsx +2 -2
- package/src/portal/helpers.ts +46 -0
- package/src/portal/login/page.tsx +3 -8
- package/src/portal/types.ts +15 -0
- package/src/types/lucide-react.d.ts +34 -0
- package/src/types/payload-config.d.ts +10 -0
- package/src/utils/adminNotification.ts +2 -2
- package/src/utils/aiAgent.ts +147 -0
- package/src/utils/auth.ts +5 -4
- package/src/utils/channels.ts +36 -0
- package/src/utils/db.ts +73 -0
- package/src/utils/emailTemplate.ts +8 -1
- package/src/utils/fireWebhooks.ts +4 -6
- package/src/utils/generateTicketSynthesis.ts +7 -12
- package/src/utils/invoicePdf.ts +88 -0
- package/src/utils/notificationQueue.ts +36 -0
- package/src/utils/push.ts +63 -0
- package/src/utils/readSettings.ts +22 -6
- package/src/utils/sanitizeHtml.ts +57 -0
- package/src/utils/slugs.ts +8 -0
- package/src/utils/teamAccess.ts +27 -0
- package/src/utils/ticketAccess.ts +53 -0
- package/src/utils/webhookDispatcher.ts +4 -6
- package/src/views/BillingView/client.tsx +49 -37
- package/src/views/ChatView/client.tsx +4 -3
- package/src/views/CrmView/client.tsx +7 -7
- package/src/views/EmailTrackingView/client.tsx +7 -6
- package/src/views/ImportConversationView/client.tsx +4 -4
- package/src/views/LogsView/client.tsx +3 -2
- package/src/views/NewTicketView/client.tsx +1 -1
- package/src/views/PendingEmailsView/client.tsx +7 -6
- package/src/views/SupportDashboardView/client.tsx +21 -21
- package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
- package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
- package/src/views/TicketDetailView/client.tsx +54 -179
- package/src/views/TicketDetailView/constants.ts +23 -0
- package/src/views/TicketDetailView/helpers.ts +17 -0
- package/src/views/TicketDetailView/types.ts +10 -0
- package/src/views/TicketInboxView/client.tsx +16 -10
- package/src/views/TicketingSettingsView/client.tsx +90 -87
- package/src/views/TimeDashboardView/client.tsx +2 -2
- package/src/views/shared/StatusPill.tsx +24 -12
- package/src/views/shared/dateLocale.ts +5 -0
- package/src/views/shared/locales/en.json +708 -0
- package/src/views/shared/locales/fr.json +708 -0
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var crypto3 = require('crypto');
|
|
4
|
+
var PDFDocument = require('pdfkit');
|
|
5
|
+
var payload = require('payload');
|
|
6
|
+
var webpush = require('web-push');
|
|
7
|
+
var sanitizeHtml = require('sanitize-html');
|
|
4
8
|
|
|
5
9
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
10
|
|
|
7
11
|
var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
|
|
12
|
+
var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
|
|
13
|
+
var webpush__default = /*#__PURE__*/_interopDefault(webpush);
|
|
14
|
+
var sanitizeHtml__default = /*#__PURE__*/_interopDefault(sanitizeHtml);
|
|
8
15
|
|
|
9
16
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
10
17
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -64,6 +71,10 @@ var DEFAULT_SLUGS = {
|
|
|
64
71
|
macros: "macros",
|
|
65
72
|
ticketStatuses: "ticket-statuses",
|
|
66
73
|
ticketFeedback: "ticket-feedback",
|
|
74
|
+
notificationQueue: "notification-queue",
|
|
75
|
+
automationRules: "automation-rules",
|
|
76
|
+
supportTeams: "support-teams",
|
|
77
|
+
pushSubscriptions: "push-subscriptions",
|
|
67
78
|
users: "users",
|
|
68
79
|
media: "media"
|
|
69
80
|
};
|
|
@@ -95,6 +106,26 @@ function handleAuthError(error) {
|
|
|
95
106
|
return null;
|
|
96
107
|
}
|
|
97
108
|
|
|
109
|
+
// src/utils/db.ts
|
|
110
|
+
function dbFind(payload, slug, options = {}) {
|
|
111
|
+
return payload.find({ collection: slug, ...options });
|
|
112
|
+
}
|
|
113
|
+
function dbFindByID(payload, slug, options) {
|
|
114
|
+
return payload.findByID({ collection: slug, ...options });
|
|
115
|
+
}
|
|
116
|
+
function dbCreate(payload, slug, options) {
|
|
117
|
+
return payload.create({ collection: slug, ...options });
|
|
118
|
+
}
|
|
119
|
+
function dbUpdate(payload, slug, options) {
|
|
120
|
+
return payload.update({ collection: slug, ...options });
|
|
121
|
+
}
|
|
122
|
+
function dbCount(payload, slug, options = {}) {
|
|
123
|
+
return payload.count({ collection: slug, ...options });
|
|
124
|
+
}
|
|
125
|
+
function dbDelete(payload, slug, options) {
|
|
126
|
+
return payload.delete({ collection: slug, ...options });
|
|
127
|
+
}
|
|
128
|
+
|
|
98
129
|
// src/utils/readSettings.ts
|
|
99
130
|
var PREF_KEY = "support-settings";
|
|
100
131
|
var USER_PREFS_KEY_PREFIX = "support-user-prefs";
|
|
@@ -108,10 +139,18 @@ var DEFAULT_USER_PREFS = {
|
|
|
108
139
|
locale: "fr",
|
|
109
140
|
signature: ""
|
|
110
141
|
};
|
|
142
|
+
var settingsCache = null;
|
|
143
|
+
var SETTINGS_TTL_MS = 6e4;
|
|
144
|
+
function invalidateSupportSettingsCache() {
|
|
145
|
+
settingsCache = null;
|
|
146
|
+
}
|
|
111
147
|
async function readSupportSettings(payload) {
|
|
148
|
+
if (settingsCache && Date.now() - settingsCache.ts < SETTINGS_TTL_MS) {
|
|
149
|
+
return settingsCache.value;
|
|
150
|
+
}
|
|
151
|
+
let value = { ...DEFAULT_SETTINGS };
|
|
112
152
|
try {
|
|
113
|
-
const prefs = await payload
|
|
114
|
-
collection: "payload-preferences",
|
|
153
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
115
154
|
where: { key: { equals: PREF_KEY } },
|
|
116
155
|
limit: 1,
|
|
117
156
|
depth: 0,
|
|
@@ -119,7 +158,7 @@ async function readSupportSettings(payload) {
|
|
|
119
158
|
});
|
|
120
159
|
if (prefs.docs.length > 0) {
|
|
121
160
|
const stored = prefs.docs[0].value;
|
|
122
|
-
|
|
161
|
+
value = {
|
|
123
162
|
email: { ...DEFAULT_SETTINGS.email, ...stored.email },
|
|
124
163
|
ai: { ...DEFAULT_SETTINGS.ai, ...stored.ai },
|
|
125
164
|
sla: { ...DEFAULT_SETTINGS.sla, ...stored.sla },
|
|
@@ -128,13 +167,13 @@ async function readSupportSettings(payload) {
|
|
|
128
167
|
}
|
|
129
168
|
} catch {
|
|
130
169
|
}
|
|
131
|
-
|
|
170
|
+
settingsCache = { value, ts: Date.now() };
|
|
171
|
+
return value;
|
|
132
172
|
}
|
|
133
173
|
async function readUserPrefs(payload, userId) {
|
|
134
174
|
try {
|
|
135
175
|
const key = `${USER_PREFS_KEY_PREFIX}-${userId}`;
|
|
136
|
-
const prefs = await payload
|
|
137
|
-
collection: "payload-preferences",
|
|
176
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
138
177
|
where: { key: { equals: key } },
|
|
139
178
|
limit: 1,
|
|
140
179
|
depth: 0,
|
|
@@ -320,8 +359,130 @@ ${text}`;
|
|
|
320
359
|
};
|
|
321
360
|
}
|
|
322
361
|
|
|
323
|
-
// src/
|
|
362
|
+
// src/utils/aiAgent.ts
|
|
363
|
+
async function applyAgentDecision(payload, slugs, ticketId, decision) {
|
|
364
|
+
if (decision.action === "reply" && decision.reply && decision.reply.trim()) {
|
|
365
|
+
const msg = await dbCreate(payload, slugs.ticketMessages, {
|
|
366
|
+
data: {
|
|
367
|
+
ticket: ticketId,
|
|
368
|
+
body: decision.reply.trim(),
|
|
369
|
+
authorType: "admin",
|
|
370
|
+
isInternal: false,
|
|
371
|
+
fromAlias: "Assistant IA"
|
|
372
|
+
},
|
|
373
|
+
overrideAccess: true
|
|
374
|
+
});
|
|
375
|
+
return { acted: "reply", messageId: msg.id, confidence: decision.confidence };
|
|
376
|
+
}
|
|
377
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
378
|
+
data: {
|
|
379
|
+
ticket: ticketId,
|
|
380
|
+
body: `[Agent IA] Escalade vers un humain. Raison : ${decision.reason || "non d\xE9termin\xE9e"} (confiance ${Math.round((decision.confidence ?? 0) * 100)} %).`,
|
|
381
|
+
authorType: "admin",
|
|
382
|
+
isInternal: true,
|
|
383
|
+
skipNotification: true
|
|
384
|
+
},
|
|
385
|
+
overrideAccess: true
|
|
386
|
+
});
|
|
387
|
+
await dbUpdate(payload, slugs.tickets, { id: ticketId, data: { status: "escalated" }, overrideAccess: true });
|
|
388
|
+
return { acted: "escalate", confidence: decision.confidence };
|
|
389
|
+
}
|
|
324
390
|
function getClient2(aiSettings) {
|
|
391
|
+
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
392
|
+
if (aiSettings.provider === "ollama") {
|
|
393
|
+
return new Anthropic({ apiKey: "ollama", baseURL: process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1" });
|
|
394
|
+
}
|
|
395
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
396
|
+
}
|
|
397
|
+
function stripHtml(s) {
|
|
398
|
+
return (s || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
399
|
+
}
|
|
400
|
+
function parseDecision(text) {
|
|
401
|
+
try {
|
|
402
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
403
|
+
if (match) {
|
|
404
|
+
const obj = JSON.parse(match[0]);
|
|
405
|
+
return {
|
|
406
|
+
action: obj.action === "reply" ? "reply" : "escalate",
|
|
407
|
+
reply: typeof obj.reply === "string" ? obj.reply : void 0,
|
|
408
|
+
reason: typeof obj.reason === "string" ? obj.reason : void 0,
|
|
409
|
+
confidence: typeof obj.confidence === "number" ? obj.confidence : 0
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
return { action: "escalate", reason: "R\xE9ponse IA non interpr\xE9table", confidence: 0 };
|
|
415
|
+
}
|
|
416
|
+
async function runAiAgent(payload, slugs, ticketId, opts) {
|
|
417
|
+
const settings = await readSupportSettings(payload);
|
|
418
|
+
const threshold = opts?.confidenceThreshold ?? 0.7;
|
|
419
|
+
const ticket = await dbFindByID(payload, slugs.tickets, { id: ticketId, depth: 0, overrideAccess: true });
|
|
420
|
+
const messages = await dbFind(payload, slugs.ticketMessages, { where: { ticket: { equals: ticketId } }, sort: "createdAt", limit: 50, depth: 0, overrideAccess: true });
|
|
421
|
+
const kb = await dbFind(payload, slugs.knowledgeBase, { where: { published: { equals: true } }, limit: 20, depth: 0, overrideAccess: true });
|
|
422
|
+
const conversation = messages.docs.map((m) => `${m.authorType}: ${m.body || ""}`).join("\n");
|
|
423
|
+
const kbText = kb.docs.map((a) => `# ${a.title || ""}
|
|
424
|
+
${stripHtml(String(a.content ?? ""))}`).join("\n\n").slice(0, 8e3);
|
|
425
|
+
const prompt = `Tu es un agent de support IA. \xC0 partir du ticket et de la base de connaissances ci-dessous, d\xE9cide :
|
|
426
|
+
- "reply" si tu peux r\xE9pondre avec certitude UNIQUEMENT \xE0 partir de la base de connaissances ;
|
|
427
|
+
- "escalate" sinon (question hors KB, ambigu\xEB, sensible, ou n\xE9cessitant une action humaine).
|
|
428
|
+
R\xE9ponds en JSON STRICT, sans texte autour : {"action":"reply"|"escalate","reply":"<r\xE9ponse au client si reply>","reason":"<courte raison>","confidence":<0 \xE0 1>}.
|
|
429
|
+
|
|
430
|
+
TICKET : ${ticket.subject || ""}
|
|
431
|
+
CONVERSATION :
|
|
432
|
+
${conversation}
|
|
433
|
+
|
|
434
|
+
BASE DE CONNAISSANCES :
|
|
435
|
+
${kbText || "(vide)"}`;
|
|
436
|
+
let decision;
|
|
437
|
+
try {
|
|
438
|
+
const client = getClient2(settings.ai);
|
|
439
|
+
const resp = await client.messages.create({
|
|
440
|
+
model: settings.ai.model || "claude-haiku-4-5-20251001",
|
|
441
|
+
max_tokens: 1024,
|
|
442
|
+
messages: [{ role: "user", content: prompt }]
|
|
443
|
+
});
|
|
444
|
+
const text = resp?.content?.[0]?.text || "";
|
|
445
|
+
decision = parseDecision(text);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
console.error("[support] AI agent decision failed:", err);
|
|
448
|
+
decision = { action: "escalate", reason: "Erreur du fournisseur IA", confidence: 0 };
|
|
449
|
+
}
|
|
450
|
+
const finalDecision = decision.action === "reply" && (decision.confidence ?? 0) >= threshold ? decision : { action: "escalate", reason: decision.reason || "Confiance insuffisante pour une r\xE9ponse automatique", confidence: decision.confidence };
|
|
451
|
+
return applyAgentDecision(payload, slugs, ticketId, finalDecision);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/endpoints/ai-agent.ts
|
|
455
|
+
function createAiAgentEndpoint(slugs) {
|
|
456
|
+
return {
|
|
457
|
+
path: "/support/ai-agent",
|
|
458
|
+
method: "post",
|
|
459
|
+
handler: async (req) => {
|
|
460
|
+
try {
|
|
461
|
+
requireAdmin(req, slugs);
|
|
462
|
+
let body = {};
|
|
463
|
+
try {
|
|
464
|
+
body = await req.json();
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
if (!body.ticketId) {
|
|
468
|
+
return Response.json({ error: "ticketId requis." }, { status: 400 });
|
|
469
|
+
}
|
|
470
|
+
const result = await runAiAgent(req.payload, slugs, body.ticketId, {
|
|
471
|
+
confidenceThreshold: body.confidenceThreshold
|
|
472
|
+
});
|
|
473
|
+
return Response.json({ ok: true, ...result });
|
|
474
|
+
} catch (error) {
|
|
475
|
+
const authResponse = handleAuthError(error);
|
|
476
|
+
if (authResponse) return authResponse;
|
|
477
|
+
console.error("[ai-agent] Error:", error);
|
|
478
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/endpoints/client-intelligence.ts
|
|
485
|
+
function getClient3(aiSettings) {
|
|
325
486
|
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
326
487
|
if (aiSettings.provider === "ollama") {
|
|
327
488
|
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
@@ -475,7 +636,7 @@ R\xE9ponds en JSON strict (pas de markdown, pas de commentaires) avec cette stru
|
|
|
475
636
|
}
|
|
476
637
|
|
|
477
638
|
Sois factuel. Ne d\xE9passe pas 5 items par tableau. R\xE9ponds UNIQUEMENT avec le JSON.`;
|
|
478
|
-
const anthropic =
|
|
639
|
+
const anthropic = getClient3(aiSettings);
|
|
479
640
|
const model = getModel2(aiSettings);
|
|
480
641
|
const res = await anthropic.messages.create({
|
|
481
642
|
model,
|
|
@@ -539,8 +700,7 @@ function createSearchEndpoint(slugs) {
|
|
|
539
700
|
return Response.json({ tickets: [], clients: [], messages: [], articles: [] });
|
|
540
701
|
}
|
|
541
702
|
const [ticketsRes, clientsRes, messagesRes, articlesRes] = await Promise.all([
|
|
542
|
-
payload.
|
|
543
|
-
collection: slugs.tickets,
|
|
703
|
+
dbFind(payload, slugs.tickets, {
|
|
544
704
|
where: {
|
|
545
705
|
or: [
|
|
546
706
|
{ ticketNumber: { contains: q } },
|
|
@@ -552,8 +712,7 @@ function createSearchEndpoint(slugs) {
|
|
|
552
712
|
depth: 1,
|
|
553
713
|
overrideAccess: true
|
|
554
714
|
}),
|
|
555
|
-
payload.
|
|
556
|
-
collection: slugs.supportClients,
|
|
715
|
+
dbFind(payload, slugs.supportClients, {
|
|
557
716
|
where: {
|
|
558
717
|
or: [
|
|
559
718
|
{ firstName: { contains: q } },
|
|
@@ -566,16 +725,14 @@ function createSearchEndpoint(slugs) {
|
|
|
566
725
|
depth: 0,
|
|
567
726
|
overrideAccess: true
|
|
568
727
|
}),
|
|
569
|
-
payload.
|
|
570
|
-
collection: slugs.ticketMessages,
|
|
728
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
571
729
|
where: { body: { contains: q } },
|
|
572
730
|
sort: "-createdAt",
|
|
573
731
|
limit: 5,
|
|
574
732
|
depth: 1,
|
|
575
733
|
overrideAccess: true
|
|
576
734
|
}),
|
|
577
|
-
payload.
|
|
578
|
-
collection: slugs.knowledgeBase,
|
|
735
|
+
dbFind(payload, slugs.knowledgeBase, {
|
|
579
736
|
where: { title: { contains: q } },
|
|
580
737
|
limit: 5,
|
|
581
738
|
depth: 0,
|
|
@@ -675,8 +832,7 @@ function createKbSearchEndpoint(slugs) {
|
|
|
675
832
|
orClauses.push({ category: { contains: t } });
|
|
676
833
|
}
|
|
677
834
|
orClauses.push({ title: { contains: q } });
|
|
678
|
-
const res = await payload.
|
|
679
|
-
collection: slugs.knowledgeBase,
|
|
835
|
+
const res = await dbFind(payload, slugs.knowledgeBase, {
|
|
680
836
|
where: {
|
|
681
837
|
and: [
|
|
682
838
|
{ published: { equals: true } },
|
|
@@ -735,16 +891,14 @@ function createBulkActionEndpoint(slugs) {
|
|
|
735
891
|
try {
|
|
736
892
|
switch (action) {
|
|
737
893
|
case "close":
|
|
738
|
-
await payload.
|
|
739
|
-
collection: slugs.tickets,
|
|
894
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
740
895
|
id: ticketId,
|
|
741
896
|
data: { status: "resolved", resolvedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
742
897
|
overrideAccess: true
|
|
743
898
|
});
|
|
744
899
|
break;
|
|
745
900
|
case "reopen":
|
|
746
|
-
await payload.
|
|
747
|
-
collection: slugs.tickets,
|
|
901
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
748
902
|
id: ticketId,
|
|
749
903
|
data: { status: "open" },
|
|
750
904
|
overrideAccess: true
|
|
@@ -752,8 +906,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
752
906
|
break;
|
|
753
907
|
case "assign":
|
|
754
908
|
if (value) {
|
|
755
|
-
await payload.
|
|
756
|
-
collection: slugs.tickets,
|
|
909
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
757
910
|
id: ticketId,
|
|
758
911
|
data: { assignedTo: Number(value) },
|
|
759
912
|
overrideAccess: true
|
|
@@ -762,8 +915,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
762
915
|
break;
|
|
763
916
|
case "set_priority":
|
|
764
917
|
if (value) {
|
|
765
|
-
await payload.
|
|
766
|
-
collection: slugs.tickets,
|
|
918
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
767
919
|
id: ticketId,
|
|
768
920
|
data: { priority: String(value) },
|
|
769
921
|
overrideAccess: true
|
|
@@ -772,8 +924,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
772
924
|
break;
|
|
773
925
|
case "set_category":
|
|
774
926
|
if (value) {
|
|
775
|
-
await payload.
|
|
776
|
-
collection: slugs.tickets,
|
|
927
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
777
928
|
id: ticketId,
|
|
778
929
|
data: { category: String(value) },
|
|
779
930
|
overrideAccess: true
|
|
@@ -781,8 +932,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
781
932
|
}
|
|
782
933
|
break;
|
|
783
934
|
case "delete":
|
|
784
|
-
await payload.
|
|
785
|
-
collection: slugs.tickets,
|
|
935
|
+
await dbDelete(payload, slugs.tickets, {
|
|
786
936
|
id: ticketId,
|
|
787
937
|
overrideAccess: true
|
|
788
938
|
});
|
|
@@ -821,8 +971,8 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
821
971
|
return Response.json({ error: "Impossible de fusionner un ticket avec lui-m\xEAme" }, { status: 400 });
|
|
822
972
|
}
|
|
823
973
|
const [source, target] = await Promise.all([
|
|
824
|
-
payload
|
|
825
|
-
payload
|
|
974
|
+
dbFindByID(payload, slugs.tickets, { id: sourceTicketId, depth: 0, overrideAccess: true }),
|
|
975
|
+
dbFindByID(payload, slugs.tickets, { id: targetTicketId, depth: 0, overrideAccess: true })
|
|
826
976
|
]);
|
|
827
977
|
if (!source) return Response.json({ error: "Ticket source introuvable" }, { status: 404 });
|
|
828
978
|
if (!target) return Response.json({ error: "Ticket cible introuvable" }, { status: 404 });
|
|
@@ -831,39 +981,34 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
831
981
|
if (sourceClient !== targetClient) {
|
|
832
982
|
return Response.json({ error: "Les deux tickets doivent appartenir au m\xEAme client" }, { status: 400 });
|
|
833
983
|
}
|
|
834
|
-
const messages = await payload.
|
|
835
|
-
collection: slugs.ticketMessages,
|
|
984
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
836
985
|
where: { ticket: { equals: sourceTicketId } },
|
|
837
986
|
limit: 500,
|
|
838
987
|
depth: 0,
|
|
839
988
|
overrideAccess: true
|
|
840
989
|
});
|
|
841
990
|
for (const msg of messages.docs) {
|
|
842
|
-
await payload.
|
|
843
|
-
collection: slugs.ticketMessages,
|
|
991
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
844
992
|
id: msg.id,
|
|
845
993
|
data: { ticket: targetTicketId },
|
|
846
994
|
overrideAccess: true
|
|
847
995
|
});
|
|
848
996
|
}
|
|
849
|
-
const timeEntries = await payload.
|
|
850
|
-
collection: slugs.timeEntries,
|
|
997
|
+
const timeEntries = await dbFind(payload, slugs.timeEntries, {
|
|
851
998
|
where: { ticket: { equals: sourceTicketId } },
|
|
852
999
|
limit: 500,
|
|
853
1000
|
depth: 0,
|
|
854
1001
|
overrideAccess: true
|
|
855
1002
|
});
|
|
856
1003
|
for (const entry of timeEntries.docs) {
|
|
857
|
-
await payload.
|
|
858
|
-
collection: slugs.timeEntries,
|
|
1004
|
+
await dbUpdate(payload, slugs.timeEntries, {
|
|
859
1005
|
id: entry.id,
|
|
860
1006
|
data: { ticket: targetTicketId },
|
|
861
1007
|
overrideAccess: true
|
|
862
1008
|
});
|
|
863
1009
|
}
|
|
864
1010
|
const sourceNumber = source.ticketNumber || `#${sourceTicketId}`;
|
|
865
|
-
await payload.
|
|
866
|
-
collection: slugs.ticketMessages,
|
|
1011
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
867
1012
|
data: {
|
|
868
1013
|
ticket: targetTicketId,
|
|
869
1014
|
body: `Messages fusionn\xE9s depuis ${sourceNumber} (${messages.totalDocs} messages, ${timeEntries.totalDocs} entr\xE9es de temps)`,
|
|
@@ -873,13 +1018,11 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
873
1018
|
},
|
|
874
1019
|
overrideAccess: true
|
|
875
1020
|
});
|
|
876
|
-
await payload.
|
|
877
|
-
collection: slugs.tickets,
|
|
1021
|
+
await dbDelete(payload, slugs.tickets, {
|
|
878
1022
|
id: sourceTicketId,
|
|
879
1023
|
overrideAccess: true
|
|
880
1024
|
});
|
|
881
|
-
const allTimeEntries = await payload.
|
|
882
|
-
collection: slugs.timeEntries,
|
|
1025
|
+
const allTimeEntries = await dbFind(payload, slugs.timeEntries, {
|
|
883
1026
|
where: { ticket: { equals: targetTicketId } },
|
|
884
1027
|
limit: 500,
|
|
885
1028
|
depth: 0,
|
|
@@ -889,8 +1032,7 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
889
1032
|
(sum, e) => sum + (e.duration || 0),
|
|
890
1033
|
0
|
|
891
1034
|
);
|
|
892
|
-
await payload.
|
|
893
|
-
collection: slugs.tickets,
|
|
1035
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
894
1036
|
id: targetTicketId,
|
|
895
1037
|
data: { totalTimeMinutes: totalMinutes },
|
|
896
1038
|
overrideAccess: true
|
|
@@ -925,8 +1067,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
925
1067
|
if (!messageId) {
|
|
926
1068
|
return Response.json({ error: "messageId required" }, { status: 400 });
|
|
927
1069
|
}
|
|
928
|
-
const message = await payload.
|
|
929
|
-
collection: slugs.ticketMessages,
|
|
1070
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
930
1071
|
id: messageId,
|
|
931
1072
|
depth: 1,
|
|
932
1073
|
overrideAccess: true
|
|
@@ -939,8 +1080,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
939
1080
|
return Response.json({ error: "Cannot resolve source ticket" }, { status: 400 });
|
|
940
1081
|
}
|
|
941
1082
|
const clientId = typeof sourceTicket.client === "object" ? sourceTicket.client.id : sourceTicket.client;
|
|
942
|
-
const newTicket = await payload.
|
|
943
|
-
collection: slugs.tickets,
|
|
1083
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
944
1084
|
data: {
|
|
945
1085
|
subject: subject || `Split: ${sourceTicket.subject}`,
|
|
946
1086
|
client: clientId,
|
|
@@ -955,8 +1095,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
955
1095
|
const attachments = message.attachments?.map((a) => ({
|
|
956
1096
|
file: typeof a.file === "object" ? a.file.id : a.file
|
|
957
1097
|
})) || [];
|
|
958
|
-
await payload.
|
|
959
|
-
collection: slugs.ticketMessages,
|
|
1098
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
960
1099
|
data: {
|
|
961
1100
|
ticket: newTicket.id,
|
|
962
1101
|
body: message.body,
|
|
@@ -969,8 +1108,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
969
1108
|
},
|
|
970
1109
|
overrideAccess: true
|
|
971
1110
|
});
|
|
972
|
-
await payload.
|
|
973
|
-
collection: slugs.ticketMessages,
|
|
1111
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
974
1112
|
data: {
|
|
975
1113
|
ticket: sourceTicket.id,
|
|
976
1114
|
body: `Message extrait vers le ticket ${newTicket.ticketNumber}`,
|
|
@@ -982,15 +1120,13 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
982
1120
|
});
|
|
983
1121
|
const existingRelated = Array.isArray(sourceTicket.relatedTickets) ? sourceTicket.relatedTickets.map((t) => typeof t === "object" ? t.id : t) : [];
|
|
984
1122
|
if (!existingRelated.includes(newTicket.id)) {
|
|
985
|
-
await payload.
|
|
986
|
-
collection: slugs.tickets,
|
|
1123
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
987
1124
|
id: sourceTicket.id,
|
|
988
1125
|
data: { relatedTickets: [...existingRelated, newTicket.id] },
|
|
989
1126
|
overrideAccess: true
|
|
990
1127
|
});
|
|
991
1128
|
}
|
|
992
|
-
await payload.
|
|
993
|
-
collection: slugs.ticketActivityLog,
|
|
1129
|
+
await dbCreate(payload, slugs.ticketActivityLog, {
|
|
994
1130
|
data: {
|
|
995
1131
|
ticket: sourceTicket.id,
|
|
996
1132
|
action: "split",
|
|
@@ -1205,8 +1341,7 @@ function createSettingsGetEndpoint(slugs) {
|
|
|
1205
1341
|
try {
|
|
1206
1342
|
const payload = req.payload;
|
|
1207
1343
|
requireAdmin(req, slugs);
|
|
1208
|
-
const prefs = await payload
|
|
1209
|
-
collection: "payload-preferences",
|
|
1344
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1210
1345
|
where: { key: { equals: PREF_KEY2 } },
|
|
1211
1346
|
limit: 1,
|
|
1212
1347
|
depth: 0,
|
|
@@ -1263,6 +1398,7 @@ function createSettingsPostEndpoint(slugs) {
|
|
|
1263
1398
|
]
|
|
1264
1399
|
}
|
|
1265
1400
|
});
|
|
1401
|
+
invalidateSupportSettingsCache();
|
|
1266
1402
|
return Response.json(merged);
|
|
1267
1403
|
} catch (error) {
|
|
1268
1404
|
const authResponse = handleAuthError(error);
|
|
@@ -1284,8 +1420,7 @@ function createSignatureGetEndpoint(slugs) {
|
|
|
1284
1420
|
try {
|
|
1285
1421
|
const payload = req.payload;
|
|
1286
1422
|
requireAdmin(req, slugs);
|
|
1287
|
-
const prefs = await payload
|
|
1288
|
-
collection: "payload-preferences",
|
|
1423
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1289
1424
|
where: { key: { equals: `${PREF_KEY3}-${req.user.id}` } },
|
|
1290
1425
|
limit: 1,
|
|
1291
1426
|
depth: 0,
|
|
@@ -1312,8 +1447,7 @@ function createSignaturePostEndpoint(slugs) {
|
|
|
1312
1447
|
requireAdmin(req, slugs);
|
|
1313
1448
|
const { signature } = await req.json();
|
|
1314
1449
|
const key = `${PREF_KEY3}-${req.user.id}`;
|
|
1315
|
-
const existing = await payload
|
|
1316
|
-
collection: "payload-preferences",
|
|
1450
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
1317
1451
|
where: { key: { equals: key } },
|
|
1318
1452
|
limit: 1,
|
|
1319
1453
|
depth: 0,
|
|
@@ -1356,8 +1490,7 @@ function createRoundRobinConfigGetEndpoint(slugs) {
|
|
|
1356
1490
|
try {
|
|
1357
1491
|
const payload = req.payload;
|
|
1358
1492
|
requireAdmin(req, slugs);
|
|
1359
|
-
const prefs = await payload
|
|
1360
|
-
collection: "payload-preferences",
|
|
1493
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1361
1494
|
where: { key: { equals: PREF_KEY4 } },
|
|
1362
1495
|
limit: 1,
|
|
1363
1496
|
depth: 0,
|
|
@@ -1383,8 +1516,7 @@ function createRoundRobinConfigPostEndpoint(slugs) {
|
|
|
1383
1516
|
const payload = req.payload;
|
|
1384
1517
|
requireAdmin(req, slugs);
|
|
1385
1518
|
const { enabled } = await req.json();
|
|
1386
|
-
const existing = await payload
|
|
1387
|
-
collection: "payload-preferences",
|
|
1519
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
1388
1520
|
where: { key: { equals: PREF_KEY4 } },
|
|
1389
1521
|
limit: 1,
|
|
1390
1522
|
depth: 0,
|
|
@@ -1428,8 +1560,7 @@ function createSlaCheckEndpoint(slugs) {
|
|
|
1428
1560
|
requireAdmin(req, slugs);
|
|
1429
1561
|
const now = /* @__PURE__ */ new Date();
|
|
1430
1562
|
const nowISO = now.toISOString();
|
|
1431
|
-
const { docs: tickets } = await payload.
|
|
1432
|
-
collection: slugs.tickets,
|
|
1563
|
+
const { docs: tickets } = await dbFind(payload, slugs.tickets, {
|
|
1433
1564
|
where: {
|
|
1434
1565
|
and: [
|
|
1435
1566
|
{ status: { in: ["open", "waiting_client"] } },
|
|
@@ -1523,6 +1654,107 @@ function createSlaCheckEndpoint(slugs) {
|
|
|
1523
1654
|
}
|
|
1524
1655
|
};
|
|
1525
1656
|
}
|
|
1657
|
+
var TRANSPARENT_GIF = Buffer.from(
|
|
1658
|
+
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
1659
|
+
"base64"
|
|
1660
|
+
);
|
|
1661
|
+
function generateTrackingToken(ticketId, messageId, secret) {
|
|
1662
|
+
return crypto3.createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex").substring(0, 16);
|
|
1663
|
+
}
|
|
1664
|
+
function createTrackOpenEndpoint(slugs) {
|
|
1665
|
+
return {
|
|
1666
|
+
path: "/support/track-open",
|
|
1667
|
+
method: "get",
|
|
1668
|
+
handler: async (req) => {
|
|
1669
|
+
const url = new URL(req.url);
|
|
1670
|
+
const ticketId = url.searchParams.get("t");
|
|
1671
|
+
const messageId = url.searchParams.get("m");
|
|
1672
|
+
const sig = url.searchParams.get("sig");
|
|
1673
|
+
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
1674
|
+
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
1675
|
+
const secret = process.env.PAYLOAD_SECRET || "";
|
|
1676
|
+
const validSig = !!secret && !!ticketId && !!messageId && !!sig && sig === generateTrackingToken(ticketId, messageId, secret);
|
|
1677
|
+
if (!validSig) {
|
|
1678
|
+
return new Response(TRANSPARENT_GIF, {
|
|
1679
|
+
status: 200,
|
|
1680
|
+
headers: {
|
|
1681
|
+
"Content-Type": "image/gif",
|
|
1682
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
1683
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
1688
|
+
try {
|
|
1689
|
+
const payload = req.payload;
|
|
1690
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
1691
|
+
id: parsedId,
|
|
1692
|
+
depth: 0,
|
|
1693
|
+
overrideAccess: true,
|
|
1694
|
+
select: { lastClientReadAt: true }
|
|
1695
|
+
});
|
|
1696
|
+
if (ticket) {
|
|
1697
|
+
const lastRead = ticket.lastClientReadAt ? new Date(ticket.lastClientReadAt).getTime() : 0;
|
|
1698
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
|
|
1699
|
+
if (lastRead < fiveMinAgo) {
|
|
1700
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1701
|
+
id: parsedId,
|
|
1702
|
+
data: { lastClientReadAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1703
|
+
overrideAccess: true
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
1708
|
+
const msg = await dbFindByID(payload, slugs.ticketMessages, {
|
|
1709
|
+
id: parsedMsgId,
|
|
1710
|
+
depth: 0,
|
|
1711
|
+
overrideAccess: true,
|
|
1712
|
+
select: { emailOpenedAt: true }
|
|
1713
|
+
});
|
|
1714
|
+
if (msg && !msg.emailOpenedAt) {
|
|
1715
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
1716
|
+
id: parsedMsgId,
|
|
1717
|
+
data: { emailOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1718
|
+
overrideAccess: true
|
|
1719
|
+
});
|
|
1720
|
+
const ticketInfo = await dbFindByID(payload, slugs.tickets, {
|
|
1721
|
+
id: parsedId,
|
|
1722
|
+
depth: 1,
|
|
1723
|
+
overrideAccess: true,
|
|
1724
|
+
select: { ticketNumber: true, subject: true, client: true }
|
|
1725
|
+
});
|
|
1726
|
+
const clientName = typeof ticketInfo?.client === "object" ? ticketInfo.client?.firstName || "Client" : "Client";
|
|
1727
|
+
try {
|
|
1728
|
+
await dbCreate(payload, "admin-notifications", {
|
|
1729
|
+
data: {
|
|
1730
|
+
title: `Email ouvert \u2014 ${ticketInfo?.ticketNumber || "TK-????"}`,
|
|
1731
|
+
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || "ticket"}"`,
|
|
1732
|
+
type: "email_opened",
|
|
1733
|
+
link: `/admin/ticket?id=${parsedId}`
|
|
1734
|
+
},
|
|
1735
|
+
overrideAccess: true
|
|
1736
|
+
});
|
|
1737
|
+
} catch {
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
console.error("[track-open] Error:", err);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return new Response(TRANSPARENT_GIF, {
|
|
1746
|
+
status: 200,
|
|
1747
|
+
headers: {
|
|
1748
|
+
"Content-Type": "image/gif",
|
|
1749
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
1750
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
1751
|
+
"Pragma": "no-cache",
|
|
1752
|
+
"Expires": "0"
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1526
1758
|
|
|
1527
1759
|
// src/utils/emailTemplate.ts
|
|
1528
1760
|
var COLORS = {
|
|
@@ -1559,7 +1791,10 @@ function escapeHtml(str) {
|
|
|
1559
1791
|
}
|
|
1560
1792
|
function emailTrackingPixel(ticketId, messageId, baseUrl) {
|
|
1561
1793
|
const url = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
1562
|
-
const
|
|
1794
|
+
const secret = process.env.PAYLOAD_SECRET || "";
|
|
1795
|
+
if (!secret || messageId === void 0 || messageId === null) return "";
|
|
1796
|
+
const sig = generateTrackingToken(String(ticketId), String(messageId), secret);
|
|
1797
|
+
const params = `t=${ticketId}&m=${messageId}&sig=${sig}`;
|
|
1563
1798
|
return `<img src="${url}/api/support/track-open?${params}" width="1" height="1" alt="" style="display:block;width:1px;height:1px;border:0;" />`;
|
|
1564
1799
|
}
|
|
1565
1800
|
function emailRichContent(html, config) {
|
|
@@ -1824,8 +2059,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1824
2059
|
const CLOSE_AFTER_REMIND_DAYS = settings.autoClose.reminderDaysBefore;
|
|
1825
2060
|
const results = { reminded: 0, closed: 0, errors: 0 };
|
|
1826
2061
|
const remindCutoff = new Date(now.getTime() - REMIND_AFTER_DAYS * 24 * 60 * 60 * 1e3);
|
|
1827
|
-
const ticketsToRemind = await payload.
|
|
1828
|
-
collection: slugs.tickets,
|
|
2062
|
+
const ticketsToRemind = await dbFind(payload, slugs.tickets, {
|
|
1829
2063
|
where: {
|
|
1830
2064
|
and: [
|
|
1831
2065
|
{ status: { equals: "waiting_client" } },
|
|
@@ -1856,8 +2090,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1856
2090
|
<p><a href="${portalUrl}">R\xE9pondre au ticket</a></p>
|
|
1857
2091
|
</div>`
|
|
1858
2092
|
});
|
|
1859
|
-
await payload.
|
|
1860
|
-
collection: slugs.tickets,
|
|
2093
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1861
2094
|
id: t.id,
|
|
1862
2095
|
data: { autoCloseRemindedAt: now.toISOString() },
|
|
1863
2096
|
overrideAccess: true
|
|
@@ -1869,8 +2102,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1869
2102
|
}
|
|
1870
2103
|
}
|
|
1871
2104
|
const closeCutoff = new Date(now.getTime() - CLOSE_AFTER_REMIND_DAYS * 24 * 60 * 60 * 1e3);
|
|
1872
|
-
const ticketsToClose = await payload.
|
|
1873
|
-
collection: slugs.tickets,
|
|
2105
|
+
const ticketsToClose = await dbFind(payload, slugs.tickets, {
|
|
1874
2106
|
where: {
|
|
1875
2107
|
and: [
|
|
1876
2108
|
{ status: { equals: "waiting_client" } },
|
|
@@ -1905,8 +2137,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1905
2137
|
const closeTotalDays = REMIND_AFTER_DAYS + CLOSE_AFTER_REMIND_DAYS;
|
|
1906
2138
|
const viaSchedule = !!t.autoCloseScheduledAt && new Date(t.autoCloseScheduledAt).getTime() <= now.getTime();
|
|
1907
2139
|
const noteBody = viaSchedule ? "Ticket r\xE9solu automatiquement \u2014 relance manuelle rest\xE9e sans r\xE9ponse du client" : `Ticket r\xE9solu automatiquement \u2014 sans r\xE9ponse client depuis ${closeTotalDays} jours`;
|
|
1908
|
-
await payload.
|
|
1909
|
-
collection: slugs.ticketMessages,
|
|
2140
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
1910
2141
|
data: {
|
|
1911
2142
|
ticket: t.id,
|
|
1912
2143
|
body: noteBody,
|
|
@@ -1916,8 +2147,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1916
2147
|
},
|
|
1917
2148
|
overrideAccess: true
|
|
1918
2149
|
});
|
|
1919
|
-
await payload.
|
|
1920
|
-
collection: slugs.tickets,
|
|
2150
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1921
2151
|
id: t.id,
|
|
1922
2152
|
// Clear the armed deadline so a later manual reopen can't be
|
|
1923
2153
|
// closed instantly by a stale timestamp.
|
|
@@ -2040,8 +2270,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2040
2270
|
}
|
|
2041
2271
|
const rawHours = Number(body.hours);
|
|
2042
2272
|
const hours = Number.isFinite(rawHours) ? Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.round(rawHours))) : 24;
|
|
2043
|
-
const ticket = await payload.
|
|
2044
|
-
collection: slugs.tickets,
|
|
2273
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2045
2274
|
id: ticketId,
|
|
2046
2275
|
depth: 1,
|
|
2047
2276
|
overrideAccess: true
|
|
@@ -2063,8 +2292,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2063
2292
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_REPLY_TO || "";
|
|
2064
2293
|
let waitingSince = ticket.firstResponseAt || ticket.createdAt;
|
|
2065
2294
|
try {
|
|
2066
|
-
const lastAdminMsg = await payload.
|
|
2067
|
-
collection: slugs.ticketMessages,
|
|
2295
|
+
const lastAdminMsg = await dbFind(payload, slugs.ticketMessages, {
|
|
2068
2296
|
where: {
|
|
2069
2297
|
and: [
|
|
2070
2298
|
{ ticket: { equals: ticketId } },
|
|
@@ -2105,8 +2333,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2105
2333
|
preheader: `Sans r\xE9ponse de votre part, votre ticket ${ticketNumber} sera cl\xF4tur\xE9 le ${deadlineLabel}.`
|
|
2106
2334
|
})
|
|
2107
2335
|
});
|
|
2108
|
-
await payload.
|
|
2109
|
-
collection: slugs.ticketMessages,
|
|
2336
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2110
2337
|
data: {
|
|
2111
2338
|
ticket: ticketId,
|
|
2112
2339
|
body: `Relance envoy\xE9e \xE0 ${client.email}. Fermeture automatique programm\xE9e le ${deadlineLabel} sans r\xE9ponse du client.`,
|
|
@@ -2116,8 +2343,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2116
2343
|
},
|
|
2117
2344
|
overrideAccess: true
|
|
2118
2345
|
});
|
|
2119
|
-
await payload.
|
|
2120
|
-
collection: slugs.tickets,
|
|
2346
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2121
2347
|
id: ticketId,
|
|
2122
2348
|
data: {
|
|
2123
2349
|
status: "waiting_client",
|
|
@@ -2152,8 +2378,7 @@ function createStatusesEndpoint(slugs) {
|
|
|
2152
2378
|
if (!req.user) {
|
|
2153
2379
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2154
2380
|
}
|
|
2155
|
-
const { docs } = await payload.
|
|
2156
|
-
collection: slugs.ticketStatuses,
|
|
2381
|
+
const { docs } = await dbFind(payload, slugs.ticketStatuses, {
|
|
2157
2382
|
sort: "sortOrder",
|
|
2158
2383
|
limit: 100,
|
|
2159
2384
|
depth: 0,
|
|
@@ -2192,16 +2417,14 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2192
2417
|
if (!macroId || !ticketId) {
|
|
2193
2418
|
return Response.json({ error: "macroId and ticketId are required" }, { status: 400 });
|
|
2194
2419
|
}
|
|
2195
|
-
const macro = await payload.
|
|
2196
|
-
collection: slugs.macros,
|
|
2420
|
+
const macro = await dbFindByID(payload, slugs.macros, {
|
|
2197
2421
|
id: macroId,
|
|
2198
2422
|
depth: 0,
|
|
2199
2423
|
overrideAccess: true
|
|
2200
2424
|
});
|
|
2201
2425
|
if (!macro) return Response.json({ error: "Macro not found" }, { status: 404 });
|
|
2202
2426
|
if (!macro.isActive) return Response.json({ error: "Macro is disabled" }, { status: 400 });
|
|
2203
|
-
const ticket = await payload.
|
|
2204
|
-
collection: slugs.tickets,
|
|
2427
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2205
2428
|
id: ticketId,
|
|
2206
2429
|
depth: 0,
|
|
2207
2430
|
overrideAccess: true
|
|
@@ -2213,8 +2436,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2213
2436
|
try {
|
|
2214
2437
|
switch (action.type) {
|
|
2215
2438
|
case "set_status":
|
|
2216
|
-
await payload.
|
|
2217
|
-
collection: slugs.tickets,
|
|
2439
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2218
2440
|
id: ticketId,
|
|
2219
2441
|
data: { status: action.value },
|
|
2220
2442
|
overrideAccess: true
|
|
@@ -2222,8 +2444,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2222
2444
|
appliedActions.push({ type: action.type, value: action.value, success: true });
|
|
2223
2445
|
break;
|
|
2224
2446
|
case "set_priority":
|
|
2225
|
-
await payload.
|
|
2226
|
-
collection: slugs.tickets,
|
|
2447
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2227
2448
|
id: ticketId,
|
|
2228
2449
|
data: { priority: action.value },
|
|
2229
2450
|
overrideAccess: true
|
|
@@ -2235,8 +2456,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2235
2456
|
if (!currentTags.includes(action.value)) {
|
|
2236
2457
|
currentTags.push(action.value);
|
|
2237
2458
|
}
|
|
2238
|
-
await payload.
|
|
2239
|
-
collection: slugs.tickets,
|
|
2459
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2240
2460
|
id: ticketId,
|
|
2241
2461
|
data: { tags: currentTags },
|
|
2242
2462
|
overrideAccess: true
|
|
@@ -2245,8 +2465,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2245
2465
|
break;
|
|
2246
2466
|
}
|
|
2247
2467
|
case "send_reply":
|
|
2248
|
-
await payload.
|
|
2249
|
-
collection: slugs.ticketMessages,
|
|
2468
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2250
2469
|
data: {
|
|
2251
2470
|
ticket: ticketId,
|
|
2252
2471
|
body: action.value,
|
|
@@ -2263,8 +2482,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2263
2482
|
appliedActions.push({ type: action.type, value: action.value, success: false, error: "Invalid user ID" });
|
|
2264
2483
|
break;
|
|
2265
2484
|
}
|
|
2266
|
-
await payload.
|
|
2267
|
-
collection: slugs.tickets,
|
|
2485
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2268
2486
|
id: ticketId,
|
|
2269
2487
|
data: { assignedTo: userId },
|
|
2270
2488
|
overrideAccess: true
|
|
@@ -2316,8 +2534,7 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2316
2534
|
return Response.json({ error: "Invalid collection. Use email-logs or auth-logs." }, { status: 400 });
|
|
2317
2535
|
}
|
|
2318
2536
|
const cutoff = days > 0 ? new Date(Date.now() - days * 864e5).toISOString() : null;
|
|
2319
|
-
const result = await payload
|
|
2320
|
-
collection: allowedCollections[collection],
|
|
2537
|
+
const result = await dbDelete(payload, allowedCollections[collection], {
|
|
2321
2538
|
where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
|
|
2322
2539
|
overrideAccess: true
|
|
2323
2540
|
});
|
|
@@ -2360,8 +2577,7 @@ function createChatbotEndpoint(slugs) {
|
|
|
2360
2577
|
return Response.json({ error: "Question too short" }, { status: 400 });
|
|
2361
2578
|
}
|
|
2362
2579
|
const payload = req.payload;
|
|
2363
|
-
const articles = await payload.
|
|
2364
|
-
collection: slugs.knowledgeBase,
|
|
2580
|
+
const articles = await dbFind(payload, slugs.knowledgeBase, {
|
|
2365
2581
|
where: { published: { equals: true } },
|
|
2366
2582
|
limit: 100,
|
|
2367
2583
|
depth: 0,
|
|
@@ -2451,8 +2667,7 @@ function createChatGetEndpoint(slugs) {
|
|
|
2451
2667
|
if (after) {
|
|
2452
2668
|
where.createdAt = { greater_than: after };
|
|
2453
2669
|
}
|
|
2454
|
-
const messages = await payload.
|
|
2455
|
-
collection: slugs.chatMessages,
|
|
2670
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2456
2671
|
where,
|
|
2457
2672
|
sort: "createdAt",
|
|
2458
2673
|
limit: 100,
|
|
@@ -2495,8 +2710,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2495
2710
|
return Response.json({ error: "Trop de sessions cr\xE9\xE9es. R\xE9essayez plus tard." }, { status: 429 });
|
|
2496
2711
|
}
|
|
2497
2712
|
const sessionId = `chat_${crypto3__default.default.randomBytes(16).toString("hex")}`;
|
|
2498
|
-
const systemMsg = await payload.
|
|
2499
|
-
collection: slugs.chatMessages,
|
|
2713
|
+
const systemMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2500
2714
|
data: {
|
|
2501
2715
|
session: sessionId,
|
|
2502
2716
|
client: req.user.id,
|
|
@@ -2516,8 +2730,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2516
2730
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2517
2731
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2518
2732
|
}
|
|
2519
|
-
const existing = await payload.
|
|
2520
|
-
collection: slugs.chatMessages,
|
|
2733
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2521
2734
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2522
2735
|
limit: 1,
|
|
2523
2736
|
overrideAccess: true
|
|
@@ -2525,8 +2738,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2525
2738
|
if (existing.docs.length === 0) {
|
|
2526
2739
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2527
2740
|
}
|
|
2528
|
-
const newMsg = await payload.
|
|
2529
|
-
collection: slugs.chatMessages,
|
|
2741
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2530
2742
|
data: {
|
|
2531
2743
|
session,
|
|
2532
2744
|
client: req.user.id,
|
|
@@ -2539,8 +2751,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2539
2751
|
return Response.json({ message: newMsg });
|
|
2540
2752
|
}
|
|
2541
2753
|
if (action === "close" && session) {
|
|
2542
|
-
const existing = await payload.
|
|
2543
|
-
collection: slugs.chatMessages,
|
|
2754
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2544
2755
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2545
2756
|
limit: 1,
|
|
2546
2757
|
overrideAccess: true
|
|
@@ -2548,8 +2759,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2548
2759
|
if (existing.docs.length === 0) {
|
|
2549
2760
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2550
2761
|
}
|
|
2551
|
-
await payload.
|
|
2552
|
-
collection: slugs.chatMessages,
|
|
2762
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2553
2763
|
data: {
|
|
2554
2764
|
session,
|
|
2555
2765
|
client: req.user.id,
|
|
@@ -2591,8 +2801,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2591
2801
|
if (!sessionId) {
|
|
2592
2802
|
return Response.json({ error: "Missing session parameter" }, { status: 400 });
|
|
2593
2803
|
}
|
|
2594
|
-
const existing = await req.payload.
|
|
2595
|
-
collection: slugs.chatMessages,
|
|
2804
|
+
const existing = await dbFind(req.payload, slugs.chatMessages, {
|
|
2596
2805
|
where: {
|
|
2597
2806
|
session: { equals: sessionId },
|
|
2598
2807
|
client: { equals: userId }
|
|
@@ -2614,8 +2823,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2614
2823
|
);
|
|
2615
2824
|
const interval = setInterval(async () => {
|
|
2616
2825
|
try {
|
|
2617
|
-
const messages = await req.payload.
|
|
2618
|
-
collection: slugs.chatMessages,
|
|
2826
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2619
2827
|
where: {
|
|
2620
2828
|
session: { equals: sessionId },
|
|
2621
2829
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2696,8 +2904,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2696
2904
|
if (session) {
|
|
2697
2905
|
const where = { session: { equals: session } };
|
|
2698
2906
|
if (after) where.createdAt = { greater_than: after };
|
|
2699
|
-
const messages = await payload.
|
|
2700
|
-
collection: slugs.chatMessages,
|
|
2907
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2701
2908
|
where,
|
|
2702
2909
|
sort: "createdAt",
|
|
2703
2910
|
limit: 200,
|
|
@@ -2711,8 +2918,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2711
2918
|
}
|
|
2712
2919
|
});
|
|
2713
2920
|
}
|
|
2714
|
-
const recentMessages = await payload.
|
|
2715
|
-
collection: slugs.chatMessages,
|
|
2921
|
+
const recentMessages = await dbFind(payload, slugs.chatMessages, {
|
|
2716
2922
|
sort: "-createdAt",
|
|
2717
2923
|
limit: 50,
|
|
2718
2924
|
depth: 1,
|
|
@@ -2775,8 +2981,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2775
2981
|
if (!session) {
|
|
2776
2982
|
return Response.json({ error: "Session requise" }, { status: 400 });
|
|
2777
2983
|
}
|
|
2778
|
-
const sessionMsg = await payload.
|
|
2779
|
-
collection: slugs.chatMessages,
|
|
2984
|
+
const sessionMsg = await dbFind(payload, slugs.chatMessages, {
|
|
2780
2985
|
where: { session: { equals: session } },
|
|
2781
2986
|
limit: 1,
|
|
2782
2987
|
depth: 0,
|
|
@@ -2794,8 +2999,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2794
2999
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2795
3000
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2796
3001
|
}
|
|
2797
|
-
const newMsg = await payload.
|
|
2798
|
-
collection: slugs.chatMessages,
|
|
3002
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2799
3003
|
data: {
|
|
2800
3004
|
session,
|
|
2801
3005
|
client: clientId,
|
|
@@ -2807,8 +3011,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2807
3011
|
overrideAccess: true
|
|
2808
3012
|
});
|
|
2809
3013
|
try {
|
|
2810
|
-
const linkedTicket = await payload.
|
|
2811
|
-
collection: slugs.tickets,
|
|
3014
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2812
3015
|
where: { chatSession: { equals: session } },
|
|
2813
3016
|
limit: 1,
|
|
2814
3017
|
depth: 0,
|
|
@@ -2816,8 +3019,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2816
3019
|
});
|
|
2817
3020
|
if (linkedTicket.docs.length > 0) {
|
|
2818
3021
|
const ticketId = linkedTicket.docs[0].id;
|
|
2819
|
-
await payload.
|
|
2820
|
-
collection: slugs.ticketMessages,
|
|
3022
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2821
3023
|
data: {
|
|
2822
3024
|
ticket: ticketId,
|
|
2823
3025
|
body: trimmedMessage,
|
|
@@ -2826,8 +3028,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2826
3028
|
},
|
|
2827
3029
|
overrideAccess: true
|
|
2828
3030
|
});
|
|
2829
|
-
await payload.
|
|
2830
|
-
collection: slugs.chatMessages,
|
|
3031
|
+
await dbUpdate(payload, slugs.chatMessages, {
|
|
2831
3032
|
id: newMsg.id,
|
|
2832
3033
|
data: { ticket: ticketId },
|
|
2833
3034
|
overrideAccess: true
|
|
@@ -2839,8 +3040,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2839
3040
|
return Response.json({ message: newMsg });
|
|
2840
3041
|
}
|
|
2841
3042
|
if (action === "close") {
|
|
2842
|
-
await payload.
|
|
2843
|
-
collection: slugs.chatMessages,
|
|
3043
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2844
3044
|
data: {
|
|
2845
3045
|
session,
|
|
2846
3046
|
client: clientId,
|
|
@@ -2851,16 +3051,14 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2851
3051
|
overrideAccess: true
|
|
2852
3052
|
});
|
|
2853
3053
|
try {
|
|
2854
|
-
const linkedTicket = await payload.
|
|
2855
|
-
collection: slugs.tickets,
|
|
3054
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2856
3055
|
where: { chatSession: { equals: session } },
|
|
2857
3056
|
limit: 1,
|
|
2858
3057
|
depth: 0,
|
|
2859
3058
|
overrideAccess: true
|
|
2860
3059
|
});
|
|
2861
3060
|
if (linkedTicket.docs.length > 0) {
|
|
2862
|
-
await payload.
|
|
2863
|
-
collection: slugs.ticketMessages,
|
|
3061
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2864
3062
|
data: {
|
|
2865
3063
|
ticket: linkedTicket.docs[0].id,
|
|
2866
3064
|
body: "Session de chat termin\xE9e par un agent.",
|
|
@@ -2923,8 +3121,7 @@ function createSessionStream(req, slugs, sessionId) {
|
|
|
2923
3121
|
);
|
|
2924
3122
|
const interval = setInterval(async () => {
|
|
2925
3123
|
try {
|
|
2926
|
-
const messages = await req.payload.
|
|
2927
|
-
collection: slugs.chatMessages,
|
|
3124
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2928
3125
|
where: {
|
|
2929
3126
|
session: { equals: sessionId },
|
|
2930
3127
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2997,8 +3194,7 @@ function createSessionListStream(req, slugs) {
|
|
|
2997
3194
|
);
|
|
2998
3195
|
const interval = setInterval(async () => {
|
|
2999
3196
|
try {
|
|
3000
|
-
const recentMessages = await req.payload.
|
|
3001
|
-
collection: slugs.chatMessages,
|
|
3197
|
+
const recentMessages = await dbFind(req.payload, slugs.chatMessages, {
|
|
3002
3198
|
sort: "-createdAt",
|
|
3003
3199
|
limit: 50,
|
|
3004
3200
|
depth: 1,
|
|
@@ -3082,12 +3278,13 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3082
3278
|
try {
|
|
3083
3279
|
const payload = req.payload;
|
|
3084
3280
|
requireAdmin(req, slugs);
|
|
3281
|
+
const teamId = new URL(req.url).searchParams.get("teamId");
|
|
3282
|
+
const teamAnd = teamId ? [{ team: { equals: Number(teamId) } }] : [];
|
|
3085
3283
|
const statuses = ["open", "waiting_client", "resolved"];
|
|
3086
3284
|
const statusCounts = await Promise.all(
|
|
3087
3285
|
statuses.map(async (status) => {
|
|
3088
|
-
const result = await payload.
|
|
3089
|
-
|
|
3090
|
-
where: { status: { equals: status } },
|
|
3286
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3287
|
+
where: { and: [{ status: { equals: status } }, ...teamAnd] },
|
|
3091
3288
|
overrideAccess: true
|
|
3092
3289
|
});
|
|
3093
3290
|
return [status, result.totalDocs];
|
|
@@ -3098,9 +3295,8 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3098
3295
|
const priorities = ["low", "normal", "high", "urgent"];
|
|
3099
3296
|
const priorityCounts = await Promise.all(
|
|
3100
3297
|
priorities.map(async (priority) => {
|
|
3101
|
-
const result = await payload.
|
|
3102
|
-
|
|
3103
|
-
where: { priority: { equals: priority } },
|
|
3298
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3299
|
+
where: { and: [{ priority: { equals: priority } }, ...teamAnd] },
|
|
3104
3300
|
overrideAccess: true
|
|
3105
3301
|
});
|
|
3106
3302
|
return [priority, result.totalDocs];
|
|
@@ -3110,9 +3306,8 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3110
3306
|
const categories = ["bug", "content", "feature", "question", "hosting"];
|
|
3111
3307
|
const categoryCounts = await Promise.all(
|
|
3112
3308
|
categories.map(async (category) => {
|
|
3113
|
-
const result = await payload.
|
|
3114
|
-
|
|
3115
|
-
where: { category: { equals: category } },
|
|
3309
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3310
|
+
where: { and: [{ category: { equals: category } }, ...teamAnd] },
|
|
3116
3311
|
overrideAccess: true
|
|
3117
3312
|
});
|
|
3118
3313
|
return [category, result.totalDocs];
|
|
@@ -3125,17 +3320,34 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3125
3320
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
|
|
3126
3321
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
3127
3322
|
const [created7, created30] = await Promise.all([
|
|
3128
|
-
payload.
|
|
3129
|
-
collection: slugs.tickets,
|
|
3323
|
+
dbCount(payload, slugs.tickets, {
|
|
3130
3324
|
where: { createdAt: { greater_than_equal: sevenDaysAgo.toISOString() } },
|
|
3131
3325
|
overrideAccess: true
|
|
3132
3326
|
}),
|
|
3133
|
-
payload.
|
|
3134
|
-
collection: slugs.tickets,
|
|
3327
|
+
dbCount(payload, slugs.tickets, {
|
|
3135
3328
|
where: { createdAt: { greater_than_equal: thirtyDaysAgo.toISOString() } },
|
|
3136
3329
|
overrideAccess: true
|
|
3137
3330
|
})
|
|
3138
3331
|
]);
|
|
3332
|
+
const volumeByDay = await Promise.all(
|
|
3333
|
+
Array.from({ length: 7 }, (_, i) => i).map(async (offset) => {
|
|
3334
|
+
const dayStart = new Date(now);
|
|
3335
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
3336
|
+
dayStart.setDate(dayStart.getDate() - (6 - offset));
|
|
3337
|
+
const dayEnd = new Date(dayStart);
|
|
3338
|
+
dayEnd.setDate(dayEnd.getDate() + 1);
|
|
3339
|
+
const r = await dbCount(payload, slugs.tickets, {
|
|
3340
|
+
where: {
|
|
3341
|
+
and: [
|
|
3342
|
+
{ createdAt: { greater_than_equal: dayStart.toISOString() } },
|
|
3343
|
+
{ createdAt: { less_than: dayEnd.toISOString() } }
|
|
3344
|
+
]
|
|
3345
|
+
},
|
|
3346
|
+
overrideAccess: true
|
|
3347
|
+
});
|
|
3348
|
+
return { date: dayStart.toISOString(), count: r.totalDocs };
|
|
3349
|
+
})
|
|
3350
|
+
);
|
|
3139
3351
|
const PAGE_SIZE2 = 100;
|
|
3140
3352
|
const MAX_PAGES2 = 50;
|
|
3141
3353
|
let totalResponseTimeMs = 0;
|
|
@@ -3146,8 +3358,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3146
3358
|
let page = 1;
|
|
3147
3359
|
let hasMore = true;
|
|
3148
3360
|
while (hasMore && page <= MAX_PAGES2) {
|
|
3149
|
-
const batch = await payload.
|
|
3150
|
-
collection: slugs.tickets,
|
|
3361
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3151
3362
|
limit: PAGE_SIZE2,
|
|
3152
3363
|
page,
|
|
3153
3364
|
depth: 0,
|
|
@@ -3180,43 +3391,62 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3180
3391
|
hasMore = batch.hasNextPage ?? false;
|
|
3181
3392
|
page++;
|
|
3182
3393
|
}
|
|
3183
|
-
const surveyCount = await payload.
|
|
3184
|
-
collection: slugs.satisfactionSurveys,
|
|
3394
|
+
const surveyCount = await dbCount(payload, slugs.satisfactionSurveys, {
|
|
3185
3395
|
overrideAccess: true
|
|
3186
3396
|
});
|
|
3187
3397
|
let satisfactionAvg = 0;
|
|
3398
|
+
let satisfactionCount = 0;
|
|
3399
|
+
let npsScore = null;
|
|
3400
|
+
let npsCount = 0;
|
|
3188
3401
|
if (surveyCount.totalDocs > 0) {
|
|
3189
|
-
let
|
|
3402
|
+
let ratingSum = 0;
|
|
3403
|
+
let ratingN = 0;
|
|
3404
|
+
let promoters = 0;
|
|
3405
|
+
let detractors = 0;
|
|
3406
|
+
let npsN = 0;
|
|
3190
3407
|
let surveyPage = 1;
|
|
3191
3408
|
let surveyHasMore = true;
|
|
3192
3409
|
while (surveyHasMore && surveyPage <= MAX_PAGES2) {
|
|
3193
|
-
const batch = await payload.
|
|
3194
|
-
collection: slugs.satisfactionSurveys,
|
|
3410
|
+
const batch = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
3195
3411
|
limit: PAGE_SIZE2,
|
|
3196
3412
|
page: surveyPage,
|
|
3197
3413
|
depth: 0,
|
|
3198
3414
|
overrideAccess: true,
|
|
3199
|
-
select: { rating: true }
|
|
3415
|
+
select: { rating: true, nps: true }
|
|
3200
3416
|
});
|
|
3201
3417
|
for (const s of batch.docs) {
|
|
3202
|
-
|
|
3418
|
+
const r = s.rating;
|
|
3419
|
+
if (typeof r === "number" && r > 0) {
|
|
3420
|
+
ratingSum += r;
|
|
3421
|
+
ratingN++;
|
|
3422
|
+
}
|
|
3423
|
+
const n = s.nps;
|
|
3424
|
+
if (typeof n === "number") {
|
|
3425
|
+
npsN++;
|
|
3426
|
+
if (n >= 9) promoters++;
|
|
3427
|
+
else if (n <= 6) detractors++;
|
|
3428
|
+
}
|
|
3203
3429
|
}
|
|
3204
3430
|
surveyHasMore = batch.hasNextPage ?? false;
|
|
3205
3431
|
surveyPage++;
|
|
3206
3432
|
}
|
|
3207
|
-
satisfactionAvg = Math.round(
|
|
3433
|
+
satisfactionAvg = ratingN > 0 ? Math.round(ratingSum / ratingN * 10) / 10 : 0;
|
|
3434
|
+
satisfactionCount = ratingN;
|
|
3435
|
+
if (npsN > 0) {
|
|
3436
|
+
npsScore = Math.round((promoters - detractors) / npsN * 100);
|
|
3437
|
+
npsCount = npsN;
|
|
3438
|
+
}
|
|
3208
3439
|
}
|
|
3209
|
-
const
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
}),
|
|
3214
|
-
payload.count({
|
|
3215
|
-
collection: slugs.pendingEmails,
|
|
3440
|
+
const clientCount = await dbCount(payload, slugs.supportClients, { overrideAccess: true });
|
|
3441
|
+
let pendingEmailsTotal = 0;
|
|
3442
|
+
try {
|
|
3443
|
+
const pe = await dbCount(payload, slugs.pendingEmails, {
|
|
3216
3444
|
where: { status: { equals: "pending" } },
|
|
3217
3445
|
overrideAccess: true
|
|
3218
|
-
})
|
|
3219
|
-
|
|
3446
|
+
});
|
|
3447
|
+
pendingEmailsTotal = pe.totalDocs;
|
|
3448
|
+
} catch {
|
|
3449
|
+
}
|
|
3220
3450
|
return new Response(JSON.stringify({
|
|
3221
3451
|
total,
|
|
3222
3452
|
byStatus,
|
|
@@ -3224,13 +3454,16 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3224
3454
|
byCategory,
|
|
3225
3455
|
createdLast7Days: created7.totalDocs,
|
|
3226
3456
|
createdLast30Days: created30.totalDocs,
|
|
3457
|
+
volumeByDay,
|
|
3227
3458
|
avgResponseTimeHours: responseTimeCount > 0 ? Math.round(totalResponseTimeMs / responseTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3228
3459
|
avgResolutionTimeHours: resolutionTimeCount > 0 ? Math.round(totalResolutionTimeMs / resolutionTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3229
3460
|
totalTimeMinutes,
|
|
3230
3461
|
satisfactionAvg,
|
|
3231
|
-
satisfactionCount
|
|
3462
|
+
satisfactionCount,
|
|
3463
|
+
npsScore,
|
|
3464
|
+
npsCount,
|
|
3232
3465
|
clientCount: clientCount.totalDocs,
|
|
3233
|
-
pendingEmailsCount:
|
|
3466
|
+
pendingEmailsCount: pendingEmailsTotal
|
|
3234
3467
|
}), {
|
|
3235
3468
|
headers: {
|
|
3236
3469
|
"Content-Type": "application/json",
|
|
@@ -3300,13 +3533,26 @@ function createBillingEndpoint(slugs) {
|
|
|
3300
3533
|
let ticketPage = 1;
|
|
3301
3534
|
let ticketHasMore = true;
|
|
3302
3535
|
while (ticketHasMore && ticketPage <= MAX_PAGES) {
|
|
3303
|
-
const batch = await payload.
|
|
3304
|
-
collection: slugs.tickets,
|
|
3536
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3305
3537
|
where: ticketWhere,
|
|
3306
3538
|
limit: PAGE_SIZE,
|
|
3307
3539
|
page: ticketPage,
|
|
3540
|
+
// depth 2 is required: we read ticket.project.client.company below.
|
|
3308
3541
|
depth: 2,
|
|
3309
|
-
overrideAccess: true
|
|
3542
|
+
overrideAccess: true,
|
|
3543
|
+
// Bound the payload to only the fields the report consumes (skips body,
|
|
3544
|
+
// richText, SLA fields, etc.) while keeping project/client populated.
|
|
3545
|
+
select: {
|
|
3546
|
+
ticketNumber: true,
|
|
3547
|
+
subject: true,
|
|
3548
|
+
status: true,
|
|
3549
|
+
billedAmount: true,
|
|
3550
|
+
aiSummary: true,
|
|
3551
|
+
aiSummaryGeneratedAt: true,
|
|
3552
|
+
aiSummaryStatus: true,
|
|
3553
|
+
project: true,
|
|
3554
|
+
client: true
|
|
3555
|
+
}
|
|
3310
3556
|
});
|
|
3311
3557
|
allTickets.push(...batch.docs);
|
|
3312
3558
|
ticketHasMore = batch.hasNextPage ?? false;
|
|
@@ -3316,8 +3562,7 @@ function createBillingEndpoint(slugs) {
|
|
|
3316
3562
|
let entryPage = 1;
|
|
3317
3563
|
let entryHasMore = true;
|
|
3318
3564
|
while (entryHasMore && entryPage <= MAX_PAGES) {
|
|
3319
|
-
const batch = await payload.
|
|
3320
|
-
collection: slugs.timeEntries,
|
|
3565
|
+
const batch = await dbFind(payload, slugs.timeEntries, {
|
|
3321
3566
|
where: {
|
|
3322
3567
|
and: [
|
|
3323
3568
|
{ date: { greater_than_equal: from } },
|
|
@@ -3415,43 +3660,205 @@ function createBillingEndpoint(slugs) {
|
|
|
3415
3660
|
}
|
|
3416
3661
|
};
|
|
3417
3662
|
}
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
function
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3663
|
+
var fmtAmount = (n) => `${n.toFixed(2)} EUR`;
|
|
3664
|
+
var fmtTime = (min) => `${Math.floor(min / 60)}h${String(min % 60).padStart(2, "0")}`;
|
|
3665
|
+
function generateInvoicePdf(data) {
|
|
3666
|
+
return new Promise((resolve, reject) => {
|
|
3667
|
+
try {
|
|
3668
|
+
const doc = new PDFDocument__default.default({ size: "A4", margin: 50 });
|
|
3669
|
+
const chunks = [];
|
|
3670
|
+
doc.on("data", (c) => chunks.push(c));
|
|
3671
|
+
doc.on("end", () => resolve(Buffer.concat(chunks)));
|
|
3672
|
+
doc.on("error", reject);
|
|
3673
|
+
doc.fillColor("#0f172a").fontSize(22).text("Invoice / Pre-billing");
|
|
3674
|
+
doc.moveDown(0.2);
|
|
3675
|
+
doc.fillColor("#64748b").fontSize(10).text(`Period: ${data.from} -> ${data.to}${data.projectId ? ` \xB7 Project #${data.projectId}` : ""}`);
|
|
3676
|
+
doc.moveDown(1);
|
|
3677
|
+
const X = { ticket: 50, subject: 130, client: 320, time: 430, amount: 500 };
|
|
3678
|
+
const drawRow = (r, opts) => {
|
|
3679
|
+
const y = doc.y;
|
|
3680
|
+
doc.fontSize(opts?.header ? 9 : 10).fillColor(opts?.header ? "#475569" : "#0f172a");
|
|
3681
|
+
if (opts?.total) doc.font("Helvetica-Bold");
|
|
3682
|
+
doc.text(r.ticket, X.ticket, y, { width: X.subject - X.ticket - 6 });
|
|
3683
|
+
doc.text(r.subject, X.subject, y, { width: X.client - X.subject - 6 });
|
|
3684
|
+
doc.text(r.client, X.client, y, { width: X.time - X.client - 6 });
|
|
3685
|
+
doc.text(r.time, X.time, y, { width: X.amount - X.time - 6, align: "right" });
|
|
3686
|
+
doc.text(r.amount, X.amount, y, { width: 545 - X.amount, align: "right" });
|
|
3687
|
+
doc.font("Helvetica");
|
|
3688
|
+
doc.moveDown(0.4);
|
|
3689
|
+
};
|
|
3690
|
+
drawRow({ ticket: "Ticket", subject: "Subject", client: "Client", time: "Time", amount: "Amount" }, { header: true });
|
|
3691
|
+
doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor("#e2e8f0").stroke();
|
|
3692
|
+
doc.moveDown(0.3);
|
|
3693
|
+
for (const l of data.lines) {
|
|
3694
|
+
if (doc.y > 760) doc.addPage();
|
|
3695
|
+
drawRow({
|
|
3696
|
+
ticket: l.ticketNumber || "",
|
|
3697
|
+
subject: l.subject || "",
|
|
3698
|
+
client: l.client || "",
|
|
3699
|
+
time: fmtTime(l.minutes),
|
|
3700
|
+
amount: fmtAmount(l.amount)
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor("#0f172a").stroke();
|
|
3704
|
+
doc.moveDown(0.3);
|
|
3705
|
+
drawRow({
|
|
3706
|
+
ticket: "",
|
|
3707
|
+
subject: `Total (${data.lines.length} ticket${data.lines.length > 1 ? "s" : ""})`,
|
|
3708
|
+
client: "",
|
|
3709
|
+
time: fmtTime(data.totalMinutes),
|
|
3710
|
+
amount: fmtAmount(data.totalAmount)
|
|
3711
|
+
}, { total: true });
|
|
3712
|
+
doc.end();
|
|
3713
|
+
} catch (err) {
|
|
3714
|
+
reject(err);
|
|
3715
|
+
}
|
|
3442
3716
|
});
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// src/endpoints/invoice.ts
|
|
3720
|
+
function createInvoiceEndpoint(slugs) {
|
|
3721
|
+
return {
|
|
3722
|
+
path: "/support/billing/invoice",
|
|
3723
|
+
method: "get",
|
|
3724
|
+
handler: async (req) => {
|
|
3725
|
+
try {
|
|
3726
|
+
requireAdmin(req, slugs);
|
|
3727
|
+
const payload = req.payload;
|
|
3728
|
+
const url = new URL(req.url);
|
|
3729
|
+
const from = url.searchParams.get("from");
|
|
3730
|
+
const to = url.searchParams.get("to");
|
|
3731
|
+
const projectId = url.searchParams.get("projectId");
|
|
3732
|
+
const format = url.searchParams.get("format") === "pdf" ? "pdf" : "html";
|
|
3733
|
+
if (!from || !to) {
|
|
3734
|
+
return Response.json({ error: "Param\xE8tres from et to requis." }, { status: 400 });
|
|
3735
|
+
}
|
|
3736
|
+
const toExclusive = new Date(to);
|
|
3737
|
+
toExclusive.setDate(toExclusive.getDate() + 1);
|
|
3738
|
+
const toIso = toExclusive.toISOString();
|
|
3739
|
+
const where = {
|
|
3740
|
+
and: [
|
|
3741
|
+
{ billable: { equals: true } },
|
|
3742
|
+
...projectId ? [{ project: { equals: Number(projectId) } }] : [],
|
|
3743
|
+
{
|
|
3744
|
+
or: [
|
|
3745
|
+
{ and: [{ updatedAt: { greater_than_equal: from } }, { updatedAt: { less_than: toIso } }] },
|
|
3746
|
+
{ and: [{ createdAt: { greater_than_equal: from } }, { createdAt: { less_than: toIso } }] },
|
|
3747
|
+
{ and: [{ resolvedAt: { greater_than_equal: from } }, { resolvedAt: { less_than: toIso } }] }
|
|
3748
|
+
]
|
|
3749
|
+
}
|
|
3750
|
+
]
|
|
3751
|
+
};
|
|
3752
|
+
const tickets = [];
|
|
3753
|
+
let page = 1;
|
|
3754
|
+
let hasMore = true;
|
|
3755
|
+
while (hasMore && page <= 50) {
|
|
3756
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3757
|
+
where,
|
|
3758
|
+
limit: 500,
|
|
3759
|
+
page,
|
|
3760
|
+
depth: 2,
|
|
3761
|
+
overrideAccess: true,
|
|
3762
|
+
select: { ticketNumber: true, subject: true, billedAmount: true, totalTimeMinutes: true, project: true, client: true }
|
|
3763
|
+
});
|
|
3764
|
+
tickets.push(...batch.docs);
|
|
3765
|
+
hasMore = batch.hasNextPage ?? false;
|
|
3766
|
+
page++;
|
|
3767
|
+
}
|
|
3768
|
+
let totalAmount = 0;
|
|
3769
|
+
let totalMinutes = 0;
|
|
3770
|
+
const lines = tickets.map((t) => {
|
|
3771
|
+
const amount = typeof t.billedAmount === "number" ? t.billedAmount : 0;
|
|
3772
|
+
const minutes = typeof t.totalTimeMinutes === "number" ? t.totalTimeMinutes : 0;
|
|
3773
|
+
totalAmount += amount;
|
|
3774
|
+
totalMinutes += minutes;
|
|
3775
|
+
const client = typeof t.client === "object" && t.client ? t.client.company || "" : "";
|
|
3776
|
+
return { ticketNumber: String(t.ticketNumber || ""), subject: String(t.subject || ""), client, minutes, amount };
|
|
3777
|
+
});
|
|
3778
|
+
if (format === "pdf") {
|
|
3779
|
+
const pdf = await generateInvoicePdf({ from, to, projectId, lines, totalMinutes, totalAmount });
|
|
3780
|
+
return new Response(new Uint8Array(pdf), {
|
|
3781
|
+
status: 200,
|
|
3782
|
+
headers: {
|
|
3783
|
+
"Content-Type": "application/pdf",
|
|
3784
|
+
"Content-Disposition": `inline; filename="invoice-${from}_${to}.pdf"`
|
|
3785
|
+
}
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
const fmtAmount2 = (n) => `${n.toFixed(2)} \u20AC`;
|
|
3789
|
+
const fmtTime2 = (min) => `${Math.floor(min / 60)}h${String(min % 60).padStart(2, "0")}`;
|
|
3790
|
+
const rows = lines.map((l) => `<tr>
|
|
3791
|
+
<td>${escapeHtml(l.ticketNumber)}</td>
|
|
3792
|
+
<td>${escapeHtml(l.subject)}</td>
|
|
3793
|
+
<td>${escapeHtml(l.client)}</td>
|
|
3794
|
+
<td style="text-align:right;">${fmtTime2(l.minutes)}</td>
|
|
3795
|
+
<td style="text-align:right;">${fmtAmount2(l.amount)}</td>
|
|
3796
|
+
</tr>`).join("");
|
|
3797
|
+
const html = `<!doctype html>
|
|
3798
|
+
<html lang="fr"><head><meta charset="utf-8"><title>Facture ${escapeHtml(from)} \u2192 ${escapeHtml(to)}</title>
|
|
3799
|
+
<style>
|
|
3800
|
+
body { font-family: system-ui, -apple-system, sans-serif; color: #1e293b; max-width: 820px; margin: 32px auto; padding: 0 24px; }
|
|
3801
|
+
h1 { font-size: 22px; margin: 0 0 4px; }
|
|
3802
|
+
.period { color: #64748b; margin-bottom: 24px; }
|
|
3803
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
3804
|
+
th, td { padding: 8px 10px; border-bottom: 1px solid #e2e8f0; text-align: left; }
|
|
3805
|
+
th { background: #f8fafc; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: #475569; }
|
|
3806
|
+
tfoot td { font-weight: 700; border-top: 2px solid #0f172a; border-bottom: none; }
|
|
3807
|
+
@media print { body { margin: 0; } }
|
|
3808
|
+
</style></head>
|
|
3809
|
+
<body>
|
|
3810
|
+
<h1>Facture / Pr\xE9-facture</h1>
|
|
3811
|
+
<div class="period">P\xE9riode : ${escapeHtml(from)} \u2192 ${escapeHtml(to)}${projectId ? ` \xB7 Projet #${escapeHtml(projectId)}` : ""}</div>
|
|
3812
|
+
<table>
|
|
3813
|
+
<thead><tr><th>Ticket</th><th>Sujet</th><th>Client</th><th style="text-align:right;">Temps</th><th style="text-align:right;">Montant</th></tr></thead>
|
|
3814
|
+
<tbody>${rows || '<tr><td colspan="5" style="color:#94a3b8;">Aucun ticket facturable sur la p\xE9riode.</td></tr>'}</tbody>
|
|
3815
|
+
<tfoot><tr><td colspan="3">Total (${lines.length} ticket${lines.length > 1 ? "s" : ""})</td><td style="text-align:right;">${fmtTime2(totalMinutes)}</td><td style="text-align:right;">${fmtAmount2(totalAmount)}</td></tr></tfoot>
|
|
3816
|
+
</table>
|
|
3817
|
+
</body></html>`;
|
|
3818
|
+
return new Response(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
3819
|
+
} catch (error) {
|
|
3820
|
+
const authResponse = handleAuthError(error);
|
|
3821
|
+
if (authResponse) return authResponse;
|
|
3822
|
+
console.error("[invoice] Error:", error);
|
|
3823
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
// src/utils/generateTicketSynthesis.ts
|
|
3830
|
+
function getClient4(aiSettings) {
|
|
3831
|
+
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
3832
|
+
if (aiSettings.provider === "ollama") {
|
|
3833
|
+
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
3834
|
+
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
3835
|
+
}
|
|
3836
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
3837
|
+
}
|
|
3838
|
+
function getModel3(aiSettings) {
|
|
3839
|
+
return aiSettings.model || "claude-haiku-4-5-20251001";
|
|
3840
|
+
}
|
|
3841
|
+
async function generateTicketSynthesis(args) {
|
|
3842
|
+
const { payload, slugs, ticketId } = args;
|
|
3843
|
+
const settings = await readSupportSettings(payload);
|
|
3844
|
+
if (settings.ai.enableSynthesis === false) {
|
|
3845
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "skipped", reason: "synthesis disabled" };
|
|
3846
|
+
}
|
|
3847
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
3848
|
+
id: ticketId,
|
|
3849
|
+
depth: 1,
|
|
3850
|
+
overrideAccess: true
|
|
3851
|
+
});
|
|
3852
|
+
if (!ticket) {
|
|
3853
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "error", reason: "ticket not found" };
|
|
3854
|
+
}
|
|
3855
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3856
|
+
id: ticketId,
|
|
3857
|
+
data: { aiSummaryStatus: "pending" },
|
|
3450
3858
|
overrideAccess: true
|
|
3451
3859
|
}).catch(() => {
|
|
3452
3860
|
});
|
|
3453
|
-
const messagesResult = await payload.
|
|
3454
|
-
collection: slugs.ticketMessages,
|
|
3861
|
+
const messagesResult = await dbFind(payload, slugs.ticketMessages, {
|
|
3455
3862
|
where: { ticket: { equals: ticketId } },
|
|
3456
3863
|
sort: "createdAt",
|
|
3457
3864
|
limit: 500,
|
|
@@ -3461,8 +3868,7 @@ async function generateTicketSynthesis(args) {
|
|
|
3461
3868
|
const messages = messagesResult.docs;
|
|
3462
3869
|
if (messages.length === 0) {
|
|
3463
3870
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3464
|
-
await payload.
|
|
3465
|
-
collection: slugs.tickets,
|
|
3871
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3466
3872
|
id: ticketId,
|
|
3467
3873
|
data: {
|
|
3468
3874
|
aiSummary: "(Aucun message dans ce ticket)",
|
|
@@ -3507,7 +3913,7 @@ Regles strictes :
|
|
|
3507
3913
|
- Si le ticket n'a pas abouti, decris quand meme le travail d'analyse realise
|
|
3508
3914
|
|
|
3509
3915
|
Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
3510
|
-
const anthropic =
|
|
3916
|
+
const anthropic = getClient4(settings.ai);
|
|
3511
3917
|
const model = getModel3(settings.ai);
|
|
3512
3918
|
try {
|
|
3513
3919
|
const res = await anthropic.messages.create({
|
|
@@ -3517,8 +3923,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3517
3923
|
});
|
|
3518
3924
|
const summary = res.content[0]?.type === "text" ? res.content[0].text.trim() : "";
|
|
3519
3925
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3520
|
-
await payload.
|
|
3521
|
-
collection: slugs.tickets,
|
|
3926
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3522
3927
|
id: ticketId,
|
|
3523
3928
|
data: {
|
|
3524
3929
|
aiSummary: summary,
|
|
@@ -3530,8 +3935,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3530
3935
|
return { summary, generatedAt, status: "done" };
|
|
3531
3936
|
} catch (err) {
|
|
3532
3937
|
const message = err instanceof Error ? err.message : String(err);
|
|
3533
|
-
await payload.
|
|
3534
|
-
collection: slugs.tickets,
|
|
3938
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3535
3939
|
id: ticketId,
|
|
3536
3940
|
data: { aiSummaryStatus: "error" },
|
|
3537
3941
|
overrideAccess: true
|
|
@@ -3561,8 +3965,7 @@ function createTicketSynthesisEndpoint(slugs) {
|
|
|
3561
3965
|
return Response.json({ error: "ticketId must be a number" }, { status: 400 });
|
|
3562
3966
|
}
|
|
3563
3967
|
if (!force) {
|
|
3564
|
-
const existing = await payload.
|
|
3565
|
-
collection: slugs.tickets,
|
|
3968
|
+
const existing = await dbFindByID(payload, slugs.tickets, {
|
|
3566
3969
|
id: ticketId,
|
|
3567
3970
|
depth: 0,
|
|
3568
3971
|
overrideAccess: true
|
|
@@ -3606,8 +4009,7 @@ function createEmailStatsEndpoint(slugs) {
|
|
|
3606
4009
|
let page = 1;
|
|
3607
4010
|
let hasMore = true;
|
|
3608
4011
|
while (hasMore && page <= MAX_PAGES2) {
|
|
3609
|
-
const result = await payload.
|
|
3610
|
-
collection: slugs.emailLogs,
|
|
4012
|
+
const result = await dbFind(payload, slugs.emailLogs, {
|
|
3611
4013
|
where: { createdAt: { greater_than: cutoff } },
|
|
3612
4014
|
sort: "-createdAt",
|
|
3613
4015
|
limit: 500,
|
|
@@ -3681,12 +4083,20 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3681
4083
|
const payload = req.payload;
|
|
3682
4084
|
requireClient(req, slugs);
|
|
3683
4085
|
const body = await req.json();
|
|
3684
|
-
const { ticketId, rating, comment } = body;
|
|
3685
|
-
if (!ticketId
|
|
3686
|
-
return Response.json(
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
4086
|
+
const { ticketId, rating, nps, comment } = body;
|
|
4087
|
+
if (!ticketId) {
|
|
4088
|
+
return Response.json({ error: "ticketId est requis." }, { status: 400 });
|
|
4089
|
+
}
|
|
4090
|
+
const hasRating = rating !== void 0 && rating !== null;
|
|
4091
|
+
const hasNps = nps !== void 0 && nps !== null;
|
|
4092
|
+
if (!hasRating && !hasNps) {
|
|
4093
|
+
return Response.json({ error: "Un rating (1-5) et/ou un nps (0-10) est requis." }, { status: 400 });
|
|
4094
|
+
}
|
|
4095
|
+
if (hasRating && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
4096
|
+
return Response.json({ error: "rating doit \xEAtre un entier 1-5." }, { status: 400 });
|
|
4097
|
+
}
|
|
4098
|
+
if (hasNps && (!Number.isInteger(nps) || nps < 0 || nps > 10)) {
|
|
4099
|
+
return Response.json({ error: "nps doit \xEAtre un entier 0-10." }, { status: 400 });
|
|
3690
4100
|
}
|
|
3691
4101
|
if (comment && typeof comment === "string" && comment.length > 5e3) {
|
|
3692
4102
|
return Response.json(
|
|
@@ -3694,8 +4104,7 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3694
4104
|
{ status: 400 }
|
|
3695
4105
|
);
|
|
3696
4106
|
}
|
|
3697
|
-
const ticket = await payload.
|
|
3698
|
-
collection: slugs.tickets,
|
|
4107
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
3699
4108
|
id: ticketId,
|
|
3700
4109
|
depth: 0,
|
|
3701
4110
|
overrideAccess: false,
|
|
@@ -3710,8 +4119,7 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3710
4119
|
{ status: 400 }
|
|
3711
4120
|
);
|
|
3712
4121
|
}
|
|
3713
|
-
const existing = await payload.
|
|
3714
|
-
collection: slugs.satisfactionSurveys,
|
|
4122
|
+
const existing = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
3715
4123
|
where: { ticket: { equals: ticketId } },
|
|
3716
4124
|
limit: 1,
|
|
3717
4125
|
depth: 0,
|
|
@@ -3723,13 +4131,13 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3723
4131
|
{ status: 409 }
|
|
3724
4132
|
);
|
|
3725
4133
|
}
|
|
3726
|
-
const survey = await payload.
|
|
3727
|
-
collection: slugs.satisfactionSurveys,
|
|
4134
|
+
const survey = await dbCreate(payload, slugs.satisfactionSurveys, {
|
|
3728
4135
|
data: {
|
|
3729
4136
|
source: "ticket",
|
|
3730
4137
|
ticket: ticketId,
|
|
3731
4138
|
client: req.user.id,
|
|
3732
|
-
rating: Math.round(rating),
|
|
4139
|
+
...hasRating ? { rating: Math.round(rating) } : {},
|
|
4140
|
+
...hasNps ? { nps: Math.round(nps) } : {},
|
|
3733
4141
|
...comment ? { comment: comment.trim() } : {}
|
|
3734
4142
|
},
|
|
3735
4143
|
overrideAccess: true
|
|
@@ -3744,124 +4152,6 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3744
4152
|
}
|
|
3745
4153
|
};
|
|
3746
4154
|
}
|
|
3747
|
-
var TRANSPARENT_GIF = Buffer.from(
|
|
3748
|
-
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
3749
|
-
"base64"
|
|
3750
|
-
);
|
|
3751
|
-
function generateTrackingToken(ticketId, messageId, secret) {
|
|
3752
|
-
return crypto3.createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex").substring(0, 16);
|
|
3753
|
-
}
|
|
3754
|
-
function createTrackOpenEndpoint(slugs) {
|
|
3755
|
-
return {
|
|
3756
|
-
path: "/support/track-open",
|
|
3757
|
-
method: "get",
|
|
3758
|
-
handler: async (req) => {
|
|
3759
|
-
const url = new URL(req.url);
|
|
3760
|
-
const ticketId = url.searchParams.get("t");
|
|
3761
|
-
const messageId = url.searchParams.get("m");
|
|
3762
|
-
const sig = url.searchParams.get("sig");
|
|
3763
|
-
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
3764
|
-
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
3765
|
-
const secret = process.env.PAYLOAD_SECRET || "";
|
|
3766
|
-
if (secret && ticketId && messageId && sig) {
|
|
3767
|
-
const expected = generateTrackingToken(ticketId, messageId, secret);
|
|
3768
|
-
if (sig !== expected) {
|
|
3769
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3770
|
-
status: 200,
|
|
3771
|
-
headers: {
|
|
3772
|
-
"Content-Type": "image/gif",
|
|
3773
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3774
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3775
|
-
}
|
|
3776
|
-
});
|
|
3777
|
-
}
|
|
3778
|
-
} else if (secret && (!sig || !ticketId || !messageId)) {
|
|
3779
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3780
|
-
status: 200,
|
|
3781
|
-
headers: {
|
|
3782
|
-
"Content-Type": "image/gif",
|
|
3783
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3784
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3785
|
-
}
|
|
3786
|
-
});
|
|
3787
|
-
}
|
|
3788
|
-
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
3789
|
-
try {
|
|
3790
|
-
const payload = req.payload;
|
|
3791
|
-
const ticket = await payload.findByID({
|
|
3792
|
-
collection: slugs.tickets,
|
|
3793
|
-
id: parsedId,
|
|
3794
|
-
depth: 0,
|
|
3795
|
-
overrideAccess: true,
|
|
3796
|
-
select: { lastClientReadAt: true }
|
|
3797
|
-
});
|
|
3798
|
-
if (ticket) {
|
|
3799
|
-
const lastRead = ticket.lastClientReadAt ? new Date(ticket.lastClientReadAt).getTime() : 0;
|
|
3800
|
-
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
|
|
3801
|
-
if (lastRead < fiveMinAgo) {
|
|
3802
|
-
await payload.update({
|
|
3803
|
-
collection: slugs.tickets,
|
|
3804
|
-
id: parsedId,
|
|
3805
|
-
data: { lastClientReadAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3806
|
-
overrideAccess: true
|
|
3807
|
-
});
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
3811
|
-
const msg = await payload.findByID({
|
|
3812
|
-
collection: slugs.ticketMessages,
|
|
3813
|
-
id: parsedMsgId,
|
|
3814
|
-
depth: 0,
|
|
3815
|
-
overrideAccess: true,
|
|
3816
|
-
select: { emailOpenedAt: true }
|
|
3817
|
-
});
|
|
3818
|
-
if (msg && !msg.emailOpenedAt) {
|
|
3819
|
-
await payload.update({
|
|
3820
|
-
collection: slugs.ticketMessages,
|
|
3821
|
-
id: parsedMsgId,
|
|
3822
|
-
data: { emailOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3823
|
-
overrideAccess: true
|
|
3824
|
-
});
|
|
3825
|
-
const ticketInfo = await payload.findByID({
|
|
3826
|
-
collection: slugs.tickets,
|
|
3827
|
-
id: parsedId,
|
|
3828
|
-
depth: 1,
|
|
3829
|
-
overrideAccess: true,
|
|
3830
|
-
select: { ticketNumber: true, subject: true, client: true }
|
|
3831
|
-
});
|
|
3832
|
-
const clientName = typeof ticketInfo?.client === "object" ? ticketInfo.client?.firstName || "Client" : "Client";
|
|
3833
|
-
try {
|
|
3834
|
-
await payload.create({
|
|
3835
|
-
collection: "admin-notifications",
|
|
3836
|
-
data: {
|
|
3837
|
-
title: `Email ouvert \u2014 ${ticketInfo?.ticketNumber || "TK-????"}`,
|
|
3838
|
-
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || "ticket"}"`,
|
|
3839
|
-
type: "email_opened",
|
|
3840
|
-
link: `/admin/ticket?id=${parsedId}`
|
|
3841
|
-
},
|
|
3842
|
-
overrideAccess: true
|
|
3843
|
-
});
|
|
3844
|
-
} catch {
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
}
|
|
3848
|
-
} catch (err) {
|
|
3849
|
-
console.error("[track-open] Error:", err);
|
|
3850
|
-
}
|
|
3851
|
-
}
|
|
3852
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3853
|
-
status: 200,
|
|
3854
|
-
headers: {
|
|
3855
|
-
"Content-Type": "image/gif",
|
|
3856
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3857
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
3858
|
-
"Pragma": "no-cache",
|
|
3859
|
-
"Expires": "0"
|
|
3860
|
-
}
|
|
3861
|
-
});
|
|
3862
|
-
}
|
|
3863
|
-
};
|
|
3864
|
-
}
|
|
3865
4155
|
|
|
3866
4156
|
// src/endpoints/export-csv.ts
|
|
3867
4157
|
function createExportCsvEndpoint(slugs) {
|
|
@@ -3877,8 +4167,7 @@ function createExportCsvEndpoint(slugs) {
|
|
|
3877
4167
|
let hasMore = true;
|
|
3878
4168
|
const allDocs = [];
|
|
3879
4169
|
while (hasMore) {
|
|
3880
|
-
const batch = await payload.
|
|
3881
|
-
collection: slugs.tickets,
|
|
4170
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3882
4171
|
limit: PAGE_SIZE2,
|
|
3883
4172
|
page,
|
|
3884
4173
|
depth: 1,
|
|
@@ -3954,21 +4243,18 @@ function createExportDataEndpoint(slugs) {
|
|
|
3954
4243
|
const payload = req.payload;
|
|
3955
4244
|
requireClient(req, slugs);
|
|
3956
4245
|
const [clientData, ticketsResult, messagesResult, surveysResult] = await Promise.all([
|
|
3957
|
-
payload.
|
|
3958
|
-
collection: slugs.supportClients,
|
|
4246
|
+
dbFindByID(payload, slugs.supportClients, {
|
|
3959
4247
|
id: req.user.id,
|
|
3960
4248
|
depth: 0,
|
|
3961
4249
|
overrideAccess: true
|
|
3962
4250
|
}),
|
|
3963
|
-
payload.
|
|
3964
|
-
collection: slugs.tickets,
|
|
4251
|
+
dbFind(payload, slugs.tickets, {
|
|
3965
4252
|
where: { client: { equals: req.user.id } },
|
|
3966
4253
|
limit: 1e3,
|
|
3967
4254
|
depth: 0,
|
|
3968
4255
|
overrideAccess: true
|
|
3969
4256
|
}),
|
|
3970
|
-
payload.
|
|
3971
|
-
collection: slugs.ticketMessages,
|
|
4257
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
3972
4258
|
where: {
|
|
3973
4259
|
"ticket.client": { equals: req.user.id },
|
|
3974
4260
|
authorType: { equals: "client" }
|
|
@@ -3977,8 +4263,7 @@ function createExportDataEndpoint(slugs) {
|
|
|
3977
4263
|
depth: 0,
|
|
3978
4264
|
overrideAccess: true
|
|
3979
4265
|
}),
|
|
3980
|
-
payload.
|
|
3981
|
-
collection: slugs.satisfactionSurveys,
|
|
4266
|
+
dbFind(payload, slugs.satisfactionSurveys, {
|
|
3982
4267
|
where: { client: { equals: req.user.id } },
|
|
3983
4268
|
limit: 500,
|
|
3984
4269
|
depth: 0,
|
|
@@ -4052,8 +4337,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4052
4337
|
if (!["create_ticket", "add_to_ticket", "ignore"].includes(action)) {
|
|
4053
4338
|
return Response.json({ error: "Invalid action" }, { status: 400 });
|
|
4054
4339
|
}
|
|
4055
|
-
const pendingEmail = await payload.
|
|
4056
|
-
collection: slugs.pendingEmails,
|
|
4340
|
+
const pendingEmail = await dbFindByID(payload, slugs.pendingEmails, {
|
|
4057
4341
|
id: Number(id),
|
|
4058
4342
|
depth: 1,
|
|
4059
4343
|
overrideAccess: true
|
|
@@ -4067,14 +4351,12 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4067
4351
|
let clientId = overrideClientId || (typeof pendingEmail.client === "object" ? pendingEmail.client?.id : pendingEmail.client);
|
|
4068
4352
|
let clientDoc = typeof pendingEmail.client === "object" ? pendingEmail.client : null;
|
|
4069
4353
|
if (overrideClientId && overrideClientId !== clientDoc?.id) {
|
|
4070
|
-
clientDoc = await payload.
|
|
4071
|
-
collection: slugs.supportClients,
|
|
4354
|
+
clientDoc = await dbFindByID(payload, slugs.supportClients, {
|
|
4072
4355
|
id: overrideClientId,
|
|
4073
4356
|
depth: 0,
|
|
4074
4357
|
overrideAccess: true
|
|
4075
4358
|
});
|
|
4076
|
-
await payload.
|
|
4077
|
-
collection: slugs.pendingEmails,
|
|
4359
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4078
4360
|
id: Number(id),
|
|
4079
4361
|
data: { client: overrideClientId },
|
|
4080
4362
|
overrideAccess: true
|
|
@@ -4091,8 +4373,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4091
4373
|
file: typeof a.file === "object" ? a.file.id : a.file
|
|
4092
4374
|
})) || [];
|
|
4093
4375
|
if (action === "ignore") {
|
|
4094
|
-
await payload.
|
|
4095
|
-
collection: slugs.pendingEmails,
|
|
4376
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4096
4377
|
id: Number(id),
|
|
4097
4378
|
data: {
|
|
4098
4379
|
status: "ignored",
|
|
@@ -4104,8 +4385,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4104
4385
|
return Response.json({ action: "ignored", pendingEmailId: Number(id) });
|
|
4105
4386
|
}
|
|
4106
4387
|
if (action === "create_ticket") {
|
|
4107
|
-
const newTicket = await payload.
|
|
4108
|
-
collection: slugs.tickets,
|
|
4388
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
4109
4389
|
data: {
|
|
4110
4390
|
subject: pendingEmail.subject,
|
|
4111
4391
|
client: clientId,
|
|
@@ -4116,8 +4396,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4116
4396
|
},
|
|
4117
4397
|
overrideAccess: true
|
|
4118
4398
|
});
|
|
4119
|
-
await payload.
|
|
4120
|
-
collection: slugs.ticketMessages,
|
|
4399
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
4121
4400
|
data: {
|
|
4122
4401
|
ticket: newTicket.id,
|
|
4123
4402
|
body: pendingEmail.body,
|
|
@@ -4144,8 +4423,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4144
4423
|
} catch (err) {
|
|
4145
4424
|
console.error("[pending-email-process] Failed to send notification:", err);
|
|
4146
4425
|
}
|
|
4147
|
-
await payload.
|
|
4148
|
-
collection: slugs.pendingEmails,
|
|
4426
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4149
4427
|
id: Number(id),
|
|
4150
4428
|
data: {
|
|
4151
4429
|
status: "processed",
|
|
@@ -4165,8 +4443,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4165
4443
|
if (!ticketId) {
|
|
4166
4444
|
return Response.json({ error: "ticketId is required for add_to_ticket" }, { status: 400 });
|
|
4167
4445
|
}
|
|
4168
|
-
const targetTicket = await payload.
|
|
4169
|
-
collection: slugs.tickets,
|
|
4446
|
+
const targetTicket = await dbFindByID(payload, slugs.tickets, {
|
|
4170
4447
|
id: ticketId,
|
|
4171
4448
|
depth: 0,
|
|
4172
4449
|
overrideAccess: true
|
|
@@ -4174,8 +4451,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4174
4451
|
if (!targetTicket) {
|
|
4175
4452
|
return Response.json({ error: "Target ticket not found" }, { status: 404 });
|
|
4176
4453
|
}
|
|
4177
|
-
await payload.
|
|
4178
|
-
collection: slugs.ticketMessages,
|
|
4454
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
4179
4455
|
data: {
|
|
4180
4456
|
ticket: ticketId,
|
|
4181
4457
|
body: pendingEmail.body,
|
|
@@ -4187,15 +4463,13 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4187
4463
|
overrideAccess: true
|
|
4188
4464
|
});
|
|
4189
4465
|
if (targetTicket.status === "resolved") {
|
|
4190
|
-
await payload.
|
|
4191
|
-
collection: slugs.tickets,
|
|
4466
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
4192
4467
|
id: ticketId,
|
|
4193
4468
|
data: { status: "open" },
|
|
4194
4469
|
overrideAccess: true
|
|
4195
4470
|
});
|
|
4196
4471
|
}
|
|
4197
|
-
await payload.
|
|
4198
|
-
collection: slugs.pendingEmails,
|
|
4472
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4199
4473
|
id: Number(id),
|
|
4200
4474
|
data: {
|
|
4201
4475
|
status: "processed",
|
|
@@ -4248,8 +4522,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4248
4522
|
if (!messageId) {
|
|
4249
4523
|
return Response.json({ error: "messageId requis" }, { status: 400 });
|
|
4250
4524
|
}
|
|
4251
|
-
const message = await payload.
|
|
4252
|
-
collection: slugs.ticketMessages,
|
|
4525
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
4253
4526
|
id: messageId,
|
|
4254
4527
|
depth: 0,
|
|
4255
4528
|
overrideAccess: true
|
|
@@ -4258,8 +4531,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4258
4531
|
return Response.json({ error: "Message introuvable" }, { status: 404 });
|
|
4259
4532
|
}
|
|
4260
4533
|
const ticketId = typeof message.ticket === "object" ? message.ticket.id : message.ticket;
|
|
4261
|
-
const ticket = await payload.
|
|
4262
|
-
collection: slugs.tickets,
|
|
4534
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
4263
4535
|
id: ticketId,
|
|
4264
4536
|
depth: 1,
|
|
4265
4537
|
overrideAccess: true
|
|
@@ -4385,8 +4657,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4385
4657
|
let created = 0;
|
|
4386
4658
|
let skipped = 0;
|
|
4387
4659
|
for (const article of KB_ARTICLES) {
|
|
4388
|
-
const existing = await payload.
|
|
4389
|
-
collection: slugs.knowledgeBase,
|
|
4660
|
+
const existing = await dbFind(payload, slugs.knowledgeBase, {
|
|
4390
4661
|
where: { slug: { equals: article.slug } },
|
|
4391
4662
|
limit: 1,
|
|
4392
4663
|
depth: 0,
|
|
@@ -4413,8 +4684,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4413
4684
|
version: 1
|
|
4414
4685
|
}
|
|
4415
4686
|
};
|
|
4416
|
-
await payload.
|
|
4417
|
-
collection: slugs.knowledgeBase,
|
|
4687
|
+
await dbCreate(payload, slugs.knowledgeBase, {
|
|
4418
4688
|
data: {
|
|
4419
4689
|
title: article.title,
|
|
4420
4690
|
slug: article.slug,
|
|
@@ -4469,8 +4739,7 @@ function createLoginEndpoint(slugs) {
|
|
|
4469
4739
|
collection: slugs.supportClients,
|
|
4470
4740
|
data: { email, password }
|
|
4471
4741
|
});
|
|
4472
|
-
payload.
|
|
4473
|
-
collection: slugs.authLogs,
|
|
4742
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4474
4743
|
data: { email, success: true, action: "login", ipAddress: ip, userAgent },
|
|
4475
4744
|
overrideAccess: true
|
|
4476
4745
|
}).catch(() => {
|
|
@@ -4494,12 +4763,14 @@ function createLoginEndpoint(slugs) {
|
|
|
4494
4763
|
);
|
|
4495
4764
|
} catch (err) {
|
|
4496
4765
|
const errorMessage = err instanceof Error ? err.message : "Erreur inconnue";
|
|
4766
|
+
if (errorMessage.includes("2FA_REQUIRED")) {
|
|
4767
|
+
return Response.json({ requires2FA: true }, { status: 200 });
|
|
4768
|
+
}
|
|
4497
4769
|
let errorReason = "Identifiants incorrects";
|
|
4498
4770
|
if (errorMessage.includes("locked") || errorMessage.includes("verrouill\xE9") || errorMessage.includes("Too many")) {
|
|
4499
4771
|
errorReason = "Compte verrouill\xE9 (trop de tentatives)";
|
|
4500
4772
|
}
|
|
4501
|
-
payload.
|
|
4502
|
-
collection: slugs.authLogs,
|
|
4773
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4503
4774
|
data: { email, success: false, action: "login", errorReason, ipAddress: ip, userAgent },
|
|
4504
4775
|
overrideAccess: true
|
|
4505
4776
|
}).catch(() => {
|
|
@@ -4520,7 +4791,10 @@ function generateSecureCode() {
|
|
|
4520
4791
|
return String(num);
|
|
4521
4792
|
}
|
|
4522
4793
|
function hashCode(code) {
|
|
4523
|
-
const secret = process.env.PAYLOAD_SECRET
|
|
4794
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
4795
|
+
if (!secret) {
|
|
4796
|
+
throw new Error("[support][2fa] PAYLOAD_SECRET is not set \u2014 refusing to operate 2FA with an insecure fallback secret");
|
|
4797
|
+
}
|
|
4524
4798
|
return crypto3.createHmac("sha256", secret).update(code).digest("hex");
|
|
4525
4799
|
}
|
|
4526
4800
|
function createAuth2faEndpoint(slugs) {
|
|
@@ -4545,8 +4819,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4545
4819
|
if (sendLimiter.check(email)) {
|
|
4546
4820
|
return Response.json(genericSendResponse);
|
|
4547
4821
|
}
|
|
4548
|
-
const clients = await payload.
|
|
4549
|
-
collection: slugs.supportClients,
|
|
4822
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4550
4823
|
where: { email: { equals: email } },
|
|
4551
4824
|
limit: 1,
|
|
4552
4825
|
depth: 0,
|
|
@@ -4559,8 +4832,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4559
4832
|
const plainCode = generateSecureCode();
|
|
4560
4833
|
const twoFactorCode = hashCode(plainCode);
|
|
4561
4834
|
const twoFactorExpiry = new Date(Date.now() + 10 * 60 * 1e3).toISOString();
|
|
4562
|
-
await payload.
|
|
4563
|
-
collection: slugs.supportClients,
|
|
4835
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4564
4836
|
id: client.id,
|
|
4565
4837
|
data: { twoFactorCode, twoFactorExpiry },
|
|
4566
4838
|
overrideAccess: true
|
|
@@ -4591,8 +4863,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4591
4863
|
{ status: 429 }
|
|
4592
4864
|
);
|
|
4593
4865
|
}
|
|
4594
|
-
const clients = await payload.
|
|
4595
|
-
collection: slugs.supportClients,
|
|
4866
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4596
4867
|
where: { email: { equals: email } },
|
|
4597
4868
|
limit: 1,
|
|
4598
4869
|
depth: 0,
|
|
@@ -4608,8 +4879,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4608
4879
|
return Response.json({ error: "Aucun code en attente" }, { status: 400 });
|
|
4609
4880
|
}
|
|
4610
4881
|
if (/* @__PURE__ */ new Date() > new Date(storedExpiry)) {
|
|
4611
|
-
await payload.
|
|
4612
|
-
collection: slugs.supportClients,
|
|
4882
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4613
4883
|
id: client.id,
|
|
4614
4884
|
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4615
4885
|
overrideAccess: true
|
|
@@ -4623,10 +4893,9 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4623
4893
|
if (submittedBuffer.length !== storedBuffer.length || !crypto3__default.default.timingSafeEqual(submittedBuffer, storedBuffer)) {
|
|
4624
4894
|
return Response.json({ error: "Code incorrect" }, { status: 400 });
|
|
4625
4895
|
}
|
|
4626
|
-
await payload.
|
|
4627
|
-
collection: slugs.supportClients,
|
|
4896
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4628
4897
|
id: client.id,
|
|
4629
|
-
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4898
|
+
data: { twoFactorCode: "", twoFactorExpiry: "", twoFactorVerifiedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4630
4899
|
overrideAccess: true
|
|
4631
4900
|
});
|
|
4632
4901
|
verifyLimiter.reset(email);
|
|
@@ -4700,15 +4969,33 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4700
4969
|
if (!profile.email) {
|
|
4701
4970
|
return Response.json({ error: "no_email" }, { status: 400 });
|
|
4702
4971
|
}
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4972
|
+
if (profile.verified_email !== true) {
|
|
4973
|
+
return Response.json({ error: "email_not_verified" }, { status: 403 });
|
|
4974
|
+
}
|
|
4975
|
+
const payload$1 = req.payload;
|
|
4976
|
+
const byGoogle = await dbFind(payload$1, slugs.supportClients, {
|
|
4977
|
+
where: { googleId: { equals: profile.id } },
|
|
4707
4978
|
limit: 1,
|
|
4708
4979
|
depth: 0,
|
|
4709
4980
|
overrideAccess: true
|
|
4710
4981
|
});
|
|
4711
|
-
let clientDoc =
|
|
4982
|
+
let clientDoc = byGoogle.docs[0];
|
|
4983
|
+
if (!clientDoc) {
|
|
4984
|
+
const byEmail = await dbFind(payload$1, slugs.supportClients, {
|
|
4985
|
+
where: { email: { equals: profile.email } },
|
|
4986
|
+
limit: 1,
|
|
4987
|
+
depth: 0,
|
|
4988
|
+
overrideAccess: true
|
|
4989
|
+
});
|
|
4990
|
+
clientDoc = byEmail.docs[0];
|
|
4991
|
+
if (clientDoc && profile.id) {
|
|
4992
|
+
await dbUpdate(payload$1, slugs.supportClients, {
|
|
4993
|
+
id: clientDoc.id,
|
|
4994
|
+
data: { googleId: profile.id },
|
|
4995
|
+
overrideAccess: true
|
|
4996
|
+
});
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4712
4999
|
if (!clientDoc) {
|
|
4713
5000
|
const allowedDomains = options?.allowedEmailDomains;
|
|
4714
5001
|
if (allowedDomains && allowedDomains.length > 0) {
|
|
@@ -4726,43 +5013,64 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4726
5013
|
const autoPassword = crypto3__default.default.randomBytes(48).toString("base64url");
|
|
4727
5014
|
const fullName = profile.name || profile.email.split("@")[0];
|
|
4728
5015
|
const nameParts = fullName.split(" ");
|
|
4729
|
-
clientDoc = await payload.
|
|
4730
|
-
collection: slugs.supportClients,
|
|
5016
|
+
clientDoc = await dbCreate(payload$1, slugs.supportClients, {
|
|
4731
5017
|
data: {
|
|
4732
5018
|
email: profile.email,
|
|
4733
5019
|
firstName: nameParts[0] || fullName,
|
|
4734
5020
|
lastName: nameParts.slice(1).join(" ") || "-",
|
|
4735
5021
|
company: fullName,
|
|
5022
|
+
googleId: profile.id,
|
|
4736
5023
|
password: autoPassword
|
|
4737
5024
|
},
|
|
4738
5025
|
overrideAccess: true
|
|
4739
5026
|
});
|
|
4740
5027
|
}
|
|
4741
|
-
const
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
id: clientDoc.id,
|
|
4745
|
-
data: { password: tempPassword },
|
|
4746
|
-
overrideAccess: true
|
|
4747
|
-
});
|
|
4748
|
-
const loginResult = await payload.login({
|
|
4749
|
-
collection: slugs.supportClients,
|
|
4750
|
-
data: { email: profile.email, password: tempPassword }
|
|
4751
|
-
});
|
|
4752
|
-
const postLoginPassword = crypto3__default.default.randomBytes(48).toString("base64url");
|
|
4753
|
-
await payload.update({
|
|
4754
|
-
collection: slugs.supportClients,
|
|
4755
|
-
id: clientDoc.id,
|
|
4756
|
-
data: { password: postLoginPassword },
|
|
4757
|
-
overrideAccess: true
|
|
4758
|
-
});
|
|
4759
|
-
if (!loginResult.token) {
|
|
4760
|
-
return Response.json({ error: "login_failed" }, { status: 400 });
|
|
5028
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
5029
|
+
if (!secret) {
|
|
5030
|
+
return Response.json({ error: "server_misconfigured" }, { status: 500 });
|
|
4761
5031
|
}
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
5032
|
+
const collectionConfig = payload$1.collections[slugs.supportClients].config;
|
|
5033
|
+
const tokenExpiration = collectionConfig.auth?.tokenExpiration ?? 7200;
|
|
5034
|
+
let sid;
|
|
5035
|
+
if (collectionConfig.auth?.useSessions) {
|
|
5036
|
+
sid = crypto3__default.default.randomUUID();
|
|
5037
|
+
const now = /* @__PURE__ */ new Date();
|
|
5038
|
+
const expiresAt = new Date(now.getTime() + tokenExpiration * 1e3);
|
|
5039
|
+
const fresh = await dbFindByID(payload$1, slugs.supportClients, {
|
|
5040
|
+
id: clientDoc.id,
|
|
5041
|
+
depth: 0,
|
|
5042
|
+
overrideAccess: true,
|
|
5043
|
+
showHiddenFields: true
|
|
5044
|
+
});
|
|
5045
|
+
const kept = Array.isArray(fresh?.sessions) ? fresh.sessions.filter((s) => new Date(s.expiresAt) > now) : [];
|
|
5046
|
+
await payload$1.db.updateOne({
|
|
5047
|
+
collection: slugs.supportClients,
|
|
5048
|
+
id: clientDoc.id,
|
|
5049
|
+
data: {
|
|
5050
|
+
sessions: [
|
|
5051
|
+
...kept,
|
|
5052
|
+
{ id: sid, createdAt: now.toISOString(), expiresAt: expiresAt.toISOString() }
|
|
5053
|
+
]
|
|
5054
|
+
},
|
|
5055
|
+
returning: false
|
|
5056
|
+
});
|
|
5057
|
+
}
|
|
5058
|
+
const fieldsToSign = payload.getFieldsToSign({
|
|
5059
|
+
collectionConfig,
|
|
5060
|
+
email: clientDoc.email,
|
|
5061
|
+
sid,
|
|
5062
|
+
user: { ...clientDoc, collection: slugs.supportClients }
|
|
5063
|
+
});
|
|
5064
|
+
const { token, exp } = await payload.jwtSign({ fieldsToSign, secret, tokenExpiration });
|
|
5065
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
5066
|
+
const cookieSecure = process.env.NODE_ENV === "production";
|
|
5067
|
+
headers.append(
|
|
5068
|
+
"Set-Cookie",
|
|
5069
|
+
`payload-token=${token}; HttpOnly; ${cookieSecure ? "Secure; " : ""}SameSite=Lax; Path=/; Max-Age=${tokenExpiration}`
|
|
5070
|
+
);
|
|
5071
|
+
return new Response(JSON.stringify({ token, user: clientDoc, exp }), {
|
|
5072
|
+
status: 200,
|
|
5073
|
+
headers
|
|
4766
5074
|
});
|
|
4767
5075
|
}
|
|
4768
5076
|
return Response.json({ error: "Action invalide" }, { status: 400 });
|
|
@@ -4803,8 +5111,7 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4803
5111
|
);
|
|
4804
5112
|
}
|
|
4805
5113
|
const clientId = req.user.id;
|
|
4806
|
-
const tickets = await payload.
|
|
4807
|
-
collection: slugs.tickets,
|
|
5114
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4808
5115
|
where: { client: { equals: clientId } },
|
|
4809
5116
|
limit: 1e4,
|
|
4810
5117
|
depth: 0,
|
|
@@ -4813,39 +5120,32 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4813
5120
|
});
|
|
4814
5121
|
const ticketIds = tickets.docs.map((t) => t.id);
|
|
4815
5122
|
if (ticketIds.length > 0) {
|
|
4816
|
-
await payload.
|
|
4817
|
-
collection: slugs.ticketMessages,
|
|
5123
|
+
await dbDelete(payload, slugs.ticketMessages, {
|
|
4818
5124
|
where: { ticket: { in: ticketIds } },
|
|
4819
5125
|
overrideAccess: true
|
|
4820
5126
|
});
|
|
4821
|
-
await payload.
|
|
4822
|
-
collection: slugs.ticketActivityLog,
|
|
5127
|
+
await dbDelete(payload, slugs.ticketActivityLog, {
|
|
4823
5128
|
where: { ticket: { in: ticketIds } },
|
|
4824
5129
|
overrideAccess: true
|
|
4825
5130
|
});
|
|
4826
|
-
await payload.
|
|
4827
|
-
collection: slugs.timeEntries,
|
|
5131
|
+
await dbDelete(payload, slugs.timeEntries, {
|
|
4828
5132
|
where: { ticket: { in: ticketIds } },
|
|
4829
5133
|
overrideAccess: true
|
|
4830
5134
|
});
|
|
4831
|
-
await payload.
|
|
4832
|
-
collection: slugs.tickets,
|
|
5135
|
+
await dbDelete(payload, slugs.tickets, {
|
|
4833
5136
|
where: { client: { equals: clientId } },
|
|
4834
5137
|
overrideAccess: true
|
|
4835
5138
|
});
|
|
4836
5139
|
}
|
|
4837
|
-
await payload.
|
|
4838
|
-
collection: slugs.satisfactionSurveys,
|
|
5140
|
+
await dbDelete(payload, slugs.satisfactionSurveys, {
|
|
4839
5141
|
where: { client: { equals: clientId } },
|
|
4840
5142
|
overrideAccess: true
|
|
4841
5143
|
});
|
|
4842
|
-
await payload.
|
|
4843
|
-
collection: slugs.chatMessages,
|
|
5144
|
+
await dbDelete(payload, slugs.chatMessages, {
|
|
4844
5145
|
where: { client: { equals: clientId } },
|
|
4845
5146
|
overrideAccess: true
|
|
4846
5147
|
});
|
|
4847
|
-
await payload.
|
|
4848
|
-
collection: slugs.supportClients,
|
|
5148
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4849
5149
|
id: clientId,
|
|
4850
5150
|
overrideAccess: true
|
|
4851
5151
|
});
|
|
@@ -4886,8 +5186,8 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4886
5186
|
return Response.json({ error: "sourceId and targetId are required and must be different" }, { status: 400 });
|
|
4887
5187
|
}
|
|
4888
5188
|
const [source, target] = await Promise.all([
|
|
4889
|
-
payload
|
|
4890
|
-
payload
|
|
5189
|
+
dbFindByID(payload, slugs.supportClients, { id: sourceId, depth: 0, overrideAccess: true }),
|
|
5190
|
+
dbFindByID(payload, slugs.supportClients, { id: targetId, depth: 0, overrideAccess: true })
|
|
4891
5191
|
]);
|
|
4892
5192
|
if (!source || !target) {
|
|
4893
5193
|
return Response.json({ error: "Source or target client not found" }, { status: 404 });
|
|
@@ -4899,63 +5199,57 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4899
5199
|
pendingEmails: 0,
|
|
4900
5200
|
satisfactionSurveys: 0
|
|
4901
5201
|
};
|
|
4902
|
-
const tickets = await payload.
|
|
4903
|
-
collection: slugs.tickets,
|
|
5202
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4904
5203
|
where: { client: { equals: sourceId } },
|
|
4905
5204
|
limit: 500,
|
|
4906
5205
|
depth: 0,
|
|
4907
5206
|
overrideAccess: true
|
|
4908
5207
|
});
|
|
4909
5208
|
for (const ticket of tickets.docs) {
|
|
4910
|
-
await payload
|
|
5209
|
+
await dbUpdate(payload, slugs.tickets, { id: ticket.id, data: { client: targetId }, overrideAccess: true });
|
|
4911
5210
|
results.tickets++;
|
|
4912
5211
|
}
|
|
4913
|
-
const messages = await payload.
|
|
4914
|
-
collection: slugs.ticketMessages,
|
|
5212
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
4915
5213
|
where: { authorClient: { equals: sourceId } },
|
|
4916
5214
|
limit: 1e3,
|
|
4917
5215
|
depth: 0,
|
|
4918
5216
|
overrideAccess: true
|
|
4919
5217
|
});
|
|
4920
5218
|
for (const msg of messages.docs) {
|
|
4921
|
-
await payload
|
|
5219
|
+
await dbUpdate(payload, slugs.ticketMessages, { id: msg.id, data: { authorClient: targetId }, overrideAccess: true });
|
|
4922
5220
|
results.ticketMessages++;
|
|
4923
5221
|
}
|
|
4924
|
-
const chats = await payload.
|
|
4925
|
-
collection: slugs.chatMessages,
|
|
5222
|
+
const chats = await dbFind(payload, slugs.chatMessages, {
|
|
4926
5223
|
where: { client: { equals: sourceId } },
|
|
4927
5224
|
limit: 1e3,
|
|
4928
5225
|
depth: 0,
|
|
4929
5226
|
overrideAccess: true
|
|
4930
5227
|
});
|
|
4931
5228
|
for (const chat of chats.docs) {
|
|
4932
|
-
await payload
|
|
5229
|
+
await dbUpdate(payload, slugs.chatMessages, { id: chat.id, data: { client: targetId }, overrideAccess: true });
|
|
4933
5230
|
results.chatMessages++;
|
|
4934
5231
|
}
|
|
4935
|
-
const pendingEmails = await payload.
|
|
4936
|
-
collection: slugs.pendingEmails,
|
|
5232
|
+
const pendingEmails = await dbFind(payload, slugs.pendingEmails, {
|
|
4937
5233
|
where: { client: { equals: sourceId } },
|
|
4938
5234
|
limit: 500,
|
|
4939
5235
|
depth: 0,
|
|
4940
5236
|
overrideAccess: true
|
|
4941
5237
|
});
|
|
4942
5238
|
for (const pe of pendingEmails.docs) {
|
|
4943
|
-
await payload
|
|
5239
|
+
await dbUpdate(payload, slugs.pendingEmails, { id: pe.id, data: { client: targetId }, overrideAccess: true });
|
|
4944
5240
|
results.pendingEmails++;
|
|
4945
5241
|
}
|
|
4946
|
-
const surveys = await payload.
|
|
4947
|
-
collection: slugs.satisfactionSurveys,
|
|
5242
|
+
const surveys = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
4948
5243
|
where: { client: { equals: sourceId } },
|
|
4949
5244
|
limit: 500,
|
|
4950
5245
|
depth: 0,
|
|
4951
5246
|
overrideAccess: true
|
|
4952
5247
|
});
|
|
4953
5248
|
for (const survey of surveys.docs) {
|
|
4954
|
-
await payload
|
|
5249
|
+
await dbUpdate(payload, slugs.satisfactionSurveys, { id: survey.id, data: { client: targetId }, overrideAccess: true });
|
|
4955
5250
|
results.satisfactionSurveys++;
|
|
4956
5251
|
}
|
|
4957
|
-
await payload.
|
|
4958
|
-
collection: slugs.supportClients,
|
|
5252
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4959
5253
|
id: sourceId,
|
|
4960
5254
|
overrideAccess: true
|
|
4961
5255
|
});
|
|
@@ -5149,8 +5443,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5149
5443
|
if (blockedEmails.includes(conversation.client.email)) {
|
|
5150
5444
|
return Response.json({ error: "Cannot create ticket from system email address" }, { status: 400 });
|
|
5151
5445
|
}
|
|
5152
|
-
const clientResult = await payload.
|
|
5153
|
-
collection: slugs.supportClients,
|
|
5446
|
+
const clientResult = await dbFind(payload, slugs.supportClients, {
|
|
5154
5447
|
where: { email: { equals: conversation.client.email } },
|
|
5155
5448
|
limit: 1,
|
|
5156
5449
|
depth: 0,
|
|
@@ -5161,8 +5454,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5161
5454
|
if (!client) {
|
|
5162
5455
|
const nameParts = conversation.client.name.split(" ");
|
|
5163
5456
|
const randomPassword = crypto3__default.default.randomBytes(48).toString("base64url");
|
|
5164
|
-
client = await payload.
|
|
5165
|
-
collection: slugs.supportClients,
|
|
5457
|
+
client = await dbCreate(payload, slugs.supportClients, {
|
|
5166
5458
|
data: {
|
|
5167
5459
|
email: conversation.client.email,
|
|
5168
5460
|
password: randomPassword,
|
|
@@ -5174,15 +5466,13 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5174
5466
|
});
|
|
5175
5467
|
isNewClient = true;
|
|
5176
5468
|
}
|
|
5177
|
-
const adminUsers = await payload.
|
|
5178
|
-
collection: slugs.users,
|
|
5469
|
+
const adminUsers = await dbFind(payload, slugs.users, {
|
|
5179
5470
|
limit: 1,
|
|
5180
5471
|
depth: 0,
|
|
5181
5472
|
overrideAccess: true
|
|
5182
5473
|
});
|
|
5183
5474
|
const adminUserId = adminUsers.docs[0]?.id;
|
|
5184
|
-
const ticket = await payload.
|
|
5185
|
-
collection: slugs.tickets,
|
|
5475
|
+
const ticket = await dbCreate(payload, slugs.tickets, {
|
|
5186
5476
|
data: {
|
|
5187
5477
|
subject: conversation.subject,
|
|
5188
5478
|
client: client.id,
|
|
@@ -5195,8 +5485,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5195
5485
|
let importedCount = 0;
|
|
5196
5486
|
for (const msg of conversation.messages) {
|
|
5197
5487
|
const isAdmin = msg.from === "admin";
|
|
5198
|
-
await payload.
|
|
5199
|
-
collection: slugs.ticketMessages,
|
|
5488
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
5200
5489
|
data: {
|
|
5201
5490
|
ticket: ticket.id,
|
|
5202
5491
|
body: msg.content,
|
|
@@ -5232,8 +5521,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5232
5521
|
// src/utils/fireWebhooks.ts
|
|
5233
5522
|
async function fireWebhooks(payload, slugs, event, data) {
|
|
5234
5523
|
try {
|
|
5235
|
-
const endpoints = await payload.
|
|
5236
|
-
collection: slugs.webhookEndpoints,
|
|
5524
|
+
const endpoints = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5237
5525
|
where: {
|
|
5238
5526
|
active: { equals: true },
|
|
5239
5527
|
events: { contains: event }
|
|
@@ -5257,8 +5545,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5257
5545
|
signal: AbortSignal.timeout(1e4)
|
|
5258
5546
|
});
|
|
5259
5547
|
try {
|
|
5260
|
-
await payload.
|
|
5261
|
-
collection: slugs.webhookEndpoints,
|
|
5548
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5262
5549
|
id: endpoint.id,
|
|
5263
5550
|
data: {
|
|
5264
5551
|
lastTriggeredAt: timestamp,
|
|
@@ -5271,8 +5558,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5271
5558
|
} catch (error) {
|
|
5272
5559
|
console.warn(`[support] Webhook delivery failed: ${endpoint.url}`, error);
|
|
5273
5560
|
try {
|
|
5274
|
-
await payload.
|
|
5275
|
-
collection: slugs.webhookEndpoints,
|
|
5561
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5276
5562
|
id: endpoint.id,
|
|
5277
5563
|
data: {
|
|
5278
5564
|
lastTriggeredAt: timestamp,
|
|
@@ -5307,8 +5593,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5307
5593
|
const settings = await readSupportSettings(payload);
|
|
5308
5594
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
|
|
5309
5595
|
const results = { processed: 0, errors: 0 };
|
|
5310
|
-
const scheduled = await payload.
|
|
5311
|
-
collection: slugs.ticketMessages,
|
|
5596
|
+
const scheduled = await dbFind(payload, slugs.ticketMessages, {
|
|
5312
5597
|
where: {
|
|
5313
5598
|
and: [
|
|
5314
5599
|
{ scheduledAt: { less_than_equal: now.toISOString() } },
|
|
@@ -5328,8 +5613,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5328
5613
|
try {
|
|
5329
5614
|
const msg = message;
|
|
5330
5615
|
const ticketId = typeof msg.ticket === "object" ? msg.ticket.id : msg.ticket;
|
|
5331
|
-
const ticket = await payload.
|
|
5332
|
-
collection: slugs.tickets,
|
|
5616
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5333
5617
|
id: ticketId,
|
|
5334
5618
|
depth: 1,
|
|
5335
5619
|
overrideAccess: true
|
|
@@ -5352,8 +5636,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5352
5636
|
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
5353
5637
|
html: `<p>Bonjour ${escapeHtml(client.firstName || "")},</p><p>Notre \xE9quipe a r\xE9pondu \xE0 votre ticket <strong>${escapeHtml(ticketNumber)}</strong>.</p><blockquote style="border-left:4px solid #2563eb;padding:12px;margin:16px 0;background:#f8fafc;">${escapeHtml(preview)}</blockquote><p><a href="${baseUrl}/support/tickets/${ticketId}">Consulter le ticket</a></p>`
|
|
5354
5638
|
});
|
|
5355
|
-
await payload.
|
|
5356
|
-
collection: slugs.ticketMessages,
|
|
5639
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5357
5640
|
id: msg.id,
|
|
5358
5641
|
data: {
|
|
5359
5642
|
scheduledSent: true,
|
|
@@ -5363,16 +5646,14 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5363
5646
|
overrideAccess: true
|
|
5364
5647
|
});
|
|
5365
5648
|
} else {
|
|
5366
|
-
await payload.
|
|
5367
|
-
collection: slugs.ticketMessages,
|
|
5649
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5368
5650
|
id: msg.id,
|
|
5369
5651
|
data: { scheduledSent: true },
|
|
5370
5652
|
overrideAccess: true
|
|
5371
5653
|
});
|
|
5372
5654
|
}
|
|
5373
5655
|
if (msg.authorType === "admin" && !msg.isInternal) {
|
|
5374
|
-
await payload.
|
|
5375
|
-
collection: slugs.tickets,
|
|
5656
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
5376
5657
|
id: ticketId,
|
|
5377
5658
|
data: { status: "waiting_client" },
|
|
5378
5659
|
overrideAccess: true
|
|
@@ -5404,6 +5685,296 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5404
5685
|
};
|
|
5405
5686
|
}
|
|
5406
5687
|
|
|
5688
|
+
// src/endpoints/process-snooze.ts
|
|
5689
|
+
function createProcessSnoozeEndpoint(slugs) {
|
|
5690
|
+
return {
|
|
5691
|
+
path: "/support/process-snooze",
|
|
5692
|
+
method: "post",
|
|
5693
|
+
handler: async (req) => {
|
|
5694
|
+
const secret = req.headers.get("x-cron-secret");
|
|
5695
|
+
const expectedSecret = process.env.CRON_SECRET;
|
|
5696
|
+
if (!expectedSecret || secret !== expectedSecret) {
|
|
5697
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5698
|
+
}
|
|
5699
|
+
try {
|
|
5700
|
+
const payload = req.payload;
|
|
5701
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
5702
|
+
const results = { processed: 0, errors: 0 };
|
|
5703
|
+
const due = await dbFind(payload, slugs.tickets, {
|
|
5704
|
+
where: {
|
|
5705
|
+
and: [
|
|
5706
|
+
{ snoozeUntil: { exists: true } },
|
|
5707
|
+
{ snoozeUntil: { less_than_equal: nowIso } }
|
|
5708
|
+
]
|
|
5709
|
+
},
|
|
5710
|
+
limit: 500,
|
|
5711
|
+
depth: 0,
|
|
5712
|
+
overrideAccess: true,
|
|
5713
|
+
select: { id: true, ticketNumber: true }
|
|
5714
|
+
});
|
|
5715
|
+
for (const ticket of due.docs) {
|
|
5716
|
+
try {
|
|
5717
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
5718
|
+
id: ticket.id,
|
|
5719
|
+
data: { snoozeUntil: null },
|
|
5720
|
+
overrideAccess: true
|
|
5721
|
+
});
|
|
5722
|
+
try {
|
|
5723
|
+
await dbCreate(payload, slugs.ticketActivityLog, {
|
|
5724
|
+
data: {
|
|
5725
|
+
ticket: ticket.id,
|
|
5726
|
+
action: "snooze_expired",
|
|
5727
|
+
detail: "Snooze \xE9chu \u2014 ticket r\xE9activ\xE9",
|
|
5728
|
+
actorType: "system"
|
|
5729
|
+
},
|
|
5730
|
+
overrideAccess: true
|
|
5731
|
+
});
|
|
5732
|
+
} catch {
|
|
5733
|
+
}
|
|
5734
|
+
results.processed++;
|
|
5735
|
+
} catch (err) {
|
|
5736
|
+
console.error("[support] Failed to wake snoozed ticket:", err);
|
|
5737
|
+
results.errors++;
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
return Response.json({ ok: true, ...results });
|
|
5741
|
+
} catch (err) {
|
|
5742
|
+
console.error("[process-snooze] Error:", err);
|
|
5743
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
};
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5749
|
+
// src/endpoints/process-digests.ts
|
|
5750
|
+
function createProcessDigestsEndpoint(slugs) {
|
|
5751
|
+
return {
|
|
5752
|
+
path: "/support/process-digests",
|
|
5753
|
+
method: "post",
|
|
5754
|
+
handler: async (req) => {
|
|
5755
|
+
const secret = req.headers.get("x-cron-secret");
|
|
5756
|
+
if (!process.env.CRON_SECRET || secret !== process.env.CRON_SECRET) {
|
|
5757
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5758
|
+
}
|
|
5759
|
+
try {
|
|
5760
|
+
const payload = req.payload;
|
|
5761
|
+
let body = {};
|
|
5762
|
+
try {
|
|
5763
|
+
body = await req.json();
|
|
5764
|
+
} catch {
|
|
5765
|
+
}
|
|
5766
|
+
const frequency = body.frequency === "weekly" ? "weekly" : "daily";
|
|
5767
|
+
const settings = await readSupportSettings(payload);
|
|
5768
|
+
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
|
|
5769
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
5770
|
+
const itemsByClient = /* @__PURE__ */ new Map();
|
|
5771
|
+
let page = 1;
|
|
5772
|
+
let hasMore = true;
|
|
5773
|
+
while (hasMore && page <= 50) {
|
|
5774
|
+
const batch = await dbFind(payload, slugs.notificationQueue, { limit: 200, page, depth: 0, overrideAccess: true, sort: "createdAt" });
|
|
5775
|
+
for (const it of batch.docs) {
|
|
5776
|
+
const raw = it;
|
|
5777
|
+
const clientId = typeof raw.client === "object" ? raw.client?.id : raw.client;
|
|
5778
|
+
if (clientId === void 0 || clientId === null) continue;
|
|
5779
|
+
const key = String(clientId);
|
|
5780
|
+
if (!itemsByClient.has(key)) itemsByClient.set(key, []);
|
|
5781
|
+
itemsByClient.get(key).push({ id: raw.id, title: raw.title, message: raw.message });
|
|
5782
|
+
}
|
|
5783
|
+
hasMore = batch.hasNextPage ?? false;
|
|
5784
|
+
page++;
|
|
5785
|
+
}
|
|
5786
|
+
const results = { clients: 0, items: 0, errors: 0 };
|
|
5787
|
+
for (const [clientId, items] of itemsByClient) {
|
|
5788
|
+
try {
|
|
5789
|
+
const client = await dbFindByID(payload, slugs.supportClients, { id: clientId, depth: 0, overrideAccess: true });
|
|
5790
|
+
if (!client || client.notificationFrequency !== frequency) continue;
|
|
5791
|
+
if (client.email) {
|
|
5792
|
+
const rows = items.map((i) => `<tr><td style="padding:8px 0;font-size:14px;color:#1e293b;border-bottom:1px solid #f1f5f9;"><strong>${escapeHtml(i.title || "")}</strong><br/><span style="color:#64748b;font-size:13px;">${escapeHtml(i.message || "")}</span></td></tr>`).join("");
|
|
5793
|
+
const html = emailWrapper("Recapitulatif de vos tickets", [
|
|
5794
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
|
|
5795
|
+
emailParagraph(`Voici le recapitulatif de l'activite sur vos tickets (${items.length} notification${items.length > 1 ? "s" : ""}) :`),
|
|
5796
|
+
`<table cellpadding="0" cellspacing="0" border="0" width="100%">${rows}</table>`,
|
|
5797
|
+
...baseUrl ? [emailParagraph(`<a href="${baseUrl}/support/dashboard">Acceder a votre espace support</a>`)] : []
|
|
5798
|
+
].join(""), { kind: "system" });
|
|
5799
|
+
await payload.sendEmail({ to: client.email, ...replyTo ? { replyTo } : {}, subject: `Recapitulatif support (${items.length})`, html });
|
|
5800
|
+
}
|
|
5801
|
+
for (const i of items) {
|
|
5802
|
+
try {
|
|
5803
|
+
await dbDelete(payload, slugs.notificationQueue, { id: i.id, overrideAccess: true });
|
|
5804
|
+
} catch {
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
results.clients++;
|
|
5808
|
+
results.items += items.length;
|
|
5809
|
+
} catch (err) {
|
|
5810
|
+
console.error("[process-digests] client error:", err);
|
|
5811
|
+
results.errors++;
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
return Response.json({ ok: true, frequency, ...results });
|
|
5815
|
+
} catch (err) {
|
|
5816
|
+
console.error("[process-digests] Error:", err);
|
|
5817
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
};
|
|
5821
|
+
}
|
|
5822
|
+
var PROVIDERS = ["whatsapp", "messenger"];
|
|
5823
|
+
function createChannelsWebhookEndpoint(slugs) {
|
|
5824
|
+
return {
|
|
5825
|
+
path: "/support/channels/webhook",
|
|
5826
|
+
method: "post",
|
|
5827
|
+
handler: async (req) => {
|
|
5828
|
+
const secret = req.headers.get("x-channel-secret");
|
|
5829
|
+
if (!process.env.CHANNELS_WEBHOOK_SECRET || secret !== process.env.CHANNELS_WEBHOOK_SECRET) {
|
|
5830
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5831
|
+
}
|
|
5832
|
+
let body;
|
|
5833
|
+
try {
|
|
5834
|
+
body = await req.json();
|
|
5835
|
+
} catch {
|
|
5836
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5837
|
+
}
|
|
5838
|
+
const provider = body.provider;
|
|
5839
|
+
const from = (body.from || "").trim();
|
|
5840
|
+
const text = (body.text || "").trim();
|
|
5841
|
+
if (!PROVIDERS.includes(provider) || !from || !text) {
|
|
5842
|
+
return Response.json({ error: "provider, from et text requis." }, { status: 400 });
|
|
5843
|
+
}
|
|
5844
|
+
try {
|
|
5845
|
+
const payload = req.payload;
|
|
5846
|
+
const email = `${from}@${provider}.channel`;
|
|
5847
|
+
const existing = await dbFind(payload, slugs.supportClients, { where: { email: { equals: email } }, limit: 1, depth: 0, overrideAccess: true });
|
|
5848
|
+
let clientId;
|
|
5849
|
+
if (existing.docs.length > 0) {
|
|
5850
|
+
clientId = existing.docs[0].id;
|
|
5851
|
+
} else {
|
|
5852
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
5853
|
+
data: { email, firstName: body.name || provider, lastName: from.slice(0, 40), company: provider, password: crypto3.randomBytes(16).toString("hex") },
|
|
5854
|
+
overrideAccess: true
|
|
5855
|
+
});
|
|
5856
|
+
clientId = created.id;
|
|
5857
|
+
}
|
|
5858
|
+
const open = await dbFind(payload, slugs.tickets, {
|
|
5859
|
+
where: { and: [{ client: { equals: clientId } }, { source: { equals: provider } }, { status: { in: ["open", "waiting_client"] } }] },
|
|
5860
|
+
sort: "-createdAt",
|
|
5861
|
+
limit: 1,
|
|
5862
|
+
depth: 0,
|
|
5863
|
+
overrideAccess: true
|
|
5864
|
+
});
|
|
5865
|
+
let ticketId;
|
|
5866
|
+
let createdTicket = false;
|
|
5867
|
+
if (open.docs.length > 0) {
|
|
5868
|
+
ticketId = open.docs[0].id;
|
|
5869
|
+
} else {
|
|
5870
|
+
const t = await dbCreate(payload, slugs.tickets, {
|
|
5871
|
+
data: { subject: text.slice(0, 80), client: clientId, status: "open", priority: "normal", source: provider },
|
|
5872
|
+
overrideAccess: true
|
|
5873
|
+
});
|
|
5874
|
+
ticketId = t.id;
|
|
5875
|
+
createdTicket = true;
|
|
5876
|
+
}
|
|
5877
|
+
const msg = await dbCreate(payload, slugs.ticketMessages, {
|
|
5878
|
+
data: { ticket: ticketId, body: text, authorType: "client", authorClient: clientId },
|
|
5879
|
+
overrideAccess: true
|
|
5880
|
+
});
|
|
5881
|
+
return Response.json({ ok: true, ticketId, createdTicket, messageId: msg.id });
|
|
5882
|
+
} catch (err) {
|
|
5883
|
+
console.error("[channels] Error:", err);
|
|
5884
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
};
|
|
5888
|
+
}
|
|
5889
|
+
var configured = false;
|
|
5890
|
+
function ensureVapid() {
|
|
5891
|
+
if (configured) return true;
|
|
5892
|
+
const publicKey = process.env.VAPID_PUBLIC_KEY;
|
|
5893
|
+
const privateKey = process.env.VAPID_PRIVATE_KEY;
|
|
5894
|
+
if (!publicKey || !privateKey) return false;
|
|
5895
|
+
webpush__default.default.setVapidDetails(process.env.VAPID_SUBJECT || "mailto:support@example.com", publicKey, privateKey);
|
|
5896
|
+
configured = true;
|
|
5897
|
+
return true;
|
|
5898
|
+
}
|
|
5899
|
+
function getVapidPublicKey() {
|
|
5900
|
+
return process.env.VAPID_PUBLIC_KEY || null;
|
|
5901
|
+
}
|
|
5902
|
+
async function sendPushToUser(payload, slugs, userId, notification) {
|
|
5903
|
+
if (!ensureVapid()) return { sent: 0 };
|
|
5904
|
+
let sent = 0;
|
|
5905
|
+
try {
|
|
5906
|
+
const subs = await dbFind(payload, slugs.pushSubscriptions, { where: { user: { equals: userId } }, limit: 100, depth: 0, overrideAccess: true });
|
|
5907
|
+
for (const s of subs.docs) {
|
|
5908
|
+
const row = s;
|
|
5909
|
+
if (!row.endpoint || !row.p256dh || !row.auth) continue;
|
|
5910
|
+
try {
|
|
5911
|
+
await webpush__default.default.sendNotification(
|
|
5912
|
+
{ endpoint: row.endpoint, keys: { p256dh: row.p256dh, auth: row.auth } },
|
|
5913
|
+
JSON.stringify(notification)
|
|
5914
|
+
);
|
|
5915
|
+
sent++;
|
|
5916
|
+
} catch (err) {
|
|
5917
|
+
const code = err?.statusCode;
|
|
5918
|
+
if (code === 404 || code === 410) {
|
|
5919
|
+
try {
|
|
5920
|
+
await dbDelete(payload, slugs.pushSubscriptions, { id: row.id, overrideAccess: true });
|
|
5921
|
+
} catch {
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
} catch (err) {
|
|
5927
|
+
console.error("[support] Push send failed:", err);
|
|
5928
|
+
}
|
|
5929
|
+
return { sent };
|
|
5930
|
+
}
|
|
5931
|
+
|
|
5932
|
+
// src/endpoints/push.ts
|
|
5933
|
+
function createVapidKeyEndpoint() {
|
|
5934
|
+
return {
|
|
5935
|
+
path: "/support/push/vapid-public-key",
|
|
5936
|
+
method: "get",
|
|
5937
|
+
handler: async () => Response.json({ publicKey: getVapidPublicKey() })
|
|
5938
|
+
};
|
|
5939
|
+
}
|
|
5940
|
+
function createPushSubscribeEndpoint(slugs) {
|
|
5941
|
+
return {
|
|
5942
|
+
path: "/support/push/subscribe",
|
|
5943
|
+
method: "post",
|
|
5944
|
+
handler: async (req) => {
|
|
5945
|
+
try {
|
|
5946
|
+
requireAdmin(req, slugs);
|
|
5947
|
+
let body;
|
|
5948
|
+
try {
|
|
5949
|
+
body = await req.json();
|
|
5950
|
+
} catch {
|
|
5951
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5952
|
+
}
|
|
5953
|
+
const sub = body?.subscription;
|
|
5954
|
+
const endpoint = sub?.endpoint;
|
|
5955
|
+
const p256dh = sub?.keys?.p256dh;
|
|
5956
|
+
const auth = sub?.keys?.auth;
|
|
5957
|
+
if (!endpoint || !p256dh || !auth) {
|
|
5958
|
+
return Response.json({ error: "subscription invalide (endpoint + keys requis)." }, { status: 400 });
|
|
5959
|
+
}
|
|
5960
|
+
const data = { user: req.user.id, endpoint, p256dh, auth, userAgent: req.headers.get("user-agent") || "" };
|
|
5961
|
+
const existing = await dbFind(req.payload, slugs.pushSubscriptions, { where: { endpoint: { equals: endpoint } }, limit: 1, depth: 0, overrideAccess: true });
|
|
5962
|
+
if (existing.docs.length > 0) {
|
|
5963
|
+
await dbUpdate(req.payload, slugs.pushSubscriptions, { id: existing.docs[0].id, data, overrideAccess: true });
|
|
5964
|
+
} else {
|
|
5965
|
+
await dbCreate(req.payload, slugs.pushSubscriptions, { data, overrideAccess: true });
|
|
5966
|
+
}
|
|
5967
|
+
return Response.json({ ok: true });
|
|
5968
|
+
} catch (error) {
|
|
5969
|
+
const authResponse = handleAuthError(error);
|
|
5970
|
+
if (authResponse) return authResponse;
|
|
5971
|
+
console.error("[push] Error:", error);
|
|
5972
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
};
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5407
5978
|
// src/endpoints/user-prefs.ts
|
|
5408
5979
|
var PREF_KEY_PREFIX = "support-user-prefs";
|
|
5409
5980
|
var DEFAULT_USER_PREFS2 = {
|
|
@@ -5419,8 +5990,7 @@ function createUserPrefsGetEndpoint(slugs) {
|
|
|
5419
5990
|
const payload = req.payload;
|
|
5420
5991
|
requireAdmin(req, slugs);
|
|
5421
5992
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5422
|
-
const prefs = await payload
|
|
5423
|
-
collection: "payload-preferences",
|
|
5993
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
5424
5994
|
where: { key: { equals: key } },
|
|
5425
5995
|
limit: 1,
|
|
5426
5996
|
depth: 0,
|
|
@@ -5454,8 +6024,7 @@ function createUserPrefsPostEndpoint(slugs) {
|
|
|
5454
6024
|
requireAdmin(req, slugs);
|
|
5455
6025
|
const body = await req.json();
|
|
5456
6026
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5457
|
-
const existing = await payload
|
|
5458
|
-
collection: "payload-preferences",
|
|
6027
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
5459
6028
|
where: { key: { equals: key } },
|
|
5460
6029
|
limit: 1,
|
|
5461
6030
|
depth: 0,
|
|
@@ -5516,8 +6085,7 @@ function createEscalateEndpoint(slugs) {
|
|
|
5516
6085
|
if (Number.isNaN(ticketId)) {
|
|
5517
6086
|
return Response.json({ error: "invalid-id" }, { status: 400 });
|
|
5518
6087
|
}
|
|
5519
|
-
const ticket = await req.payload.
|
|
5520
|
-
collection: slugs.tickets,
|
|
6088
|
+
const ticket = await dbUpdate(req.payload, slugs.tickets, {
|
|
5521
6089
|
id: ticketId,
|
|
5522
6090
|
data: { status: "escalated" },
|
|
5523
6091
|
overrideAccess: true
|
|
@@ -5565,8 +6133,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5565
6133
|
{ status: 400 }
|
|
5566
6134
|
);
|
|
5567
6135
|
}
|
|
5568
|
-
const ticket = await payload.
|
|
5569
|
-
collection: slugs.tickets,
|
|
6136
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5570
6137
|
id: ticketId,
|
|
5571
6138
|
depth: 0,
|
|
5572
6139
|
overrideAccess: true
|
|
@@ -5578,8 +6145,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5578
6145
|
if (ticketClientId !== req.user.id) {
|
|
5579
6146
|
return Response.json({ error: "forbidden" }, { status: 403 });
|
|
5580
6147
|
}
|
|
5581
|
-
const existing = await payload
|
|
5582
|
-
collection: "ticket-feedback",
|
|
6148
|
+
const existing = await dbFind(payload, "ticket-feedback", {
|
|
5583
6149
|
where: { ticket: { equals: ticketId } },
|
|
5584
6150
|
limit: 1,
|
|
5585
6151
|
depth: 0,
|
|
@@ -5591,8 +6157,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5591
6157
|
{ status: 409 }
|
|
5592
6158
|
);
|
|
5593
6159
|
}
|
|
5594
|
-
const feedback = await payload
|
|
5595
|
-
collection: "ticket-feedback",
|
|
6160
|
+
const feedback = await dbCreate(payload, "ticket-feedback", {
|
|
5596
6161
|
data: {
|
|
5597
6162
|
ticket: ticketId,
|
|
5598
6163
|
client: req.user.id,
|
|
@@ -5616,6 +6181,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5616
6181
|
// src/endpoints/transfer-ticket.ts
|
|
5617
6182
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5618
6183
|
var MAX_TRANSFERS_PER_DAY = 5;
|
|
6184
|
+
var transferLimiter = new RateLimiter(24 * 60 * 60 * 1e3, MAX_TRANSFERS_PER_DAY);
|
|
5619
6185
|
function createTransferTicketEndpoint(slugs) {
|
|
5620
6186
|
return {
|
|
5621
6187
|
path: "/support/tickets/:id/transfer",
|
|
@@ -5640,8 +6206,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5640
6206
|
const includeAttachments = body.includeAttachments !== false;
|
|
5641
6207
|
const customMessage = typeof body.message === "string" ? body.message.slice(0, 1e3) : "";
|
|
5642
6208
|
const payload = req.payload;
|
|
5643
|
-
const ticket = await payload.
|
|
5644
|
-
collection: slugs.tickets,
|
|
6209
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5645
6210
|
id: ticketId,
|
|
5646
6211
|
depth: 1,
|
|
5647
6212
|
overrideAccess: true
|
|
@@ -5652,10 +6217,15 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5652
6217
|
if (!isAdmin && !isOwner) {
|
|
5653
6218
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5654
6219
|
}
|
|
6220
|
+
if (transferLimiter.check(`${req.user.id}:${ticketId}`)) {
|
|
6221
|
+
return Response.json(
|
|
6222
|
+
{ error: `Limite atteinte (${MAX_TRANSFERS_PER_DAY} transferts par 24h sur ce ticket)` },
|
|
6223
|
+
{ status: 429 }
|
|
6224
|
+
);
|
|
6225
|
+
}
|
|
5655
6226
|
try {
|
|
5656
6227
|
const since = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
5657
|
-
const existing = await payload.
|
|
5658
|
-
collection: slugs.emailLogs,
|
|
6228
|
+
const existing = await dbCount(payload, slugs.emailLogs, {
|
|
5659
6229
|
where: {
|
|
5660
6230
|
and: [
|
|
5661
6231
|
{ action: { equals: "transfer" } },
|
|
@@ -5673,8 +6243,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5673
6243
|
}
|
|
5674
6244
|
} catch {
|
|
5675
6245
|
}
|
|
5676
|
-
const messages = await payload.
|
|
5677
|
-
collection: slugs.ticketMessages,
|
|
6246
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
5678
6247
|
where: {
|
|
5679
6248
|
and: [
|
|
5680
6249
|
{ ticket: { equals: ticketId } },
|
|
@@ -5754,8 +6323,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5754
6323
|
html
|
|
5755
6324
|
});
|
|
5756
6325
|
try {
|
|
5757
|
-
await payload.
|
|
5758
|
-
collection: slugs.emailLogs,
|
|
6326
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5759
6327
|
data: {
|
|
5760
6328
|
status: "success",
|
|
5761
6329
|
action: "transfer",
|
|
@@ -5795,6 +6363,8 @@ function statusToLabel(status) {
|
|
|
5795
6363
|
}
|
|
5796
6364
|
}
|
|
5797
6365
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6366
|
+
var MAX_COLLABORATORS_PER_TICKET = 20;
|
|
6367
|
+
var inviteLimiter = new RateLimiter(60 * 60 * 1e3, 10);
|
|
5798
6368
|
function createInviteCollaboratorEndpoint(slugs) {
|
|
5799
6369
|
return {
|
|
5800
6370
|
path: "/support/tickets/:id/invite",
|
|
@@ -5818,8 +6388,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5818
6388
|
}
|
|
5819
6389
|
const role = body.role === "collaborator" ? "collaborator" : "viewer";
|
|
5820
6390
|
const payload = req.payload;
|
|
5821
|
-
const ticket = await payload.
|
|
5822
|
-
collection: slugs.tickets,
|
|
6391
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5823
6392
|
id: ticketId,
|
|
5824
6393
|
depth: 1,
|
|
5825
6394
|
overrideAccess: true
|
|
@@ -5831,13 +6400,22 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5831
6400
|
if (!isAdmin && !isOwner) {
|
|
5832
6401
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5833
6402
|
}
|
|
6403
|
+
if (inviteLimiter.check(String(req.user.id))) {
|
|
6404
|
+
return Response.json({ error: "Trop d'invitations. R\xE9essayez plus tard." }, { status: 429 });
|
|
6405
|
+
}
|
|
6406
|
+
const collabCount = await dbCount(payload, "ticket-collaborators", { where: { ticket: { equals: ticketId } }, overrideAccess: true }).catch(() => ({ totalDocs: 0 }));
|
|
6407
|
+
if (collabCount.totalDocs >= MAX_COLLABORATORS_PER_TICKET) {
|
|
6408
|
+
return Response.json(
|
|
6409
|
+
{ error: `Limite de ${MAX_COLLABORATORS_PER_TICKET} collaborateurs atteinte sur ce ticket` },
|
|
6410
|
+
{ status: 429 }
|
|
6411
|
+
);
|
|
6412
|
+
}
|
|
5834
6413
|
const ownerEmail = typeof ticket.client === "object" ? ticket.client?.email : void 0;
|
|
5835
6414
|
if (ownerEmail && ownerEmail.toLowerCase() === email) {
|
|
5836
6415
|
return Response.json({ error: "Ce client est d\xE9j\xE0 propri\xE9taire du ticket" }, { status: 400 });
|
|
5837
6416
|
}
|
|
5838
6417
|
let inviteeId = null;
|
|
5839
|
-
const existing = await payload.
|
|
5840
|
-
collection: slugs.supportClients,
|
|
6418
|
+
const existing = await dbFind(payload, slugs.supportClients, {
|
|
5841
6419
|
where: { email: { equals: email } },
|
|
5842
6420
|
limit: 1,
|
|
5843
6421
|
depth: 0,
|
|
@@ -5847,8 +6425,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5847
6425
|
inviteeId = existing.docs[0].id;
|
|
5848
6426
|
} else {
|
|
5849
6427
|
const tempPassword = crypto3.randomBytes(16).toString("hex");
|
|
5850
|
-
const created = await payload.
|
|
5851
|
-
collection: slugs.supportClients,
|
|
6428
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
5852
6429
|
data: {
|
|
5853
6430
|
email,
|
|
5854
6431
|
firstName: "Invit\xE9",
|
|
@@ -5865,8 +6442,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5865
6442
|
if (!inviteeId) {
|
|
5866
6443
|
return Response.json({ error: "Impossible de cr\xE9er le client invit\xE9" }, { status: 500 });
|
|
5867
6444
|
}
|
|
5868
|
-
const dupe = await payload
|
|
5869
|
-
collection: "ticket-collaborators",
|
|
6445
|
+
const dupe = await dbFind(payload, "ticket-collaborators", {
|
|
5870
6446
|
where: {
|
|
5871
6447
|
and: [
|
|
5872
6448
|
{ ticket: { equals: ticketId } },
|
|
@@ -5879,15 +6455,13 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5879
6455
|
});
|
|
5880
6456
|
const invitationToken = crypto3.randomBytes(24).toString("hex");
|
|
5881
6457
|
if (dupe.docs.length > 0) {
|
|
5882
|
-
await payload
|
|
5883
|
-
collection: "ticket-collaborators",
|
|
6458
|
+
await dbUpdate(payload, "ticket-collaborators", {
|
|
5884
6459
|
id: dupe.docs[0].id,
|
|
5885
6460
|
data: { role, invitationToken },
|
|
5886
6461
|
overrideAccess: true
|
|
5887
6462
|
});
|
|
5888
6463
|
} else {
|
|
5889
|
-
await payload
|
|
5890
|
-
collection: "ticket-collaborators",
|
|
6464
|
+
await dbCreate(payload, "ticket-collaborators", {
|
|
5891
6465
|
data: {
|
|
5892
6466
|
ticket: ticketId,
|
|
5893
6467
|
client: inviteeId,
|
|
@@ -5930,8 +6504,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5930
6504
|
)
|
|
5931
6505
|
});
|
|
5932
6506
|
try {
|
|
5933
|
-
await payload.
|
|
5934
|
-
collection: slugs.emailLogs,
|
|
6507
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5935
6508
|
data: {
|
|
5936
6509
|
status: "success",
|
|
5937
6510
|
action: "invite",
|
|
@@ -5984,6 +6557,7 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5984
6557
|
];
|
|
5985
6558
|
if (!f || f.ai !== false) {
|
|
5986
6559
|
endpoints.push(createAiEndpoint(slugs));
|
|
6560
|
+
endpoints.push(createAiAgentEndpoint(slugs));
|
|
5987
6561
|
endpoints.push(...createClientIntelligenceEndpoint(slugs));
|
|
5988
6562
|
endpoints.push(createTicketSynthesisEndpoint(slugs));
|
|
5989
6563
|
}
|
|
@@ -6014,21 +6588,28 @@ function createSupportEndpoints(slugs, options) {
|
|
|
6014
6588
|
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs));
|
|
6015
6589
|
endpoints.push(createAdminChatStreamEndpoint(slugs));
|
|
6016
6590
|
}
|
|
6017
|
-
if (!f || f.timeTracking !== false)
|
|
6591
|
+
if (!f || f.timeTracking !== false) {
|
|
6592
|
+
endpoints.push(createBillingEndpoint(slugs));
|
|
6593
|
+
endpoints.push(createInvoiceEndpoint(slugs));
|
|
6594
|
+
}
|
|
6018
6595
|
if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs));
|
|
6019
6596
|
if (!f || f.emailTracking !== false) {
|
|
6020
6597
|
endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs));
|
|
6021
6598
|
}
|
|
6022
6599
|
if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs));
|
|
6023
6600
|
if (!f || f.scheduledReplies !== false) endpoints.push(createProcessScheduledEndpoint(slugs));
|
|
6601
|
+
if (!f || f.snooze !== false) endpoints.push(createProcessSnoozeEndpoint(slugs));
|
|
6602
|
+
endpoints.push(createProcessDigestsEndpoint(slugs));
|
|
6603
|
+
endpoints.push(createChannelsWebhookEndpoint(slugs));
|
|
6604
|
+
endpoints.push(createVapidKeyEndpoint());
|
|
6605
|
+
endpoints.push(createPushSubscribeEndpoint(slugs));
|
|
6024
6606
|
return endpoints;
|
|
6025
6607
|
}
|
|
6026
6608
|
|
|
6027
6609
|
// src/utils/adminNotification.ts
|
|
6028
6610
|
async function createAdminNotification(payload, data, collectionSlug = "admin-notifications") {
|
|
6029
6611
|
try {
|
|
6030
|
-
await payload
|
|
6031
|
-
collection: collectionSlug,
|
|
6612
|
+
await dbCreate(payload, collectionSlug, {
|
|
6032
6613
|
data: {
|
|
6033
6614
|
title: data.title,
|
|
6034
6615
|
message: data.message,
|
|
@@ -6048,8 +6629,7 @@ function dispatchWebhook(data, event, payload, slugs) {
|
|
|
6048
6629
|
}
|
|
6049
6630
|
async function _dispatch(data, event, payload, slugs) {
|
|
6050
6631
|
try {
|
|
6051
|
-
const { docs: endpoints } = await payload.
|
|
6052
|
-
collection: slugs.webhookEndpoints,
|
|
6632
|
+
const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
|
|
6053
6633
|
where: {
|
|
6054
6634
|
and: [
|
|
6055
6635
|
{ active: { equals: true } },
|
|
@@ -6086,8 +6666,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
6086
6666
|
signal: AbortSignal.timeout(1e4)
|
|
6087
6667
|
// 10s timeout
|
|
6088
6668
|
});
|
|
6089
|
-
await payload.
|
|
6090
|
-
collection: slugs.webhookEndpoints,
|
|
6669
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6091
6670
|
id: endpoint.id,
|
|
6092
6671
|
data: {
|
|
6093
6672
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6101,8 +6680,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
6101
6680
|
} catch (err) {
|
|
6102
6681
|
console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
|
|
6103
6682
|
try {
|
|
6104
|
-
await payload.
|
|
6105
|
-
collection: slugs.webhookEndpoints,
|
|
6683
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6106
6684
|
id: endpoint.id,
|
|
6107
6685
|
data: {
|
|
6108
6686
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6336,8 +6914,7 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6336
6914
|
if (typeof policyRef === "object" && policyRef.firstResponseTime) {
|
|
6337
6915
|
return policyRef;
|
|
6338
6916
|
}
|
|
6339
|
-
return await payload.
|
|
6340
|
-
collection: slugs.slaPolicies,
|
|
6917
|
+
return await dbFindByID(payload, slugs.slaPolicies, {
|
|
6341
6918
|
id: policyId,
|
|
6342
6919
|
depth: 0,
|
|
6343
6920
|
overrideAccess: true
|
|
@@ -6347,9 +6924,22 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6347
6924
|
}
|
|
6348
6925
|
}
|
|
6349
6926
|
const priority = field(ticket, "priority") || "normal";
|
|
6927
|
+
const teamRef = field(ticket, "team");
|
|
6928
|
+
const teamId = typeof teamRef === "object" && teamRef !== null ? teamRef.id : teamRef;
|
|
6929
|
+
if (teamId) {
|
|
6930
|
+
try {
|
|
6931
|
+
const teamPolicies = await dbFind(payload, slugs.slaPolicies, {
|
|
6932
|
+
where: { and: [{ team: { equals: teamId } }, { priority: { equals: priority } }] },
|
|
6933
|
+
limit: 1,
|
|
6934
|
+
depth: 0,
|
|
6935
|
+
overrideAccess: true
|
|
6936
|
+
});
|
|
6937
|
+
if (teamPolicies.docs.length > 0) return teamPolicies.docs[0];
|
|
6938
|
+
} catch {
|
|
6939
|
+
}
|
|
6940
|
+
}
|
|
6350
6941
|
try {
|
|
6351
|
-
const defaults = await payload.
|
|
6352
|
-
collection: slugs.slaPolicies,
|
|
6942
|
+
const defaults = await dbFind(payload, slugs.slaPolicies, {
|
|
6353
6943
|
where: {
|
|
6354
6944
|
and: [
|
|
6355
6945
|
{ isDefault: { equals: true } },
|
|
@@ -6386,8 +6976,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6386
6976
|
if (typeof policyRef === "object" && policyRef.escalateOnBreach !== void 0) {
|
|
6387
6977
|
policy = policyRef;
|
|
6388
6978
|
} else {
|
|
6389
|
-
policy = await payload.
|
|
6390
|
-
collection: slugs.slaPolicies,
|
|
6979
|
+
policy = await dbFindByID(payload, slugs.slaPolicies, {
|
|
6391
6980
|
id: policyId,
|
|
6392
6981
|
depth: 0,
|
|
6393
6982
|
overrideAccess: true
|
|
@@ -6400,8 +6989,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6400
6989
|
try {
|
|
6401
6990
|
const escalateToId = typeof policy.escalateTo === "object" ? policy.escalateTo.id : policy.escalateTo;
|
|
6402
6991
|
if (!escalateToId) return;
|
|
6403
|
-
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await payload.
|
|
6404
|
-
collection: slugs.users,
|
|
6992
|
+
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await dbFindByID(payload, slugs.users, {
|
|
6405
6993
|
id: escalateToId,
|
|
6406
6994
|
depth: 0,
|
|
6407
6995
|
overrideAccess: true
|
|
@@ -6449,8 +7037,7 @@ function createAssignSlaDeadlines(slugs, notificationSlug = "admin-notifications
|
|
|
6449
7037
|
if (!field(doc, "slaPolicy") && policy.id) {
|
|
6450
7038
|
updateData.slaPolicy = policy.id;
|
|
6451
7039
|
}
|
|
6452
|
-
await payload.
|
|
6453
|
-
collection: slugs.tickets,
|
|
7040
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6454
7041
|
id: doc.id,
|
|
6455
7042
|
data: updateData,
|
|
6456
7043
|
overrideAccess: true
|
|
@@ -6473,8 +7060,7 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6473
7060
|
const now = /* @__PURE__ */ new Date();
|
|
6474
7061
|
const deadline = new Date(doc.slaResolutionDue);
|
|
6475
7062
|
const breached = now > deadline;
|
|
6476
|
-
await payload.
|
|
6477
|
-
collection: slugs.tickets,
|
|
7063
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6478
7064
|
id: doc.id,
|
|
6479
7065
|
data: { slaResolutionBreached: breached },
|
|
6480
7066
|
overrideAccess: true
|
|
@@ -6488,6 +7074,35 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6488
7074
|
return doc;
|
|
6489
7075
|
};
|
|
6490
7076
|
}
|
|
7077
|
+
function createPauseSlaOnHold(slugs) {
|
|
7078
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
7079
|
+
if (operation !== "update" || !previousDoc) return doc;
|
|
7080
|
+
if (previousDoc.status === doc.status) return doc;
|
|
7081
|
+
if (!doc.slaResolutionDue) return doc;
|
|
7082
|
+
const wasWaiting = previousDoc.status === "waiting_client";
|
|
7083
|
+
const isWaiting = doc.status === "waiting_client";
|
|
7084
|
+
try {
|
|
7085
|
+
if (!wasWaiting && isWaiting && !doc.slaPausedAt && doc.status !== "resolved") {
|
|
7086
|
+
await req.payload.update({
|
|
7087
|
+
collection: slugs.tickets,
|
|
7088
|
+
id: doc.id,
|
|
7089
|
+
data: { slaPausedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
7090
|
+
overrideAccess: true
|
|
7091
|
+
});
|
|
7092
|
+
} else if (wasWaiting && !isWaiting && doc.slaPausedAt) {
|
|
7093
|
+
const pausedMs = Date.now() - new Date(doc.slaPausedAt).getTime();
|
|
7094
|
+
const data = { slaPausedAt: null };
|
|
7095
|
+
if (pausedMs > 0) {
|
|
7096
|
+
data.slaResolutionDue = new Date(new Date(doc.slaResolutionDue).getTime() + pausedMs).toISOString();
|
|
7097
|
+
}
|
|
7098
|
+
await req.payload.update({ collection: slugs.tickets, id: doc.id, data, overrideAccess: true });
|
|
7099
|
+
}
|
|
7100
|
+
} catch (err) {
|
|
7101
|
+
console.error("[sla] Failed to pause/resume SLA on hold:", err);
|
|
7102
|
+
}
|
|
7103
|
+
return doc;
|
|
7104
|
+
};
|
|
7105
|
+
}
|
|
6491
7106
|
function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications") {
|
|
6492
7107
|
return async ({ doc, operation, req }) => {
|
|
6493
7108
|
if (operation !== "create") return doc;
|
|
@@ -6495,8 +7110,7 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6495
7110
|
try {
|
|
6496
7111
|
const { payload } = req;
|
|
6497
7112
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
6498
|
-
const ticket = await payload.
|
|
6499
|
-
collection: slugs.tickets,
|
|
7113
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
6500
7114
|
id: ticketId,
|
|
6501
7115
|
depth: 1,
|
|
6502
7116
|
overrideAccess: true
|
|
@@ -6507,22 +7121,123 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6507
7121
|
const now = /* @__PURE__ */ new Date();
|
|
6508
7122
|
const deadline = new Date(ticket.slaFirstResponseDue);
|
|
6509
7123
|
const breached = now > deadline;
|
|
6510
|
-
await payload.
|
|
6511
|
-
collection: slugs.tickets,
|
|
7124
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6512
7125
|
id: ticketId,
|
|
6513
7126
|
data: { slaFirstResponseBreached: breached },
|
|
6514
7127
|
overrideAccess: true
|
|
6515
7128
|
});
|
|
6516
|
-
if (breached) {
|
|
6517
|
-
await handleSlaBreach(payload, ticket, "first_response", slugs, notificationSlug);
|
|
7129
|
+
if (breached) {
|
|
7130
|
+
await handleSlaBreach(payload, ticket, "first_response", slugs, notificationSlug);
|
|
7131
|
+
}
|
|
7132
|
+
} catch (err) {
|
|
7133
|
+
console.error("[sla] Failed to check SLA on reply:", err);
|
|
7134
|
+
}
|
|
7135
|
+
return doc;
|
|
7136
|
+
};
|
|
7137
|
+
}
|
|
7138
|
+
|
|
7139
|
+
// src/hooks/automationRules.ts
|
|
7140
|
+
function evaluateConditions(doc, rule) {
|
|
7141
|
+
const conds = Array.isArray(rule.conditions) ? rule.conditions : [];
|
|
7142
|
+
if (conds.length === 0) return true;
|
|
7143
|
+
const results = conds.map((c) => {
|
|
7144
|
+
const fieldVal = String(doc[c.field] ?? "");
|
|
7145
|
+
const v = String(c.value ?? "");
|
|
7146
|
+
switch (c.operator) {
|
|
7147
|
+
case "equals":
|
|
7148
|
+
return fieldVal === v;
|
|
7149
|
+
case "not_equals":
|
|
7150
|
+
return fieldVal !== v;
|
|
7151
|
+
case "contains":
|
|
7152
|
+
return fieldVal.toLowerCase().includes(v.toLowerCase());
|
|
7153
|
+
default:
|
|
7154
|
+
return false;
|
|
7155
|
+
}
|
|
7156
|
+
});
|
|
7157
|
+
return rule.matchType === "any" ? results.some(Boolean) : results.every(Boolean);
|
|
7158
|
+
}
|
|
7159
|
+
function createApplyAutomationRules(slugs) {
|
|
7160
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
7161
|
+
if (req.context?.skipAutomation) return doc;
|
|
7162
|
+
const events = [];
|
|
7163
|
+
if (operation === "create") events.push("ticket_created");
|
|
7164
|
+
if (operation === "update") {
|
|
7165
|
+
events.push("ticket_updated");
|
|
7166
|
+
if (previousDoc && previousDoc.status !== doc.status) events.push("ticket_status_changed");
|
|
7167
|
+
}
|
|
7168
|
+
if (events.length === 0) return doc;
|
|
7169
|
+
try {
|
|
7170
|
+
const rules = await dbFind(req.payload, slugs.automationRules, {
|
|
7171
|
+
where: { and: [{ enabled: { equals: true } }, { event: { in: events } }] },
|
|
7172
|
+
sort: "order",
|
|
7173
|
+
limit: 200,
|
|
7174
|
+
depth: 0,
|
|
7175
|
+
overrideAccess: true
|
|
7176
|
+
});
|
|
7177
|
+
const changes = {};
|
|
7178
|
+
const tags = Array.isArray(doc.tags) ? [...doc.tags] : [];
|
|
7179
|
+
let tagsTouched = false;
|
|
7180
|
+
for (const rule of rules.docs) {
|
|
7181
|
+
if (!evaluateConditions(doc, rule)) continue;
|
|
7182
|
+
for (const a of Array.isArray(rule.actions) ? rule.actions : []) {
|
|
7183
|
+
if (!a.value) continue;
|
|
7184
|
+
switch (a.type) {
|
|
7185
|
+
case "set_status":
|
|
7186
|
+
changes.status = a.value;
|
|
7187
|
+
break;
|
|
7188
|
+
case "set_priority":
|
|
7189
|
+
changes.priority = a.value;
|
|
7190
|
+
break;
|
|
7191
|
+
case "set_category":
|
|
7192
|
+
changes.category = a.value;
|
|
7193
|
+
break;
|
|
7194
|
+
case "assign":
|
|
7195
|
+
changes.assignedTo = a.value;
|
|
7196
|
+
break;
|
|
7197
|
+
case "add_tag":
|
|
7198
|
+
if (!tags.includes(a.value)) {
|
|
7199
|
+
tags.push(a.value);
|
|
7200
|
+
tagsTouched = true;
|
|
7201
|
+
}
|
|
7202
|
+
break;
|
|
7203
|
+
}
|
|
7204
|
+
}
|
|
7205
|
+
}
|
|
7206
|
+
if (tagsTouched) changes.tags = tags;
|
|
7207
|
+
if (Object.keys(changes).length > 0) {
|
|
7208
|
+
await req.payload.update({
|
|
7209
|
+
collection: slugs.tickets,
|
|
7210
|
+
id: doc.id,
|
|
7211
|
+
data: changes,
|
|
7212
|
+
overrideAccess: true,
|
|
7213
|
+
context: { skipAutomation: true }
|
|
7214
|
+
});
|
|
6518
7215
|
}
|
|
6519
7216
|
} catch (err) {
|
|
6520
|
-
console.error("[
|
|
7217
|
+
console.error("[support] Failed to apply automation rules:", err);
|
|
6521
7218
|
}
|
|
6522
7219
|
return doc;
|
|
6523
7220
|
};
|
|
6524
7221
|
}
|
|
6525
7222
|
|
|
7223
|
+
// src/utils/teamAccess.ts
|
|
7224
|
+
function isTeamScopingEnabled() {
|
|
7225
|
+
return process.env.SUPPORT_TEAM_SCOPING === "1" || process.env.SUPPORT_TEAM_SCOPING === "true";
|
|
7226
|
+
}
|
|
7227
|
+
async function resolveAgentTeamIds(payload, slugs, userId) {
|
|
7228
|
+
try {
|
|
7229
|
+
const teams = await dbFind(payload, slugs.supportTeams, {
|
|
7230
|
+
where: { members: { in: [userId] } },
|
|
7231
|
+
limit: 200,
|
|
7232
|
+
depth: 0,
|
|
7233
|
+
overrideAccess: true
|
|
7234
|
+
});
|
|
7235
|
+
return teams.docs.map((t) => t.id);
|
|
7236
|
+
} catch {
|
|
7237
|
+
return [];
|
|
7238
|
+
}
|
|
7239
|
+
}
|
|
7240
|
+
|
|
6526
7241
|
// src/collections/Tickets.ts
|
|
6527
7242
|
function createAssignTicketNumber(slugs) {
|
|
6528
7243
|
return async ({ data, operation, req }) => {
|
|
@@ -6785,56 +7500,6 @@ function createNotifyClientOnResolve(slugs) {
|
|
|
6785
7500
|
return doc;
|
|
6786
7501
|
};
|
|
6787
7502
|
}
|
|
6788
|
-
function createAutoCalculateSLA(slugs) {
|
|
6789
|
-
return async ({ doc, operation, req }) => {
|
|
6790
|
-
if (operation !== "create") return doc;
|
|
6791
|
-
try {
|
|
6792
|
-
const { payload } = req;
|
|
6793
|
-
const ticketPriority = doc.priority || "normal";
|
|
6794
|
-
let policy = null;
|
|
6795
|
-
const byPriority = await payload.find({
|
|
6796
|
-
collection: slugs.slaPolicies,
|
|
6797
|
-
where: { priority: { equals: ticketPriority } },
|
|
6798
|
-
limit: 1,
|
|
6799
|
-
depth: 0,
|
|
6800
|
-
overrideAccess: true
|
|
6801
|
-
});
|
|
6802
|
-
if (byPriority.docs.length > 0) {
|
|
6803
|
-
policy = byPriority.docs[0];
|
|
6804
|
-
} else {
|
|
6805
|
-
const defaults = await payload.find({
|
|
6806
|
-
collection: slugs.slaPolicies,
|
|
6807
|
-
where: { isDefault: { equals: true } },
|
|
6808
|
-
limit: 1,
|
|
6809
|
-
depth: 0,
|
|
6810
|
-
overrideAccess: true
|
|
6811
|
-
});
|
|
6812
|
-
if (defaults.docs.length > 0) {
|
|
6813
|
-
policy = defaults.docs[0];
|
|
6814
|
-
}
|
|
6815
|
-
}
|
|
6816
|
-
if (!policy) return doc;
|
|
6817
|
-
const now = /* @__PURE__ */ new Date();
|
|
6818
|
-
const firstResponseMinutes = policy.firstResponseTime || 240;
|
|
6819
|
-
const resolutionMinutes = policy.resolutionTime || 1440;
|
|
6820
|
-
const firstResponseDue = new Date(now.getTime() + firstResponseMinutes * 6e4);
|
|
6821
|
-
const resolutionDue = new Date(now.getTime() + resolutionMinutes * 6e4);
|
|
6822
|
-
await payload.update({
|
|
6823
|
-
collection: slugs.tickets,
|
|
6824
|
-
id: doc.id,
|
|
6825
|
-
data: {
|
|
6826
|
-
slaPolicy: policy.id,
|
|
6827
|
-
slaFirstResponseDue: firstResponseDue.toISOString(),
|
|
6828
|
-
slaResolutionDue: resolutionDue.toISOString()
|
|
6829
|
-
},
|
|
6830
|
-
overrideAccess: true
|
|
6831
|
-
});
|
|
6832
|
-
} catch (err) {
|
|
6833
|
-
console.error("[support] Failed to auto-calculate SLA:", err);
|
|
6834
|
-
}
|
|
6835
|
-
return doc;
|
|
6836
|
-
};
|
|
6837
|
-
}
|
|
6838
7503
|
function createFireTicketWebhooks(slugs) {
|
|
6839
7504
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
6840
7505
|
const { payload } = req;
|
|
@@ -6926,8 +7591,7 @@ function createCascadeDelete(slugs) {
|
|
|
6926
7591
|
return async ({ id, req }) => {
|
|
6927
7592
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys];
|
|
6928
7593
|
for (const slug of collections) {
|
|
6929
|
-
await req.payload
|
|
6930
|
-
collection: slug,
|
|
7594
|
+
await dbDelete(req.payload, slug, {
|
|
6931
7595
|
where: { ticket: { equals: id } },
|
|
6932
7596
|
overrideAccess: true
|
|
6933
7597
|
});
|
|
@@ -7052,6 +7716,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7052
7716
|
{
|
|
7053
7717
|
name: "status",
|
|
7054
7718
|
type: "select",
|
|
7719
|
+
index: true,
|
|
7055
7720
|
defaultValue: "open",
|
|
7056
7721
|
label: "Statut",
|
|
7057
7722
|
options: [
|
|
@@ -7066,6 +7731,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7066
7731
|
{
|
|
7067
7732
|
name: "priority",
|
|
7068
7733
|
type: "select",
|
|
7734
|
+
index: true,
|
|
7069
7735
|
defaultValue: "normal",
|
|
7070
7736
|
label: "Priorite",
|
|
7071
7737
|
options: [
|
|
@@ -7081,7 +7747,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7081
7747
|
{
|
|
7082
7748
|
type: "row",
|
|
7083
7749
|
fields: [
|
|
7084
|
-
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, label: "Client", admin: { width: "50%" } }
|
|
7750
|
+
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, index: true, label: "Client", admin: { width: "50%" } }
|
|
7085
7751
|
]
|
|
7086
7752
|
},
|
|
7087
7753
|
// Billing collapsible
|
|
@@ -7150,15 +7816,16 @@ function createTicketsCollection(slugs, options) {
|
|
|
7150
7816
|
{
|
|
7151
7817
|
type: "row",
|
|
7152
7818
|
fields: [
|
|
7153
|
-
{ name: "slaFirstResponseDue", type: "date", label: "Echeance 1ere reponse (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7154
|
-
{ name: "slaResolutionDue", type: "date", label: "Echeance resolution (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
7819
|
+
{ name: "slaFirstResponseDue", type: "date", index: true, label: "Echeance 1ere reponse (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7820
|
+
{ name: "slaResolutionDue", type: "date", index: true, label: "Echeance resolution (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
7155
7821
|
]
|
|
7156
7822
|
},
|
|
7157
7823
|
{
|
|
7158
7824
|
type: "row",
|
|
7159
7825
|
fields: [
|
|
7160
7826
|
{ name: "slaFirstResponseBreached", type: "checkbox", label: "1ere reponse depassee", admin: { readOnly: true, width: "50%" } },
|
|
7161
|
-
{ name: "slaResolutionBreached", type: "checkbox", label: "Resolution depassee", admin: { readOnly: true, width: "50%" } }
|
|
7827
|
+
{ name: "slaResolutionBreached", type: "checkbox", label: "Resolution depassee", admin: { readOnly: true, width: "50%" } },
|
|
7828
|
+
{ name: "slaPausedAt", type: "date", label: "SLA en pause depuis", admin: { hidden: true } }
|
|
7162
7829
|
]
|
|
7163
7830
|
}
|
|
7164
7831
|
]
|
|
@@ -7180,7 +7847,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7180
7847
|
},
|
|
7181
7848
|
{ name: "mergedInto", type: "relationship", relationTo: slugs.tickets, label: "Fusionne dans", admin: { readOnly: true } },
|
|
7182
7849
|
{ name: "autoCloseRemindedAt", type: "date", label: "Rappel auto-close envoye", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7183
|
-
{ name: "autoCloseScheduledAt", type: "date", label: "Fermeture auto programmee", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" }, description: "Echeance ferme posee par une relance manuelle. Le ticket se ferme apres cette date sans reponse client." } }
|
|
7850
|
+
{ name: "autoCloseScheduledAt", type: "date", index: true, label: "Fermeture auto programmee", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" }, description: "Echeance ferme posee par une relance manuelle. Le ticket se ferme apres cette date sans reponse client." } }
|
|
7184
7851
|
]
|
|
7185
7852
|
},
|
|
7186
7853
|
// Sidebar
|
|
@@ -7189,6 +7856,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7189
7856
|
{
|
|
7190
7857
|
name: "category",
|
|
7191
7858
|
type: "select",
|
|
7859
|
+
index: true,
|
|
7192
7860
|
label: "Categorie",
|
|
7193
7861
|
options: [
|
|
7194
7862
|
{ label: "Bug / Dysfonctionnement", value: "bug" },
|
|
@@ -7199,7 +7867,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7199
7867
|
],
|
|
7200
7868
|
admin: { position: "sidebar" }
|
|
7201
7869
|
},
|
|
7202
|
-
{ name: "assignedTo", type: "relationship", relationTo: slugs.users, label: "Assigne a", admin: { position: "sidebar" } },
|
|
7870
|
+
{ name: "assignedTo", type: "relationship", relationTo: slugs.users, index: true, label: "Assigne a", admin: { position: "sidebar" } },
|
|
7871
|
+
{ name: "team", type: "relationship", relationTo: slugs.supportTeams, index: true, label: "Equipe", admin: { position: "sidebar" } },
|
|
7203
7872
|
{
|
|
7204
7873
|
name: "source",
|
|
7205
7874
|
type: "select",
|
|
@@ -7209,7 +7878,9 @@ function createTicketsCollection(slugs, options) {
|
|
|
7209
7878
|
{ label: "Portail client", value: "portal" },
|
|
7210
7879
|
{ label: "Chat en direct", value: "live-chat" },
|
|
7211
7880
|
{ label: "Email", value: "email" },
|
|
7212
|
-
{ label: "Admin", value: "admin" }
|
|
7881
|
+
{ label: "Admin", value: "admin" },
|
|
7882
|
+
{ label: "WhatsApp", value: "whatsapp" },
|
|
7883
|
+
{ label: "Messenger", value: "messenger" }
|
|
7213
7884
|
],
|
|
7214
7885
|
admin: { position: "sidebar" }
|
|
7215
7886
|
},
|
|
@@ -7260,8 +7931,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7260
7931
|
afterChange: [
|
|
7261
7932
|
createTrackSLA(slugs),
|
|
7262
7933
|
createTrackAiSummaryOnResolve(slugs),
|
|
7263
|
-
createAutoCalculateSLA(slugs),
|
|
7264
7934
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
7935
|
+
createPauseSlaOnHold(slugs),
|
|
7265
7936
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
7266
7937
|
createLogTicketActivity(slugs),
|
|
7267
7938
|
createNotifyOnAssignment(slugs),
|
|
@@ -7269,7 +7940,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7269
7940
|
createTicketStatusEmail(slugs),
|
|
7270
7941
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
7271
7942
|
createFireTicketWebhooks(slugs),
|
|
7272
|
-
createDispatchWebhookOnTicket(slugs)
|
|
7943
|
+
createDispatchWebhookOnTicket(slugs),
|
|
7944
|
+
createApplyAutomationRules(slugs)
|
|
7273
7945
|
],
|
|
7274
7946
|
beforeDelete: [createCascadeDelete(slugs)]
|
|
7275
7947
|
},
|
|
@@ -7280,12 +7952,20 @@ function createTicketsCollection(slugs, options) {
|
|
|
7280
7952
|
return false;
|
|
7281
7953
|
},
|
|
7282
7954
|
read: async ({ req }) => {
|
|
7283
|
-
if (req.user?.collection === slugs.users)
|
|
7955
|
+
if (req.user?.collection === slugs.users) {
|
|
7956
|
+
if (!isTeamScopingEnabled()) return true;
|
|
7957
|
+
const teamIds = await resolveAgentTeamIds(req.payload, slugs, req.user.id);
|
|
7958
|
+
return {
|
|
7959
|
+
or: [
|
|
7960
|
+
{ team: { in: teamIds.length > 0 ? teamIds : [-1] } },
|
|
7961
|
+
{ team: { exists: false } }
|
|
7962
|
+
]
|
|
7963
|
+
};
|
|
7964
|
+
}
|
|
7284
7965
|
if (req.user?.collection === slugs.supportClients) {
|
|
7285
7966
|
let collabTicketIds = [];
|
|
7286
7967
|
try {
|
|
7287
|
-
const rows = await req.payload
|
|
7288
|
-
collection: "ticket-collaborators",
|
|
7968
|
+
const rows = await dbFind(req.payload, "ticket-collaborators", {
|
|
7289
7969
|
where: { client: { equals: req.user.id } },
|
|
7290
7970
|
limit: 1e3,
|
|
7291
7971
|
depth: 0,
|
|
@@ -7320,6 +8000,125 @@ function createTicketsCollection(slugs, options) {
|
|
|
7320
8000
|
};
|
|
7321
8001
|
}
|
|
7322
8002
|
|
|
8003
|
+
// src/utils/ticketAccess.ts
|
|
8004
|
+
async function resolveAccessibleTicketIds(payload, slugs, clientId) {
|
|
8005
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8006
|
+
try {
|
|
8007
|
+
const owned = await dbFind(payload, slugs.tickets, {
|
|
8008
|
+
where: { client: { equals: clientId } },
|
|
8009
|
+
limit: 5e3,
|
|
8010
|
+
depth: 0,
|
|
8011
|
+
overrideAccess: true
|
|
8012
|
+
});
|
|
8013
|
+
for (const t of owned.docs) ids.add(t.id);
|
|
8014
|
+
} catch {
|
|
8015
|
+
}
|
|
8016
|
+
try {
|
|
8017
|
+
const collab = await dbFind(payload, "ticket-collaborators", {
|
|
8018
|
+
where: { client: { equals: clientId } },
|
|
8019
|
+
limit: 1e3,
|
|
8020
|
+
depth: 0,
|
|
8021
|
+
overrideAccess: true
|
|
8022
|
+
});
|
|
8023
|
+
for (const r of collab.docs) {
|
|
8024
|
+
const row = r;
|
|
8025
|
+
const tid = typeof row.ticket === "object" ? row.ticket?.id : row.ticket;
|
|
8026
|
+
if (tid !== void 0 && tid !== null) ids.add(tid);
|
|
8027
|
+
}
|
|
8028
|
+
} catch {
|
|
8029
|
+
}
|
|
8030
|
+
return Array.from(ids);
|
|
8031
|
+
}
|
|
8032
|
+
var OPTIONS = {
|
|
8033
|
+
allowedTags: [
|
|
8034
|
+
"p",
|
|
8035
|
+
"br",
|
|
8036
|
+
"b",
|
|
8037
|
+
"strong",
|
|
8038
|
+
"i",
|
|
8039
|
+
"em",
|
|
8040
|
+
"u",
|
|
8041
|
+
"s",
|
|
8042
|
+
"strike",
|
|
8043
|
+
"del",
|
|
8044
|
+
"ins",
|
|
8045
|
+
"mark",
|
|
8046
|
+
"a",
|
|
8047
|
+
"ul",
|
|
8048
|
+
"ol",
|
|
8049
|
+
"li",
|
|
8050
|
+
"blockquote",
|
|
8051
|
+
"code",
|
|
8052
|
+
"pre",
|
|
8053
|
+
"kbd",
|
|
8054
|
+
"span",
|
|
8055
|
+
"div",
|
|
8056
|
+
"h1",
|
|
8057
|
+
"h2",
|
|
8058
|
+
"h3",
|
|
8059
|
+
"h4",
|
|
8060
|
+
"h5",
|
|
8061
|
+
"h6",
|
|
8062
|
+
"hr",
|
|
8063
|
+
"img",
|
|
8064
|
+
"table",
|
|
8065
|
+
"thead",
|
|
8066
|
+
"tbody",
|
|
8067
|
+
"tr",
|
|
8068
|
+
"th",
|
|
8069
|
+
"td"
|
|
8070
|
+
],
|
|
8071
|
+
allowedAttributes: {
|
|
8072
|
+
a: ["href", "title", "target", "rel"],
|
|
8073
|
+
img: ["src", "alt", "title", "width", "height"],
|
|
8074
|
+
code: ["class"],
|
|
8075
|
+
pre: ["class"],
|
|
8076
|
+
span: ["class"],
|
|
8077
|
+
"*": ["style"]
|
|
8078
|
+
},
|
|
8079
|
+
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
8080
|
+
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
|
8081
|
+
allowProtocolRelative: false,
|
|
8082
|
+
disallowedTagsMode: "discard",
|
|
8083
|
+
// Force safe rel on links opening a new tab (defense against tabnabbing).
|
|
8084
|
+
transformTags: {
|
|
8085
|
+
a: sanitizeHtml__default.default.simpleTransform("a", { rel: "noopener noreferrer nofollow" }, true)
|
|
8086
|
+
},
|
|
8087
|
+
// Only allow CSS that cannot carry a script payload.
|
|
8088
|
+
allowedStyles: {
|
|
8089
|
+
"*": {
|
|
8090
|
+
color: [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}\s*\)$/i, /^[a-z-]+$/i],
|
|
8091
|
+
"background-color": [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}\s*\)$/i, /^[a-z-]+$/i],
|
|
8092
|
+
"text-align": [/^(left|right|center|justify)$/],
|
|
8093
|
+
"font-weight": [/^(normal|bold|[1-9]00)$/],
|
|
8094
|
+
"font-style": [/^(normal|italic)$/],
|
|
8095
|
+
"text-decoration": [/^(underline|line-through|none)$/]
|
|
8096
|
+
}
|
|
8097
|
+
}
|
|
8098
|
+
};
|
|
8099
|
+
function sanitizeMessageHtml(html) {
|
|
8100
|
+
if (!html) return html;
|
|
8101
|
+
return sanitizeHtml__default.default(html, OPTIONS);
|
|
8102
|
+
}
|
|
8103
|
+
|
|
8104
|
+
// src/utils/notificationQueue.ts
|
|
8105
|
+
async function queueClientNotification(payload, slugs, item) {
|
|
8106
|
+
try {
|
|
8107
|
+
await dbCreate(payload, slugs.notificationQueue, {
|
|
8108
|
+
data: {
|
|
8109
|
+
client: item.client,
|
|
8110
|
+
type: item.type,
|
|
8111
|
+
title: item.title,
|
|
8112
|
+
message: item.message,
|
|
8113
|
+
...item.ticket ? { ticket: item.ticket } : {}
|
|
8114
|
+
},
|
|
8115
|
+
overrideAccess: true
|
|
8116
|
+
});
|
|
8117
|
+
} catch (err) {
|
|
8118
|
+
console.error("[support] Failed to queue client notification:", err);
|
|
8119
|
+
}
|
|
8120
|
+
}
|
|
8121
|
+
|
|
7323
8122
|
// src/collections/TicketMessages.ts
|
|
7324
8123
|
function createAssignAuthor(slugs) {
|
|
7325
8124
|
return async ({ data, operation, req }) => {
|
|
@@ -7382,29 +8181,46 @@ function createNotifyClient(slugs) {
|
|
|
7382
8181
|
const internalBcc = process.env.SUPPORT_CLIENT_BCC || void 0;
|
|
7383
8182
|
const rawContent = doc.bodyHtml ? emailRichContent(doc.bodyHtml) : emailQuote(doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body);
|
|
7384
8183
|
const replyPreview = (doc.body || "").replace(/\s+/g, " ").trim().slice(0, 90);
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
8184
|
+
const freq = client.notificationFrequency;
|
|
8185
|
+
if (freq === "daily" || freq === "weekly") {
|
|
8186
|
+
await queueClientNotification(req.payload, slugs, {
|
|
8187
|
+
client: client.id,
|
|
8188
|
+
type: "reply",
|
|
8189
|
+
title: `Nouvelle reponse \u2014 ${ticketNumber}`,
|
|
8190
|
+
message: replyPreview || `Reponse a votre ticket ${ticketNumber}`,
|
|
8191
|
+
ticket: ticketId
|
|
8192
|
+
});
|
|
8193
|
+
return doc;
|
|
8194
|
+
}
|
|
8195
|
+
void (async () => {
|
|
8196
|
+
try {
|
|
8197
|
+
await req.payload.sendEmail({
|
|
8198
|
+
to: client.email,
|
|
8199
|
+
...supportEmail ? { replyTo: supportEmail } : {},
|
|
8200
|
+
...internalBcc ? { bcc: internalBcc } : {},
|
|
8201
|
+
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
8202
|
+
html: emailWrapper(`Nouvelle reponse \u2014 ${ticketNumber}`, [
|
|
8203
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
|
|
8204
|
+
emailParagraph(`Notre equipe a apporte une reponse a votre ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(subject)}</em>.`),
|
|
8205
|
+
rawContent,
|
|
8206
|
+
emailButton("Consulter le ticket", portalUrl),
|
|
8207
|
+
emailParagraph('<span style="font-size: 13px; color: #6b7280;">Vous pouvez egalement repondre directement a cet email. Votre message sera automatiquement ajoute au ticket.</span>'),
|
|
8208
|
+
emailTrackingPixel(ticketId, doc.id)
|
|
8209
|
+
].join(""), {
|
|
8210
|
+
kind: "reply",
|
|
8211
|
+
preheader: replyPreview ? `${replyPreview}\u2026` : `Nouvelle reponse a votre ticket ${ticketNumber}`
|
|
8212
|
+
})
|
|
8213
|
+
});
|
|
8214
|
+
await req.payload.update({
|
|
8215
|
+
collection: slugs.ticketMessages,
|
|
8216
|
+
id: doc.id,
|
|
8217
|
+
data: { emailSentAt: (/* @__PURE__ */ new Date()).toISOString(), emailSentTo: client.email },
|
|
8218
|
+
overrideAccess: true
|
|
8219
|
+
});
|
|
8220
|
+
} catch (err) {
|
|
8221
|
+
console.error("[support] Failed to notify client (async):", err);
|
|
8222
|
+
}
|
|
8223
|
+
})();
|
|
7408
8224
|
} catch (err) {
|
|
7409
8225
|
console.error("[support] Failed to notify client:", err);
|
|
7410
8226
|
}
|
|
@@ -7442,8 +8258,7 @@ function createSyncTicketReplyToChat(slugs) {
|
|
|
7442
8258
|
if (!ticket?.chatSession) return doc;
|
|
7443
8259
|
if (doc.skipNotification) return doc;
|
|
7444
8260
|
const clientId = typeof ticket.client === "object" ? ticket.client.id : ticket.client;
|
|
7445
|
-
await payload.
|
|
7446
|
-
collection: slugs.chatMessages,
|
|
8261
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
7447
8262
|
data: {
|
|
7448
8263
|
session: ticket.chatSession,
|
|
7449
8264
|
client: clientId,
|
|
@@ -7505,6 +8320,15 @@ function createNotifyAdminOnClientMessage(slugs, notificationSlug) {
|
|
|
7505
8320
|
}
|
|
7506
8321
|
const preview = doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body;
|
|
7507
8322
|
const headerTitle = isNewTicket ? `Nouveau ticket ${ticketNumber}` : `Nouveau message \u2014 ${ticketNumber}`;
|
|
8323
|
+
const assignedId = assignedAdmin?.id ?? (ticket.assignedTo && typeof ticket.assignedTo !== "object" ? ticket.assignedTo : void 0);
|
|
8324
|
+
if (assignedId) {
|
|
8325
|
+
void sendPushToUser(payload, slugs, assignedId, {
|
|
8326
|
+
title: headerTitle,
|
|
8327
|
+
body: `${clientName}: ${preview || subject}`,
|
|
8328
|
+
url: `/admin/collections/${slugs.tickets}/${ticketId}`
|
|
8329
|
+
}).catch(() => {
|
|
8330
|
+
});
|
|
8331
|
+
}
|
|
7508
8332
|
if (primaryEmail) {
|
|
7509
8333
|
await payload.sendEmail({
|
|
7510
8334
|
// INTERNAL email (to the support team) — recipients are the contact
|
|
@@ -7560,6 +8384,57 @@ function createDispatchWebhookOnReply(slugs) {
|
|
|
7560
8384
|
return doc;
|
|
7561
8385
|
};
|
|
7562
8386
|
}
|
|
8387
|
+
function createSanitizeMessageHtml() {
|
|
8388
|
+
return ({ data }) => {
|
|
8389
|
+
if (data && typeof data.bodyHtml === "string" && data.bodyHtml) {
|
|
8390
|
+
data.bodyHtml = sanitizeMessageHtml(data.bodyHtml);
|
|
8391
|
+
}
|
|
8392
|
+
return data;
|
|
8393
|
+
};
|
|
8394
|
+
}
|
|
8395
|
+
var MENTION_RE = /@([^\s@]+@[^\s@]+\.[^\s@]+)/g;
|
|
8396
|
+
function createResolveMentions(slugs) {
|
|
8397
|
+
return async ({ data, operation, req }) => {
|
|
8398
|
+
if (operation !== "create") return data;
|
|
8399
|
+
const body = typeof data?.body === "string" ? data.body : "";
|
|
8400
|
+
const emails = Array.from(new Set([...body.matchAll(MENTION_RE)].map((m) => m[1].toLowerCase())));
|
|
8401
|
+
if (emails.length === 0) return data;
|
|
8402
|
+
try {
|
|
8403
|
+
const users = await dbFind(req.payload, slugs.users, { where: { email: { in: emails } }, limit: 50, depth: 0, overrideAccess: true });
|
|
8404
|
+
const ids = users.docs.map((u) => u.id);
|
|
8405
|
+
if (ids.length > 0) data.mentions = ids;
|
|
8406
|
+
} catch {
|
|
8407
|
+
}
|
|
8408
|
+
return data;
|
|
8409
|
+
};
|
|
8410
|
+
}
|
|
8411
|
+
function createNotifyMentions(slugs) {
|
|
8412
|
+
return async ({ doc, operation, req }) => {
|
|
8413
|
+
if (operation !== "create") return doc;
|
|
8414
|
+
const mentions = Array.isArray(doc.mentions) ? doc.mentions : [];
|
|
8415
|
+
if (mentions.length === 0) return doc;
|
|
8416
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8417
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
8418
|
+
for (const m of mentions) {
|
|
8419
|
+
const userId = typeof m === "object" ? m.id : m;
|
|
8420
|
+
try {
|
|
8421
|
+
const user = await dbFindByID(req.payload, slugs.users, { id: userId, depth: 0, overrideAccess: true });
|
|
8422
|
+
if (!user?.email) continue;
|
|
8423
|
+
await req.payload.sendEmail({
|
|
8424
|
+
to: user.email,
|
|
8425
|
+
subject: `Vous avez ete mentionne \u2014 ticket #${ticketId}`,
|
|
8426
|
+
html: emailWrapper("Mention", [
|
|
8427
|
+
emailParagraph(`Vous avez ete mentionne dans un message du ticket <strong>#${escapeHtml(String(ticketId))}</strong>.`),
|
|
8428
|
+
...baseUrl ? [emailButton("Ouvrir le ticket", `${baseUrl}/admin/collections/${slugs.tickets}/${ticketId}`, "dark")] : []
|
|
8429
|
+
].join(""), { kind: "system" })
|
|
8430
|
+
});
|
|
8431
|
+
} catch (err) {
|
|
8432
|
+
console.error("[support] Failed to notify mention:", err);
|
|
8433
|
+
}
|
|
8434
|
+
}
|
|
8435
|
+
return doc;
|
|
8436
|
+
};
|
|
8437
|
+
}
|
|
7563
8438
|
function createTicketMessagesCollection(slugs, options) {
|
|
7564
8439
|
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
7565
8440
|
return {
|
|
@@ -7567,7 +8442,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7567
8442
|
labels: { singular: "Message", plural: "Messages" },
|
|
7568
8443
|
admin: { hidden: true, group: "Support", defaultColumns: ["ticket", "authorType", "createdAt"] },
|
|
7569
8444
|
fields: [
|
|
7570
|
-
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, required: true, label: "Ticket" },
|
|
8445
|
+
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, required: true, index: true, label: "Ticket" },
|
|
7571
8446
|
{ name: "body", type: "textarea", required: true, label: "Message" },
|
|
7572
8447
|
{ name: "bodyHtml", type: "textarea", label: "Message HTML", admin: { hidden: true } },
|
|
7573
8448
|
{
|
|
@@ -7576,6 +8451,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7576
8451
|
{
|
|
7577
8452
|
name: "authorType",
|
|
7578
8453
|
type: "select",
|
|
8454
|
+
index: true,
|
|
7579
8455
|
label: "Type d'auteur",
|
|
7580
8456
|
defaultValue: "admin",
|
|
7581
8457
|
options: [
|
|
@@ -7611,8 +8487,9 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7611
8487
|
},
|
|
7612
8488
|
{ name: "isInternal", type: "checkbox", defaultValue: false, label: "Note interne", admin: { position: "sidebar" } },
|
|
7613
8489
|
{ name: "isSolution", type: "checkbox", defaultValue: false, label: "Reponse solution", admin: { position: "sidebar" } },
|
|
8490
|
+
{ name: "mentions", type: "relationship", relationTo: slugs.users, hasMany: true, label: "Agents mentionnes", admin: { hidden: true } },
|
|
7614
8491
|
{ name: "skipNotification", type: "checkbox", defaultValue: false, label: "Sans notification", admin: { position: "sidebar", condition: (data) => data?.skipNotification === true } },
|
|
7615
|
-
{ name: "scheduledAt", type: "date", label: "Programme pour", admin: { date: { pickerAppearance: "dayAndTime" }, position: "sidebar", condition: (data) => !!data?.scheduledAt } },
|
|
8492
|
+
{ name: "scheduledAt", type: "date", index: true, label: "Programme pour", admin: { date: { pickerAppearance: "dayAndTime" }, position: "sidebar", condition: (data) => !!data?.scheduledAt } },
|
|
7616
8493
|
{ name: "scheduledSent", type: "checkbox", defaultValue: false, admin: { hidden: true } },
|
|
7617
8494
|
{ name: "editedAt", type: "date", label: "Modifie le", admin: { hidden: true } },
|
|
7618
8495
|
{ name: "deletedAt", type: "date", label: "Supprime le", admin: { hidden: true } },
|
|
@@ -7621,7 +8498,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7621
8498
|
{ name: "emailOpenedAt", type: "date", label: "Email ouvert le", admin: { hidden: true } }
|
|
7622
8499
|
],
|
|
7623
8500
|
hooks: {
|
|
7624
|
-
beforeChange: [createAssignAuthor(slugs)],
|
|
8501
|
+
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
7625
8502
|
afterChange: [
|
|
7626
8503
|
createAutoUpdateStatus(slugs),
|
|
7627
8504
|
createNotifyClient(slugs),
|
|
@@ -7630,17 +8507,19 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7630
8507
|
createSyncTicketReplyToChat(slugs),
|
|
7631
8508
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
7632
8509
|
createFireMessageWebhooks(slugs),
|
|
7633
|
-
createDispatchWebhookOnReply(slugs)
|
|
8510
|
+
createDispatchWebhookOnReply(slugs),
|
|
8511
|
+
createNotifyMentions(slugs)
|
|
7634
8512
|
]
|
|
7635
8513
|
},
|
|
7636
8514
|
access: {
|
|
7637
8515
|
create: ({ req }) => req.user?.collection === slugs.users || req.user?.collection === slugs.supportClients,
|
|
7638
|
-
read: ({ req }) => {
|
|
8516
|
+
read: async ({ req }) => {
|
|
7639
8517
|
if (req.user?.collection === slugs.users) return true;
|
|
7640
8518
|
if (req.user?.collection === slugs.supportClients) {
|
|
8519
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
7641
8520
|
return {
|
|
7642
8521
|
and: [
|
|
7643
|
-
{
|
|
8522
|
+
{ ticket: { in: ids.length > 0 ? ids : [-1] } },
|
|
7644
8523
|
{ isInternal: { equals: false } },
|
|
7645
8524
|
{ or: [{ scheduledAt: { exists: false } }, { scheduledSent: { equals: true } }] }
|
|
7646
8525
|
]
|
|
@@ -7660,8 +8539,6 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7660
8539
|
timestamps: true
|
|
7661
8540
|
};
|
|
7662
8541
|
}
|
|
7663
|
-
|
|
7664
|
-
// src/collections/SupportClients.ts
|
|
7665
8542
|
function createSendInvitationOnCreate(slugs) {
|
|
7666
8543
|
return async ({ doc, operation, req }) => {
|
|
7667
8544
|
if (operation !== "create") return doc;
|
|
@@ -7701,6 +8578,26 @@ function createSendInvitationOnCreate(slugs) {
|
|
|
7701
8578
|
return doc;
|
|
7702
8579
|
};
|
|
7703
8580
|
}
|
|
8581
|
+
var TWO_FA_WINDOW_MS = 5 * 60 * 1e3;
|
|
8582
|
+
function createEnforce2FA(slugs) {
|
|
8583
|
+
return async ({ req, user }) => {
|
|
8584
|
+
if (!user?.twoFactorEnabled) return user;
|
|
8585
|
+
const raw = user.twoFactorVerifiedAt;
|
|
8586
|
+
const verifiedAt = raw ? new Date(raw).getTime() : 0;
|
|
8587
|
+
if (!(verifiedAt > Date.now() - TWO_FA_WINDOW_MS)) {
|
|
8588
|
+
throw new payload.APIError("2FA_REQUIRED", 401);
|
|
8589
|
+
}
|
|
8590
|
+
await req.payload.update({
|
|
8591
|
+
collection: slugs.supportClients,
|
|
8592
|
+
id: user.id,
|
|
8593
|
+
data: { twoFactorVerifiedAt: null },
|
|
8594
|
+
req,
|
|
8595
|
+
overrideAccess: true
|
|
8596
|
+
});
|
|
8597
|
+
user.twoFactorVerifiedAt = null;
|
|
8598
|
+
return user;
|
|
8599
|
+
};
|
|
8600
|
+
}
|
|
7704
8601
|
function createSupportClientsCollection(slugs) {
|
|
7705
8602
|
return {
|
|
7706
8603
|
slug: slugs.supportClients,
|
|
@@ -7829,6 +8726,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7829
8726
|
name: "opportunities",
|
|
7830
8727
|
type: "textarea",
|
|
7831
8728
|
label: "Opportunites commerciales",
|
|
8729
|
+
access: {
|
|
8730
|
+
// Admin-only: never serialize commercial notes onto the client's own doc.
|
|
8731
|
+
read: ({ req }) => req.user?.collection === slugs.users
|
|
8732
|
+
},
|
|
7832
8733
|
admin: {
|
|
7833
8734
|
description: "Notes commerciales (visible uniquement par les admins)"
|
|
7834
8735
|
}
|
|
@@ -7872,6 +8773,19 @@ function createSupportClientsCollection(slugs) {
|
|
|
7872
8773
|
type: "date",
|
|
7873
8774
|
admin: { hidden: true }
|
|
7874
8775
|
},
|
|
8776
|
+
{
|
|
8777
|
+
// Set by /support/2fa "verify"; gates login via the beforeLogin hook.
|
|
8778
|
+
name: "twoFactorVerifiedAt",
|
|
8779
|
+
type: "date",
|
|
8780
|
+
admin: { hidden: true }
|
|
8781
|
+
},
|
|
8782
|
+
{
|
|
8783
|
+
// Google OAuth subject id — accounts are linked by this, not email alone.
|
|
8784
|
+
name: "googleId",
|
|
8785
|
+
type: "text",
|
|
8786
|
+
index: true,
|
|
8787
|
+
admin: { hidden: true }
|
|
8788
|
+
},
|
|
7875
8789
|
{
|
|
7876
8790
|
name: "notifyOnReply",
|
|
7877
8791
|
type: "checkbox",
|
|
@@ -7938,6 +8852,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7938
8852
|
name: "notes",
|
|
7939
8853
|
type: "textarea",
|
|
7940
8854
|
label: "Notes internes",
|
|
8855
|
+
access: {
|
|
8856
|
+
// Admin-only: never serialize internal notes onto the client's own doc.
|
|
8857
|
+
read: ({ req }) => req.user?.collection === slugs.users
|
|
8858
|
+
},
|
|
7941
8859
|
admin: {
|
|
7942
8860
|
description: "Visible uniquement par les admins",
|
|
7943
8861
|
position: "sidebar"
|
|
@@ -7945,6 +8863,7 @@ function createSupportClientsCollection(slugs) {
|
|
|
7945
8863
|
}
|
|
7946
8864
|
],
|
|
7947
8865
|
hooks: {
|
|
8866
|
+
beforeLogin: [createEnforce2FA(slugs)],
|
|
7948
8867
|
afterChange: [createSendInvitationOnCreate(slugs)]
|
|
7949
8868
|
},
|
|
7950
8869
|
access: {
|
|
@@ -8158,6 +9077,7 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
8158
9077
|
type: "relationship",
|
|
8159
9078
|
relationTo: slugs.tickets,
|
|
8160
9079
|
required: true,
|
|
9080
|
+
index: true,
|
|
8161
9081
|
label: "Ticket"
|
|
8162
9082
|
},
|
|
8163
9083
|
{
|
|
@@ -8203,10 +9123,11 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
8203
9123
|
access: {
|
|
8204
9124
|
create: () => false,
|
|
8205
9125
|
// Created only by hooks via overrideAccess
|
|
8206
|
-
read: ({ req }) => {
|
|
9126
|
+
read: async ({ req }) => {
|
|
8207
9127
|
if (req.user?.collection === slugs.users) return true;
|
|
8208
9128
|
if (req.user?.collection === slugs.supportClients) {
|
|
8209
|
-
|
|
9129
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
9130
|
+
return { ticket: { in: ids.length > 0 ? ids : [-1] } };
|
|
8210
9131
|
}
|
|
8211
9132
|
return false;
|
|
8212
9133
|
},
|
|
@@ -8275,12 +9196,21 @@ function createSatisfactionSurveysCollection(slugs) {
|
|
|
8275
9196
|
{
|
|
8276
9197
|
name: "rating",
|
|
8277
9198
|
type: "number",
|
|
8278
|
-
required: true,
|
|
8279
9199
|
min: 1,
|
|
8280
9200
|
max: 5,
|
|
8281
9201
|
label: "Note (1-5)",
|
|
8282
9202
|
admin: {
|
|
8283
|
-
description: "1 = Tr\xE8s insatisfait, 5 = Tr\xE8s satisfait"
|
|
9203
|
+
description: "1 = Tr\xE8s insatisfait, 5 = Tr\xE8s satisfait (CSAT)"
|
|
9204
|
+
}
|
|
9205
|
+
},
|
|
9206
|
+
{
|
|
9207
|
+
name: "nps",
|
|
9208
|
+
type: "number",
|
|
9209
|
+
min: 0,
|
|
9210
|
+
max: 10,
|
|
9211
|
+
label: "NPS (0-10)",
|
|
9212
|
+
admin: {
|
|
9213
|
+
description: "\xAB Recommanderiez-vous ? \xBB \u2014 promoteur 9-10, passif 7-8, d\xE9tracteur 0-6"
|
|
8284
9214
|
}
|
|
8285
9215
|
},
|
|
8286
9216
|
{
|
|
@@ -9002,6 +9932,16 @@ function createSlaPoliciesCollection(slugs) {
|
|
|
9002
9932
|
admin: {
|
|
9003
9933
|
description: "Utiliser cette politique pour les tickets sans SLA sp\xE9cifique (par priorit\xE9)"
|
|
9004
9934
|
}
|
|
9935
|
+
},
|
|
9936
|
+
{
|
|
9937
|
+
name: "team",
|
|
9938
|
+
type: "relationship",
|
|
9939
|
+
relationTo: slugs.supportTeams,
|
|
9940
|
+
index: true,
|
|
9941
|
+
label: "\xC9quipe (optionnel)",
|
|
9942
|
+
admin: {
|
|
9943
|
+
description: "Limiter cette politique \xE0 une \xE9quipe \u2014 prioritaire sur la politique par d\xE9faut"
|
|
9944
|
+
}
|
|
9005
9945
|
}
|
|
9006
9946
|
],
|
|
9007
9947
|
access: {
|
|
@@ -9526,6 +10466,148 @@ function createTicketCollaboratorsCollection(slugs) {
|
|
|
9526
10466
|
};
|
|
9527
10467
|
}
|
|
9528
10468
|
|
|
10469
|
+
// src/collections/NotificationQueue.ts
|
|
10470
|
+
function createNotificationQueueCollection(slugs) {
|
|
10471
|
+
return {
|
|
10472
|
+
slug: slugs.notificationQueue,
|
|
10473
|
+
labels: { singular: "Notification en file", plural: "Notifications en file" },
|
|
10474
|
+
admin: { hidden: true, group: "Support", defaultColumns: ["client", "type", "title", "createdAt"] },
|
|
10475
|
+
access: {
|
|
10476
|
+
// Internal: written/drained only via overrideAccess (hooks + cron).
|
|
10477
|
+
create: () => false,
|
|
10478
|
+
update: () => false,
|
|
10479
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10480
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10481
|
+
},
|
|
10482
|
+
fields: [
|
|
10483
|
+
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, index: true, label: "Client" },
|
|
10484
|
+
{ name: "type", type: "text", label: "Type" },
|
|
10485
|
+
{ name: "title", type: "text", label: "Titre" },
|
|
10486
|
+
{ name: "message", type: "textarea", label: "Message" },
|
|
10487
|
+
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, label: "Ticket" }
|
|
10488
|
+
],
|
|
10489
|
+
timestamps: true
|
|
10490
|
+
};
|
|
10491
|
+
}
|
|
10492
|
+
|
|
10493
|
+
// src/collections/AutomationRules.ts
|
|
10494
|
+
var CONDITION_FIELDS = ["status", "priority", "category", "source", "subject"];
|
|
10495
|
+
var ACTION_TYPES = [
|
|
10496
|
+
{ label: "D\xE9finir le statut", value: "set_status" },
|
|
10497
|
+
{ label: "D\xE9finir la priorit\xE9", value: "set_priority" },
|
|
10498
|
+
{ label: "D\xE9finir la cat\xE9gorie", value: "set_category" },
|
|
10499
|
+
{ label: "Assigner \xE0 (id agent)", value: "assign" },
|
|
10500
|
+
{ label: "Ajouter un tag", value: "add_tag" }
|
|
10501
|
+
];
|
|
10502
|
+
function createAutomationRulesCollection(slugs) {
|
|
10503
|
+
return {
|
|
10504
|
+
slug: slugs.automationRules,
|
|
10505
|
+
labels: { singular: "R\xE8gle d'automatisation", plural: "R\xE8gles d'automatisation" },
|
|
10506
|
+
admin: { useAsTitle: "name", group: "Support", defaultColumns: ["name", "event", "enabled", "order"] },
|
|
10507
|
+
access: {
|
|
10508
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10509
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
10510
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
10511
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10512
|
+
},
|
|
10513
|
+
fields: [
|
|
10514
|
+
{ name: "name", type: "text", required: true, label: "Nom" },
|
|
10515
|
+
{ name: "enabled", type: "checkbox", defaultValue: true, label: "Active", index: true },
|
|
10516
|
+
{
|
|
10517
|
+
name: "event",
|
|
10518
|
+
type: "select",
|
|
10519
|
+
defaultValue: "ticket_created",
|
|
10520
|
+
index: true,
|
|
10521
|
+
label: "D\xE9clencheur",
|
|
10522
|
+
options: [
|
|
10523
|
+
{ label: "Ticket cr\xE9\xE9", value: "ticket_created" },
|
|
10524
|
+
{ label: "Ticket mis \xE0 jour", value: "ticket_updated" },
|
|
10525
|
+
{ label: "Changement de statut", value: "ticket_status_changed" }
|
|
10526
|
+
]
|
|
10527
|
+
},
|
|
10528
|
+
{
|
|
10529
|
+
name: "matchType",
|
|
10530
|
+
type: "select",
|
|
10531
|
+
defaultValue: "all",
|
|
10532
|
+
label: "Correspondance",
|
|
10533
|
+
options: [{ label: "Toutes les conditions (ET)", value: "all" }, { label: "Au moins une (OU)", value: "any" }]
|
|
10534
|
+
},
|
|
10535
|
+
{ name: "order", type: "number", defaultValue: 0, label: "Ordre d'\xE9valuation" },
|
|
10536
|
+
{
|
|
10537
|
+
name: "conditions",
|
|
10538
|
+
type: "array",
|
|
10539
|
+
label: "Conditions",
|
|
10540
|
+
fields: [
|
|
10541
|
+
{ name: "field", type: "select", required: true, options: CONDITION_FIELDS.map((v) => ({ label: v, value: v })) },
|
|
10542
|
+
{
|
|
10543
|
+
name: "operator",
|
|
10544
|
+
type: "select",
|
|
10545
|
+
defaultValue: "equals",
|
|
10546
|
+
required: true,
|
|
10547
|
+
options: [{ label: "= \xE9gal", value: "equals" }, { label: "\u2260 diff\xE9rent", value: "not_equals" }, { label: "contient", value: "contains" }]
|
|
10548
|
+
},
|
|
10549
|
+
{ name: "value", type: "text", required: true }
|
|
10550
|
+
]
|
|
10551
|
+
},
|
|
10552
|
+
{
|
|
10553
|
+
name: "actions",
|
|
10554
|
+
type: "array",
|
|
10555
|
+
label: "Actions",
|
|
10556
|
+
fields: [
|
|
10557
|
+
{ name: "type", type: "select", required: true, options: ACTION_TYPES },
|
|
10558
|
+
{ name: "value", type: "text", required: true }
|
|
10559
|
+
]
|
|
10560
|
+
}
|
|
10561
|
+
],
|
|
10562
|
+
timestamps: true
|
|
10563
|
+
};
|
|
10564
|
+
}
|
|
10565
|
+
|
|
10566
|
+
// src/collections/SupportTeam.ts
|
|
10567
|
+
function createSupportTeamCollection(slugs) {
|
|
10568
|
+
return {
|
|
10569
|
+
slug: slugs.supportTeams,
|
|
10570
|
+
labels: { singular: "\xC9quipe support", plural: "\xC9quipes support" },
|
|
10571
|
+
admin: { useAsTitle: "name", group: "Support", defaultColumns: ["name", "members"] },
|
|
10572
|
+
access: {
|
|
10573
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10574
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
10575
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
10576
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10577
|
+
},
|
|
10578
|
+
fields: [
|
|
10579
|
+
{ name: "name", type: "text", required: true, label: "Nom" },
|
|
10580
|
+
{ name: "description", type: "textarea", label: "Description" },
|
|
10581
|
+
{ name: "members", type: "relationship", relationTo: slugs.users, hasMany: true, index: true, label: "Membres (agents)" }
|
|
10582
|
+
],
|
|
10583
|
+
timestamps: true
|
|
10584
|
+
};
|
|
10585
|
+
}
|
|
10586
|
+
|
|
10587
|
+
// src/collections/PushSubscription.ts
|
|
10588
|
+
function createPushSubscriptionCollection(slugs) {
|
|
10589
|
+
return {
|
|
10590
|
+
slug: slugs.pushSubscriptions,
|
|
10591
|
+
labels: { singular: "Abonnement push", plural: "Abonnements push" },
|
|
10592
|
+
admin: { hidden: true, group: "Support", defaultColumns: ["user", "endpoint", "createdAt"] },
|
|
10593
|
+
access: {
|
|
10594
|
+
create: () => false,
|
|
10595
|
+
// written via the subscribe endpoint (overrideAccess)
|
|
10596
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10597
|
+
update: () => false,
|
|
10598
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10599
|
+
},
|
|
10600
|
+
fields: [
|
|
10601
|
+
{ name: "user", type: "relationship", relationTo: slugs.users, index: true, label: "Agent" },
|
|
10602
|
+
{ name: "endpoint", type: "text", required: true, index: true, label: "Endpoint" },
|
|
10603
|
+
{ name: "p256dh", type: "text", label: "Cl\xE9 p256dh" },
|
|
10604
|
+
{ name: "auth", type: "text", label: "Cl\xE9 auth" },
|
|
10605
|
+
{ name: "userAgent", type: "text", label: "User-Agent" }
|
|
10606
|
+
],
|
|
10607
|
+
timestamps: true
|
|
10608
|
+
};
|
|
10609
|
+
}
|
|
10610
|
+
|
|
9529
10611
|
// src/plugin.ts
|
|
9530
10612
|
function viewConfig(component, path) {
|
|
9531
10613
|
return { Component: component, path };
|
|
@@ -9561,7 +10643,11 @@ function supportPlugin(config) {
|
|
|
9561
10643
|
createSatisfactionSurveysCollection(slugs),
|
|
9562
10644
|
createKnowledgeBaseCollection(slugs),
|
|
9563
10645
|
createTicketFeedbackCollection(slugs, { notificationSlug: config?.notificationSlug }),
|
|
9564
|
-
createTicketCollaboratorsCollection(slugs)
|
|
10646
|
+
createTicketCollaboratorsCollection(slugs),
|
|
10647
|
+
createNotificationQueueCollection(slugs),
|
|
10648
|
+
createAutomationRulesCollection(slugs),
|
|
10649
|
+
createSupportTeamCollection(slugs),
|
|
10650
|
+
createPushSubscriptionCollection(slugs)
|
|
9565
10651
|
];
|
|
9566
10652
|
if (features.authLogs !== false) supportCollections.push(createAuthLogsCollection(slugs));
|
|
9567
10653
|
if (features.timeTracking !== false) supportCollections.push(createTimeEntriesCollection(slugs));
|