@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.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import crypto3, {
|
|
1
|
+
import crypto3, { createHmac, randomBytes } from 'crypto';
|
|
2
|
+
import PDFDocument from 'pdfkit';
|
|
3
|
+
import { APIError, getFieldsToSign, jwtSign } from 'payload';
|
|
4
|
+
import webpush from 'web-push';
|
|
5
|
+
import sanitizeHtml from 'sanitize-html';
|
|
2
6
|
|
|
3
7
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
8
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -58,6 +62,10 @@ var DEFAULT_SLUGS = {
|
|
|
58
62
|
macros: "macros",
|
|
59
63
|
ticketStatuses: "ticket-statuses",
|
|
60
64
|
ticketFeedback: "ticket-feedback",
|
|
65
|
+
notificationQueue: "notification-queue",
|
|
66
|
+
automationRules: "automation-rules",
|
|
67
|
+
supportTeams: "support-teams",
|
|
68
|
+
pushSubscriptions: "push-subscriptions",
|
|
61
69
|
users: "users",
|
|
62
70
|
media: "media"
|
|
63
71
|
};
|
|
@@ -89,6 +97,26 @@ function handleAuthError(error) {
|
|
|
89
97
|
return null;
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
// src/utils/db.ts
|
|
101
|
+
function dbFind(payload, slug, options = {}) {
|
|
102
|
+
return payload.find({ collection: slug, ...options });
|
|
103
|
+
}
|
|
104
|
+
function dbFindByID(payload, slug, options) {
|
|
105
|
+
return payload.findByID({ collection: slug, ...options });
|
|
106
|
+
}
|
|
107
|
+
function dbCreate(payload, slug, options) {
|
|
108
|
+
return payload.create({ collection: slug, ...options });
|
|
109
|
+
}
|
|
110
|
+
function dbUpdate(payload, slug, options) {
|
|
111
|
+
return payload.update({ collection: slug, ...options });
|
|
112
|
+
}
|
|
113
|
+
function dbCount(payload, slug, options = {}) {
|
|
114
|
+
return payload.count({ collection: slug, ...options });
|
|
115
|
+
}
|
|
116
|
+
function dbDelete(payload, slug, options) {
|
|
117
|
+
return payload.delete({ collection: slug, ...options });
|
|
118
|
+
}
|
|
119
|
+
|
|
92
120
|
// src/utils/readSettings.ts
|
|
93
121
|
var PREF_KEY = "support-settings";
|
|
94
122
|
var USER_PREFS_KEY_PREFIX = "support-user-prefs";
|
|
@@ -102,10 +130,18 @@ var DEFAULT_USER_PREFS = {
|
|
|
102
130
|
locale: "fr",
|
|
103
131
|
signature: ""
|
|
104
132
|
};
|
|
133
|
+
var settingsCache = null;
|
|
134
|
+
var SETTINGS_TTL_MS = 6e4;
|
|
135
|
+
function invalidateSupportSettingsCache() {
|
|
136
|
+
settingsCache = null;
|
|
137
|
+
}
|
|
105
138
|
async function readSupportSettings(payload) {
|
|
139
|
+
if (settingsCache && Date.now() - settingsCache.ts < SETTINGS_TTL_MS) {
|
|
140
|
+
return settingsCache.value;
|
|
141
|
+
}
|
|
142
|
+
let value = { ...DEFAULT_SETTINGS };
|
|
106
143
|
try {
|
|
107
|
-
const prefs = await payload
|
|
108
|
-
collection: "payload-preferences",
|
|
144
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
109
145
|
where: { key: { equals: PREF_KEY } },
|
|
110
146
|
limit: 1,
|
|
111
147
|
depth: 0,
|
|
@@ -113,7 +149,7 @@ async function readSupportSettings(payload) {
|
|
|
113
149
|
});
|
|
114
150
|
if (prefs.docs.length > 0) {
|
|
115
151
|
const stored = prefs.docs[0].value;
|
|
116
|
-
|
|
152
|
+
value = {
|
|
117
153
|
email: { ...DEFAULT_SETTINGS.email, ...stored.email },
|
|
118
154
|
ai: { ...DEFAULT_SETTINGS.ai, ...stored.ai },
|
|
119
155
|
sla: { ...DEFAULT_SETTINGS.sla, ...stored.sla },
|
|
@@ -122,13 +158,13 @@ async function readSupportSettings(payload) {
|
|
|
122
158
|
}
|
|
123
159
|
} catch {
|
|
124
160
|
}
|
|
125
|
-
|
|
161
|
+
settingsCache = { value, ts: Date.now() };
|
|
162
|
+
return value;
|
|
126
163
|
}
|
|
127
164
|
async function readUserPrefs(payload, userId) {
|
|
128
165
|
try {
|
|
129
166
|
const key = `${USER_PREFS_KEY_PREFIX}-${userId}`;
|
|
130
|
-
const prefs = await payload
|
|
131
|
-
collection: "payload-preferences",
|
|
167
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
132
168
|
where: { key: { equals: key } },
|
|
133
169
|
limit: 1,
|
|
134
170
|
depth: 0,
|
|
@@ -314,8 +350,130 @@ ${text}`;
|
|
|
314
350
|
};
|
|
315
351
|
}
|
|
316
352
|
|
|
317
|
-
// src/
|
|
353
|
+
// src/utils/aiAgent.ts
|
|
354
|
+
async function applyAgentDecision(payload, slugs, ticketId, decision) {
|
|
355
|
+
if (decision.action === "reply" && decision.reply && decision.reply.trim()) {
|
|
356
|
+
const msg = await dbCreate(payload, slugs.ticketMessages, {
|
|
357
|
+
data: {
|
|
358
|
+
ticket: ticketId,
|
|
359
|
+
body: decision.reply.trim(),
|
|
360
|
+
authorType: "admin",
|
|
361
|
+
isInternal: false,
|
|
362
|
+
fromAlias: "Assistant IA"
|
|
363
|
+
},
|
|
364
|
+
overrideAccess: true
|
|
365
|
+
});
|
|
366
|
+
return { acted: "reply", messageId: msg.id, confidence: decision.confidence };
|
|
367
|
+
}
|
|
368
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
369
|
+
data: {
|
|
370
|
+
ticket: ticketId,
|
|
371
|
+
body: `[Agent IA] Escalade vers un humain. Raison : ${decision.reason || "non d\xE9termin\xE9e"} (confiance ${Math.round((decision.confidence ?? 0) * 100)} %).`,
|
|
372
|
+
authorType: "admin",
|
|
373
|
+
isInternal: true,
|
|
374
|
+
skipNotification: true
|
|
375
|
+
},
|
|
376
|
+
overrideAccess: true
|
|
377
|
+
});
|
|
378
|
+
await dbUpdate(payload, slugs.tickets, { id: ticketId, data: { status: "escalated" }, overrideAccess: true });
|
|
379
|
+
return { acted: "escalate", confidence: decision.confidence };
|
|
380
|
+
}
|
|
318
381
|
function getClient2(aiSettings) {
|
|
382
|
+
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
383
|
+
if (aiSettings.provider === "ollama") {
|
|
384
|
+
return new Anthropic({ apiKey: "ollama", baseURL: process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1" });
|
|
385
|
+
}
|
|
386
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
387
|
+
}
|
|
388
|
+
function stripHtml(s) {
|
|
389
|
+
return (s || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
390
|
+
}
|
|
391
|
+
function parseDecision(text) {
|
|
392
|
+
try {
|
|
393
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
394
|
+
if (match) {
|
|
395
|
+
const obj = JSON.parse(match[0]);
|
|
396
|
+
return {
|
|
397
|
+
action: obj.action === "reply" ? "reply" : "escalate",
|
|
398
|
+
reply: typeof obj.reply === "string" ? obj.reply : void 0,
|
|
399
|
+
reason: typeof obj.reason === "string" ? obj.reason : void 0,
|
|
400
|
+
confidence: typeof obj.confidence === "number" ? obj.confidence : 0
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
}
|
|
405
|
+
return { action: "escalate", reason: "R\xE9ponse IA non interpr\xE9table", confidence: 0 };
|
|
406
|
+
}
|
|
407
|
+
async function runAiAgent(payload, slugs, ticketId, opts) {
|
|
408
|
+
const settings = await readSupportSettings(payload);
|
|
409
|
+
const threshold = opts?.confidenceThreshold ?? 0.7;
|
|
410
|
+
const ticket = await dbFindByID(payload, slugs.tickets, { id: ticketId, depth: 0, overrideAccess: true });
|
|
411
|
+
const messages = await dbFind(payload, slugs.ticketMessages, { where: { ticket: { equals: ticketId } }, sort: "createdAt", limit: 50, depth: 0, overrideAccess: true });
|
|
412
|
+
const kb = await dbFind(payload, slugs.knowledgeBase, { where: { published: { equals: true } }, limit: 20, depth: 0, overrideAccess: true });
|
|
413
|
+
const conversation = messages.docs.map((m) => `${m.authorType}: ${m.body || ""}`).join("\n");
|
|
414
|
+
const kbText = kb.docs.map((a) => `# ${a.title || ""}
|
|
415
|
+
${stripHtml(String(a.content ?? ""))}`).join("\n\n").slice(0, 8e3);
|
|
416
|
+
const prompt = `Tu es un agent de support IA. \xC0 partir du ticket et de la base de connaissances ci-dessous, d\xE9cide :
|
|
417
|
+
- "reply" si tu peux r\xE9pondre avec certitude UNIQUEMENT \xE0 partir de la base de connaissances ;
|
|
418
|
+
- "escalate" sinon (question hors KB, ambigu\xEB, sensible, ou n\xE9cessitant une action humaine).
|
|
419
|
+
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>}.
|
|
420
|
+
|
|
421
|
+
TICKET : ${ticket.subject || ""}
|
|
422
|
+
CONVERSATION :
|
|
423
|
+
${conversation}
|
|
424
|
+
|
|
425
|
+
BASE DE CONNAISSANCES :
|
|
426
|
+
${kbText || "(vide)"}`;
|
|
427
|
+
let decision;
|
|
428
|
+
try {
|
|
429
|
+
const client = getClient2(settings.ai);
|
|
430
|
+
const resp = await client.messages.create({
|
|
431
|
+
model: settings.ai.model || "claude-haiku-4-5-20251001",
|
|
432
|
+
max_tokens: 1024,
|
|
433
|
+
messages: [{ role: "user", content: prompt }]
|
|
434
|
+
});
|
|
435
|
+
const text = resp?.content?.[0]?.text || "";
|
|
436
|
+
decision = parseDecision(text);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
console.error("[support] AI agent decision failed:", err);
|
|
439
|
+
decision = { action: "escalate", reason: "Erreur du fournisseur IA", confidence: 0 };
|
|
440
|
+
}
|
|
441
|
+
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 };
|
|
442
|
+
return applyAgentDecision(payload, slugs, ticketId, finalDecision);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/endpoints/ai-agent.ts
|
|
446
|
+
function createAiAgentEndpoint(slugs) {
|
|
447
|
+
return {
|
|
448
|
+
path: "/support/ai-agent",
|
|
449
|
+
method: "post",
|
|
450
|
+
handler: async (req) => {
|
|
451
|
+
try {
|
|
452
|
+
requireAdmin(req, slugs);
|
|
453
|
+
let body = {};
|
|
454
|
+
try {
|
|
455
|
+
body = await req.json();
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
if (!body.ticketId) {
|
|
459
|
+
return Response.json({ error: "ticketId requis." }, { status: 400 });
|
|
460
|
+
}
|
|
461
|
+
const result = await runAiAgent(req.payload, slugs, body.ticketId, {
|
|
462
|
+
confidenceThreshold: body.confidenceThreshold
|
|
463
|
+
});
|
|
464
|
+
return Response.json({ ok: true, ...result });
|
|
465
|
+
} catch (error) {
|
|
466
|
+
const authResponse = handleAuthError(error);
|
|
467
|
+
if (authResponse) return authResponse;
|
|
468
|
+
console.error("[ai-agent] Error:", error);
|
|
469
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/endpoints/client-intelligence.ts
|
|
476
|
+
function getClient3(aiSettings) {
|
|
319
477
|
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
320
478
|
if (aiSettings.provider === "ollama") {
|
|
321
479
|
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
@@ -469,7 +627,7 @@ R\xE9ponds en JSON strict (pas de markdown, pas de commentaires) avec cette stru
|
|
|
469
627
|
}
|
|
470
628
|
|
|
471
629
|
Sois factuel. Ne d\xE9passe pas 5 items par tableau. R\xE9ponds UNIQUEMENT avec le JSON.`;
|
|
472
|
-
const anthropic =
|
|
630
|
+
const anthropic = getClient3(aiSettings);
|
|
473
631
|
const model = getModel2(aiSettings);
|
|
474
632
|
const res = await anthropic.messages.create({
|
|
475
633
|
model,
|
|
@@ -533,8 +691,7 @@ function createSearchEndpoint(slugs) {
|
|
|
533
691
|
return Response.json({ tickets: [], clients: [], messages: [], articles: [] });
|
|
534
692
|
}
|
|
535
693
|
const [ticketsRes, clientsRes, messagesRes, articlesRes] = await Promise.all([
|
|
536
|
-
payload.
|
|
537
|
-
collection: slugs.tickets,
|
|
694
|
+
dbFind(payload, slugs.tickets, {
|
|
538
695
|
where: {
|
|
539
696
|
or: [
|
|
540
697
|
{ ticketNumber: { contains: q } },
|
|
@@ -546,8 +703,7 @@ function createSearchEndpoint(slugs) {
|
|
|
546
703
|
depth: 1,
|
|
547
704
|
overrideAccess: true
|
|
548
705
|
}),
|
|
549
|
-
payload.
|
|
550
|
-
collection: slugs.supportClients,
|
|
706
|
+
dbFind(payload, slugs.supportClients, {
|
|
551
707
|
where: {
|
|
552
708
|
or: [
|
|
553
709
|
{ firstName: { contains: q } },
|
|
@@ -560,16 +716,14 @@ function createSearchEndpoint(slugs) {
|
|
|
560
716
|
depth: 0,
|
|
561
717
|
overrideAccess: true
|
|
562
718
|
}),
|
|
563
|
-
payload.
|
|
564
|
-
collection: slugs.ticketMessages,
|
|
719
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
565
720
|
where: { body: { contains: q } },
|
|
566
721
|
sort: "-createdAt",
|
|
567
722
|
limit: 5,
|
|
568
723
|
depth: 1,
|
|
569
724
|
overrideAccess: true
|
|
570
725
|
}),
|
|
571
|
-
payload.
|
|
572
|
-
collection: slugs.knowledgeBase,
|
|
726
|
+
dbFind(payload, slugs.knowledgeBase, {
|
|
573
727
|
where: { title: { contains: q } },
|
|
574
728
|
limit: 5,
|
|
575
729
|
depth: 0,
|
|
@@ -669,8 +823,7 @@ function createKbSearchEndpoint(slugs) {
|
|
|
669
823
|
orClauses.push({ category: { contains: t } });
|
|
670
824
|
}
|
|
671
825
|
orClauses.push({ title: { contains: q } });
|
|
672
|
-
const res = await payload.
|
|
673
|
-
collection: slugs.knowledgeBase,
|
|
826
|
+
const res = await dbFind(payload, slugs.knowledgeBase, {
|
|
674
827
|
where: {
|
|
675
828
|
and: [
|
|
676
829
|
{ published: { equals: true } },
|
|
@@ -729,16 +882,14 @@ function createBulkActionEndpoint(slugs) {
|
|
|
729
882
|
try {
|
|
730
883
|
switch (action) {
|
|
731
884
|
case "close":
|
|
732
|
-
await payload.
|
|
733
|
-
collection: slugs.tickets,
|
|
885
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
734
886
|
id: ticketId,
|
|
735
887
|
data: { status: "resolved", resolvedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
736
888
|
overrideAccess: true
|
|
737
889
|
});
|
|
738
890
|
break;
|
|
739
891
|
case "reopen":
|
|
740
|
-
await payload.
|
|
741
|
-
collection: slugs.tickets,
|
|
892
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
742
893
|
id: ticketId,
|
|
743
894
|
data: { status: "open" },
|
|
744
895
|
overrideAccess: true
|
|
@@ -746,8 +897,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
746
897
|
break;
|
|
747
898
|
case "assign":
|
|
748
899
|
if (value) {
|
|
749
|
-
await payload.
|
|
750
|
-
collection: slugs.tickets,
|
|
900
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
751
901
|
id: ticketId,
|
|
752
902
|
data: { assignedTo: Number(value) },
|
|
753
903
|
overrideAccess: true
|
|
@@ -756,8 +906,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
756
906
|
break;
|
|
757
907
|
case "set_priority":
|
|
758
908
|
if (value) {
|
|
759
|
-
await payload.
|
|
760
|
-
collection: slugs.tickets,
|
|
909
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
761
910
|
id: ticketId,
|
|
762
911
|
data: { priority: String(value) },
|
|
763
912
|
overrideAccess: true
|
|
@@ -766,8 +915,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
766
915
|
break;
|
|
767
916
|
case "set_category":
|
|
768
917
|
if (value) {
|
|
769
|
-
await payload.
|
|
770
|
-
collection: slugs.tickets,
|
|
918
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
771
919
|
id: ticketId,
|
|
772
920
|
data: { category: String(value) },
|
|
773
921
|
overrideAccess: true
|
|
@@ -775,8 +923,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
775
923
|
}
|
|
776
924
|
break;
|
|
777
925
|
case "delete":
|
|
778
|
-
await payload.
|
|
779
|
-
collection: slugs.tickets,
|
|
926
|
+
await dbDelete(payload, slugs.tickets, {
|
|
780
927
|
id: ticketId,
|
|
781
928
|
overrideAccess: true
|
|
782
929
|
});
|
|
@@ -815,8 +962,8 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
815
962
|
return Response.json({ error: "Impossible de fusionner un ticket avec lui-m\xEAme" }, { status: 400 });
|
|
816
963
|
}
|
|
817
964
|
const [source, target] = await Promise.all([
|
|
818
|
-
payload
|
|
819
|
-
payload
|
|
965
|
+
dbFindByID(payload, slugs.tickets, { id: sourceTicketId, depth: 0, overrideAccess: true }),
|
|
966
|
+
dbFindByID(payload, slugs.tickets, { id: targetTicketId, depth: 0, overrideAccess: true })
|
|
820
967
|
]);
|
|
821
968
|
if (!source) return Response.json({ error: "Ticket source introuvable" }, { status: 404 });
|
|
822
969
|
if (!target) return Response.json({ error: "Ticket cible introuvable" }, { status: 404 });
|
|
@@ -825,39 +972,34 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
825
972
|
if (sourceClient !== targetClient) {
|
|
826
973
|
return Response.json({ error: "Les deux tickets doivent appartenir au m\xEAme client" }, { status: 400 });
|
|
827
974
|
}
|
|
828
|
-
const messages = await payload.
|
|
829
|
-
collection: slugs.ticketMessages,
|
|
975
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
830
976
|
where: { ticket: { equals: sourceTicketId } },
|
|
831
977
|
limit: 500,
|
|
832
978
|
depth: 0,
|
|
833
979
|
overrideAccess: true
|
|
834
980
|
});
|
|
835
981
|
for (const msg of messages.docs) {
|
|
836
|
-
await payload.
|
|
837
|
-
collection: slugs.ticketMessages,
|
|
982
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
838
983
|
id: msg.id,
|
|
839
984
|
data: { ticket: targetTicketId },
|
|
840
985
|
overrideAccess: true
|
|
841
986
|
});
|
|
842
987
|
}
|
|
843
|
-
const timeEntries = await payload.
|
|
844
|
-
collection: slugs.timeEntries,
|
|
988
|
+
const timeEntries = await dbFind(payload, slugs.timeEntries, {
|
|
845
989
|
where: { ticket: { equals: sourceTicketId } },
|
|
846
990
|
limit: 500,
|
|
847
991
|
depth: 0,
|
|
848
992
|
overrideAccess: true
|
|
849
993
|
});
|
|
850
994
|
for (const entry of timeEntries.docs) {
|
|
851
|
-
await payload.
|
|
852
|
-
collection: slugs.timeEntries,
|
|
995
|
+
await dbUpdate(payload, slugs.timeEntries, {
|
|
853
996
|
id: entry.id,
|
|
854
997
|
data: { ticket: targetTicketId },
|
|
855
998
|
overrideAccess: true
|
|
856
999
|
});
|
|
857
1000
|
}
|
|
858
1001
|
const sourceNumber = source.ticketNumber || `#${sourceTicketId}`;
|
|
859
|
-
await payload.
|
|
860
|
-
collection: slugs.ticketMessages,
|
|
1002
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
861
1003
|
data: {
|
|
862
1004
|
ticket: targetTicketId,
|
|
863
1005
|
body: `Messages fusionn\xE9s depuis ${sourceNumber} (${messages.totalDocs} messages, ${timeEntries.totalDocs} entr\xE9es de temps)`,
|
|
@@ -867,13 +1009,11 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
867
1009
|
},
|
|
868
1010
|
overrideAccess: true
|
|
869
1011
|
});
|
|
870
|
-
await payload.
|
|
871
|
-
collection: slugs.tickets,
|
|
1012
|
+
await dbDelete(payload, slugs.tickets, {
|
|
872
1013
|
id: sourceTicketId,
|
|
873
1014
|
overrideAccess: true
|
|
874
1015
|
});
|
|
875
|
-
const allTimeEntries = await payload.
|
|
876
|
-
collection: slugs.timeEntries,
|
|
1016
|
+
const allTimeEntries = await dbFind(payload, slugs.timeEntries, {
|
|
877
1017
|
where: { ticket: { equals: targetTicketId } },
|
|
878
1018
|
limit: 500,
|
|
879
1019
|
depth: 0,
|
|
@@ -883,8 +1023,7 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
883
1023
|
(sum, e) => sum + (e.duration || 0),
|
|
884
1024
|
0
|
|
885
1025
|
);
|
|
886
|
-
await payload.
|
|
887
|
-
collection: slugs.tickets,
|
|
1026
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
888
1027
|
id: targetTicketId,
|
|
889
1028
|
data: { totalTimeMinutes: totalMinutes },
|
|
890
1029
|
overrideAccess: true
|
|
@@ -919,8 +1058,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
919
1058
|
if (!messageId) {
|
|
920
1059
|
return Response.json({ error: "messageId required" }, { status: 400 });
|
|
921
1060
|
}
|
|
922
|
-
const message = await payload.
|
|
923
|
-
collection: slugs.ticketMessages,
|
|
1061
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
924
1062
|
id: messageId,
|
|
925
1063
|
depth: 1,
|
|
926
1064
|
overrideAccess: true
|
|
@@ -933,8 +1071,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
933
1071
|
return Response.json({ error: "Cannot resolve source ticket" }, { status: 400 });
|
|
934
1072
|
}
|
|
935
1073
|
const clientId = typeof sourceTicket.client === "object" ? sourceTicket.client.id : sourceTicket.client;
|
|
936
|
-
const newTicket = await payload.
|
|
937
|
-
collection: slugs.tickets,
|
|
1074
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
938
1075
|
data: {
|
|
939
1076
|
subject: subject || `Split: ${sourceTicket.subject}`,
|
|
940
1077
|
client: clientId,
|
|
@@ -949,8 +1086,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
949
1086
|
const attachments = message.attachments?.map((a) => ({
|
|
950
1087
|
file: typeof a.file === "object" ? a.file.id : a.file
|
|
951
1088
|
})) || [];
|
|
952
|
-
await payload.
|
|
953
|
-
collection: slugs.ticketMessages,
|
|
1089
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
954
1090
|
data: {
|
|
955
1091
|
ticket: newTicket.id,
|
|
956
1092
|
body: message.body,
|
|
@@ -963,8 +1099,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
963
1099
|
},
|
|
964
1100
|
overrideAccess: true
|
|
965
1101
|
});
|
|
966
|
-
await payload.
|
|
967
|
-
collection: slugs.ticketMessages,
|
|
1102
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
968
1103
|
data: {
|
|
969
1104
|
ticket: sourceTicket.id,
|
|
970
1105
|
body: `Message extrait vers le ticket ${newTicket.ticketNumber}`,
|
|
@@ -976,15 +1111,13 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
976
1111
|
});
|
|
977
1112
|
const existingRelated = Array.isArray(sourceTicket.relatedTickets) ? sourceTicket.relatedTickets.map((t) => typeof t === "object" ? t.id : t) : [];
|
|
978
1113
|
if (!existingRelated.includes(newTicket.id)) {
|
|
979
|
-
await payload.
|
|
980
|
-
collection: slugs.tickets,
|
|
1114
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
981
1115
|
id: sourceTicket.id,
|
|
982
1116
|
data: { relatedTickets: [...existingRelated, newTicket.id] },
|
|
983
1117
|
overrideAccess: true
|
|
984
1118
|
});
|
|
985
1119
|
}
|
|
986
|
-
await payload.
|
|
987
|
-
collection: slugs.ticketActivityLog,
|
|
1120
|
+
await dbCreate(payload, slugs.ticketActivityLog, {
|
|
988
1121
|
data: {
|
|
989
1122
|
ticket: sourceTicket.id,
|
|
990
1123
|
action: "split",
|
|
@@ -1199,8 +1332,7 @@ function createSettingsGetEndpoint(slugs) {
|
|
|
1199
1332
|
try {
|
|
1200
1333
|
const payload = req.payload;
|
|
1201
1334
|
requireAdmin(req, slugs);
|
|
1202
|
-
const prefs = await payload
|
|
1203
|
-
collection: "payload-preferences",
|
|
1335
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1204
1336
|
where: { key: { equals: PREF_KEY2 } },
|
|
1205
1337
|
limit: 1,
|
|
1206
1338
|
depth: 0,
|
|
@@ -1257,6 +1389,7 @@ function createSettingsPostEndpoint(slugs) {
|
|
|
1257
1389
|
]
|
|
1258
1390
|
}
|
|
1259
1391
|
});
|
|
1392
|
+
invalidateSupportSettingsCache();
|
|
1260
1393
|
return Response.json(merged);
|
|
1261
1394
|
} catch (error) {
|
|
1262
1395
|
const authResponse = handleAuthError(error);
|
|
@@ -1278,8 +1411,7 @@ function createSignatureGetEndpoint(slugs) {
|
|
|
1278
1411
|
try {
|
|
1279
1412
|
const payload = req.payload;
|
|
1280
1413
|
requireAdmin(req, slugs);
|
|
1281
|
-
const prefs = await payload
|
|
1282
|
-
collection: "payload-preferences",
|
|
1414
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1283
1415
|
where: { key: { equals: `${PREF_KEY3}-${req.user.id}` } },
|
|
1284
1416
|
limit: 1,
|
|
1285
1417
|
depth: 0,
|
|
@@ -1306,8 +1438,7 @@ function createSignaturePostEndpoint(slugs) {
|
|
|
1306
1438
|
requireAdmin(req, slugs);
|
|
1307
1439
|
const { signature } = await req.json();
|
|
1308
1440
|
const key = `${PREF_KEY3}-${req.user.id}`;
|
|
1309
|
-
const existing = await payload
|
|
1310
|
-
collection: "payload-preferences",
|
|
1441
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
1311
1442
|
where: { key: { equals: key } },
|
|
1312
1443
|
limit: 1,
|
|
1313
1444
|
depth: 0,
|
|
@@ -1350,8 +1481,7 @@ function createRoundRobinConfigGetEndpoint(slugs) {
|
|
|
1350
1481
|
try {
|
|
1351
1482
|
const payload = req.payload;
|
|
1352
1483
|
requireAdmin(req, slugs);
|
|
1353
|
-
const prefs = await payload
|
|
1354
|
-
collection: "payload-preferences",
|
|
1484
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1355
1485
|
where: { key: { equals: PREF_KEY4 } },
|
|
1356
1486
|
limit: 1,
|
|
1357
1487
|
depth: 0,
|
|
@@ -1377,8 +1507,7 @@ function createRoundRobinConfigPostEndpoint(slugs) {
|
|
|
1377
1507
|
const payload = req.payload;
|
|
1378
1508
|
requireAdmin(req, slugs);
|
|
1379
1509
|
const { enabled } = await req.json();
|
|
1380
|
-
const existing = await payload
|
|
1381
|
-
collection: "payload-preferences",
|
|
1510
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
1382
1511
|
where: { key: { equals: PREF_KEY4 } },
|
|
1383
1512
|
limit: 1,
|
|
1384
1513
|
depth: 0,
|
|
@@ -1422,8 +1551,7 @@ function createSlaCheckEndpoint(slugs) {
|
|
|
1422
1551
|
requireAdmin(req, slugs);
|
|
1423
1552
|
const now = /* @__PURE__ */ new Date();
|
|
1424
1553
|
const nowISO = now.toISOString();
|
|
1425
|
-
const { docs: tickets } = await payload.
|
|
1426
|
-
collection: slugs.tickets,
|
|
1554
|
+
const { docs: tickets } = await dbFind(payload, slugs.tickets, {
|
|
1427
1555
|
where: {
|
|
1428
1556
|
and: [
|
|
1429
1557
|
{ status: { in: ["open", "waiting_client"] } },
|
|
@@ -1517,6 +1645,107 @@ function createSlaCheckEndpoint(slugs) {
|
|
|
1517
1645
|
}
|
|
1518
1646
|
};
|
|
1519
1647
|
}
|
|
1648
|
+
var TRANSPARENT_GIF = Buffer.from(
|
|
1649
|
+
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
1650
|
+
"base64"
|
|
1651
|
+
);
|
|
1652
|
+
function generateTrackingToken(ticketId, messageId, secret) {
|
|
1653
|
+
return createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex").substring(0, 16);
|
|
1654
|
+
}
|
|
1655
|
+
function createTrackOpenEndpoint(slugs) {
|
|
1656
|
+
return {
|
|
1657
|
+
path: "/support/track-open",
|
|
1658
|
+
method: "get",
|
|
1659
|
+
handler: async (req) => {
|
|
1660
|
+
const url = new URL(req.url);
|
|
1661
|
+
const ticketId = url.searchParams.get("t");
|
|
1662
|
+
const messageId = url.searchParams.get("m");
|
|
1663
|
+
const sig = url.searchParams.get("sig");
|
|
1664
|
+
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
1665
|
+
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
1666
|
+
const secret = process.env.PAYLOAD_SECRET || "";
|
|
1667
|
+
const validSig = !!secret && !!ticketId && !!messageId && !!sig && sig === generateTrackingToken(ticketId, messageId, secret);
|
|
1668
|
+
if (!validSig) {
|
|
1669
|
+
return new Response(TRANSPARENT_GIF, {
|
|
1670
|
+
status: 200,
|
|
1671
|
+
headers: {
|
|
1672
|
+
"Content-Type": "image/gif",
|
|
1673
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
1674
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
1679
|
+
try {
|
|
1680
|
+
const payload = req.payload;
|
|
1681
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
1682
|
+
id: parsedId,
|
|
1683
|
+
depth: 0,
|
|
1684
|
+
overrideAccess: true,
|
|
1685
|
+
select: { lastClientReadAt: true }
|
|
1686
|
+
});
|
|
1687
|
+
if (ticket) {
|
|
1688
|
+
const lastRead = ticket.lastClientReadAt ? new Date(ticket.lastClientReadAt).getTime() : 0;
|
|
1689
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
|
|
1690
|
+
if (lastRead < fiveMinAgo) {
|
|
1691
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1692
|
+
id: parsedId,
|
|
1693
|
+
data: { lastClientReadAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1694
|
+
overrideAccess: true
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
1699
|
+
const msg = await dbFindByID(payload, slugs.ticketMessages, {
|
|
1700
|
+
id: parsedMsgId,
|
|
1701
|
+
depth: 0,
|
|
1702
|
+
overrideAccess: true,
|
|
1703
|
+
select: { emailOpenedAt: true }
|
|
1704
|
+
});
|
|
1705
|
+
if (msg && !msg.emailOpenedAt) {
|
|
1706
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
1707
|
+
id: parsedMsgId,
|
|
1708
|
+
data: { emailOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1709
|
+
overrideAccess: true
|
|
1710
|
+
});
|
|
1711
|
+
const ticketInfo = await dbFindByID(payload, slugs.tickets, {
|
|
1712
|
+
id: parsedId,
|
|
1713
|
+
depth: 1,
|
|
1714
|
+
overrideAccess: true,
|
|
1715
|
+
select: { ticketNumber: true, subject: true, client: true }
|
|
1716
|
+
});
|
|
1717
|
+
const clientName = typeof ticketInfo?.client === "object" ? ticketInfo.client?.firstName || "Client" : "Client";
|
|
1718
|
+
try {
|
|
1719
|
+
await dbCreate(payload, "admin-notifications", {
|
|
1720
|
+
data: {
|
|
1721
|
+
title: `Email ouvert \u2014 ${ticketInfo?.ticketNumber || "TK-????"}`,
|
|
1722
|
+
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || "ticket"}"`,
|
|
1723
|
+
type: "email_opened",
|
|
1724
|
+
link: `/admin/ticket?id=${parsedId}`
|
|
1725
|
+
},
|
|
1726
|
+
overrideAccess: true
|
|
1727
|
+
});
|
|
1728
|
+
} catch {
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
console.error("[track-open] Error:", err);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return new Response(TRANSPARENT_GIF, {
|
|
1737
|
+
status: 200,
|
|
1738
|
+
headers: {
|
|
1739
|
+
"Content-Type": "image/gif",
|
|
1740
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
1741
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
1742
|
+
"Pragma": "no-cache",
|
|
1743
|
+
"Expires": "0"
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1520
1749
|
|
|
1521
1750
|
// src/utils/emailTemplate.ts
|
|
1522
1751
|
var COLORS = {
|
|
@@ -1553,7 +1782,10 @@ function escapeHtml(str) {
|
|
|
1553
1782
|
}
|
|
1554
1783
|
function emailTrackingPixel(ticketId, messageId, baseUrl) {
|
|
1555
1784
|
const url = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
1556
|
-
const
|
|
1785
|
+
const secret = process.env.PAYLOAD_SECRET || "";
|
|
1786
|
+
if (!secret || messageId === void 0 || messageId === null) return "";
|
|
1787
|
+
const sig = generateTrackingToken(String(ticketId), String(messageId), secret);
|
|
1788
|
+
const params = `t=${ticketId}&m=${messageId}&sig=${sig}`;
|
|
1557
1789
|
return `<img src="${url}/api/support/track-open?${params}" width="1" height="1" alt="" style="display:block;width:1px;height:1px;border:0;" />`;
|
|
1558
1790
|
}
|
|
1559
1791
|
function emailRichContent(html, config) {
|
|
@@ -1818,8 +2050,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1818
2050
|
const CLOSE_AFTER_REMIND_DAYS = settings.autoClose.reminderDaysBefore;
|
|
1819
2051
|
const results = { reminded: 0, closed: 0, errors: 0 };
|
|
1820
2052
|
const remindCutoff = new Date(now.getTime() - REMIND_AFTER_DAYS * 24 * 60 * 60 * 1e3);
|
|
1821
|
-
const ticketsToRemind = await payload.
|
|
1822
|
-
collection: slugs.tickets,
|
|
2053
|
+
const ticketsToRemind = await dbFind(payload, slugs.tickets, {
|
|
1823
2054
|
where: {
|
|
1824
2055
|
and: [
|
|
1825
2056
|
{ status: { equals: "waiting_client" } },
|
|
@@ -1850,8 +2081,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1850
2081
|
<p><a href="${portalUrl}">R\xE9pondre au ticket</a></p>
|
|
1851
2082
|
</div>`
|
|
1852
2083
|
});
|
|
1853
|
-
await payload.
|
|
1854
|
-
collection: slugs.tickets,
|
|
2084
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1855
2085
|
id: t.id,
|
|
1856
2086
|
data: { autoCloseRemindedAt: now.toISOString() },
|
|
1857
2087
|
overrideAccess: true
|
|
@@ -1863,8 +2093,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1863
2093
|
}
|
|
1864
2094
|
}
|
|
1865
2095
|
const closeCutoff = new Date(now.getTime() - CLOSE_AFTER_REMIND_DAYS * 24 * 60 * 60 * 1e3);
|
|
1866
|
-
const ticketsToClose = await payload.
|
|
1867
|
-
collection: slugs.tickets,
|
|
2096
|
+
const ticketsToClose = await dbFind(payload, slugs.tickets, {
|
|
1868
2097
|
where: {
|
|
1869
2098
|
and: [
|
|
1870
2099
|
{ status: { equals: "waiting_client" } },
|
|
@@ -1899,8 +2128,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1899
2128
|
const closeTotalDays = REMIND_AFTER_DAYS + CLOSE_AFTER_REMIND_DAYS;
|
|
1900
2129
|
const viaSchedule = !!t.autoCloseScheduledAt && new Date(t.autoCloseScheduledAt).getTime() <= now.getTime();
|
|
1901
2130
|
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`;
|
|
1902
|
-
await payload.
|
|
1903
|
-
collection: slugs.ticketMessages,
|
|
2131
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
1904
2132
|
data: {
|
|
1905
2133
|
ticket: t.id,
|
|
1906
2134
|
body: noteBody,
|
|
@@ -1910,8 +2138,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1910
2138
|
},
|
|
1911
2139
|
overrideAccess: true
|
|
1912
2140
|
});
|
|
1913
|
-
await payload.
|
|
1914
|
-
collection: slugs.tickets,
|
|
2141
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1915
2142
|
id: t.id,
|
|
1916
2143
|
// Clear the armed deadline so a later manual reopen can't be
|
|
1917
2144
|
// closed instantly by a stale timestamp.
|
|
@@ -2034,8 +2261,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2034
2261
|
}
|
|
2035
2262
|
const rawHours = Number(body.hours);
|
|
2036
2263
|
const hours = Number.isFinite(rawHours) ? Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.round(rawHours))) : 24;
|
|
2037
|
-
const ticket = await payload.
|
|
2038
|
-
collection: slugs.tickets,
|
|
2264
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2039
2265
|
id: ticketId,
|
|
2040
2266
|
depth: 1,
|
|
2041
2267
|
overrideAccess: true
|
|
@@ -2057,8 +2283,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2057
2283
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_REPLY_TO || "";
|
|
2058
2284
|
let waitingSince = ticket.firstResponseAt || ticket.createdAt;
|
|
2059
2285
|
try {
|
|
2060
|
-
const lastAdminMsg = await payload.
|
|
2061
|
-
collection: slugs.ticketMessages,
|
|
2286
|
+
const lastAdminMsg = await dbFind(payload, slugs.ticketMessages, {
|
|
2062
2287
|
where: {
|
|
2063
2288
|
and: [
|
|
2064
2289
|
{ ticket: { equals: ticketId } },
|
|
@@ -2099,8 +2324,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2099
2324
|
preheader: `Sans r\xE9ponse de votre part, votre ticket ${ticketNumber} sera cl\xF4tur\xE9 le ${deadlineLabel}.`
|
|
2100
2325
|
})
|
|
2101
2326
|
});
|
|
2102
|
-
await payload.
|
|
2103
|
-
collection: slugs.ticketMessages,
|
|
2327
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2104
2328
|
data: {
|
|
2105
2329
|
ticket: ticketId,
|
|
2106
2330
|
body: `Relance envoy\xE9e \xE0 ${client.email}. Fermeture automatique programm\xE9e le ${deadlineLabel} sans r\xE9ponse du client.`,
|
|
@@ -2110,8 +2334,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2110
2334
|
},
|
|
2111
2335
|
overrideAccess: true
|
|
2112
2336
|
});
|
|
2113
|
-
await payload.
|
|
2114
|
-
collection: slugs.tickets,
|
|
2337
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2115
2338
|
id: ticketId,
|
|
2116
2339
|
data: {
|
|
2117
2340
|
status: "waiting_client",
|
|
@@ -2146,8 +2369,7 @@ function createStatusesEndpoint(slugs) {
|
|
|
2146
2369
|
if (!req.user) {
|
|
2147
2370
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2148
2371
|
}
|
|
2149
|
-
const { docs } = await payload.
|
|
2150
|
-
collection: slugs.ticketStatuses,
|
|
2372
|
+
const { docs } = await dbFind(payload, slugs.ticketStatuses, {
|
|
2151
2373
|
sort: "sortOrder",
|
|
2152
2374
|
limit: 100,
|
|
2153
2375
|
depth: 0,
|
|
@@ -2186,16 +2408,14 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2186
2408
|
if (!macroId || !ticketId) {
|
|
2187
2409
|
return Response.json({ error: "macroId and ticketId are required" }, { status: 400 });
|
|
2188
2410
|
}
|
|
2189
|
-
const macro = await payload.
|
|
2190
|
-
collection: slugs.macros,
|
|
2411
|
+
const macro = await dbFindByID(payload, slugs.macros, {
|
|
2191
2412
|
id: macroId,
|
|
2192
2413
|
depth: 0,
|
|
2193
2414
|
overrideAccess: true
|
|
2194
2415
|
});
|
|
2195
2416
|
if (!macro) return Response.json({ error: "Macro not found" }, { status: 404 });
|
|
2196
2417
|
if (!macro.isActive) return Response.json({ error: "Macro is disabled" }, { status: 400 });
|
|
2197
|
-
const ticket = await payload.
|
|
2198
|
-
collection: slugs.tickets,
|
|
2418
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2199
2419
|
id: ticketId,
|
|
2200
2420
|
depth: 0,
|
|
2201
2421
|
overrideAccess: true
|
|
@@ -2207,8 +2427,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2207
2427
|
try {
|
|
2208
2428
|
switch (action.type) {
|
|
2209
2429
|
case "set_status":
|
|
2210
|
-
await payload.
|
|
2211
|
-
collection: slugs.tickets,
|
|
2430
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2212
2431
|
id: ticketId,
|
|
2213
2432
|
data: { status: action.value },
|
|
2214
2433
|
overrideAccess: true
|
|
@@ -2216,8 +2435,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2216
2435
|
appliedActions.push({ type: action.type, value: action.value, success: true });
|
|
2217
2436
|
break;
|
|
2218
2437
|
case "set_priority":
|
|
2219
|
-
await payload.
|
|
2220
|
-
collection: slugs.tickets,
|
|
2438
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2221
2439
|
id: ticketId,
|
|
2222
2440
|
data: { priority: action.value },
|
|
2223
2441
|
overrideAccess: true
|
|
@@ -2229,8 +2447,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2229
2447
|
if (!currentTags.includes(action.value)) {
|
|
2230
2448
|
currentTags.push(action.value);
|
|
2231
2449
|
}
|
|
2232
|
-
await payload.
|
|
2233
|
-
collection: slugs.tickets,
|
|
2450
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2234
2451
|
id: ticketId,
|
|
2235
2452
|
data: { tags: currentTags },
|
|
2236
2453
|
overrideAccess: true
|
|
@@ -2239,8 +2456,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2239
2456
|
break;
|
|
2240
2457
|
}
|
|
2241
2458
|
case "send_reply":
|
|
2242
|
-
await payload.
|
|
2243
|
-
collection: slugs.ticketMessages,
|
|
2459
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2244
2460
|
data: {
|
|
2245
2461
|
ticket: ticketId,
|
|
2246
2462
|
body: action.value,
|
|
@@ -2257,8 +2473,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2257
2473
|
appliedActions.push({ type: action.type, value: action.value, success: false, error: "Invalid user ID" });
|
|
2258
2474
|
break;
|
|
2259
2475
|
}
|
|
2260
|
-
await payload.
|
|
2261
|
-
collection: slugs.tickets,
|
|
2476
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2262
2477
|
id: ticketId,
|
|
2263
2478
|
data: { assignedTo: userId },
|
|
2264
2479
|
overrideAccess: true
|
|
@@ -2310,8 +2525,7 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2310
2525
|
return Response.json({ error: "Invalid collection. Use email-logs or auth-logs." }, { status: 400 });
|
|
2311
2526
|
}
|
|
2312
2527
|
const cutoff = days > 0 ? new Date(Date.now() - days * 864e5).toISOString() : null;
|
|
2313
|
-
const result = await payload
|
|
2314
|
-
collection: allowedCollections[collection],
|
|
2528
|
+
const result = await dbDelete(payload, allowedCollections[collection], {
|
|
2315
2529
|
where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
|
|
2316
2530
|
overrideAccess: true
|
|
2317
2531
|
});
|
|
@@ -2354,8 +2568,7 @@ function createChatbotEndpoint(slugs) {
|
|
|
2354
2568
|
return Response.json({ error: "Question too short" }, { status: 400 });
|
|
2355
2569
|
}
|
|
2356
2570
|
const payload = req.payload;
|
|
2357
|
-
const articles = await payload.
|
|
2358
|
-
collection: slugs.knowledgeBase,
|
|
2571
|
+
const articles = await dbFind(payload, slugs.knowledgeBase, {
|
|
2359
2572
|
where: { published: { equals: true } },
|
|
2360
2573
|
limit: 100,
|
|
2361
2574
|
depth: 0,
|
|
@@ -2445,8 +2658,7 @@ function createChatGetEndpoint(slugs) {
|
|
|
2445
2658
|
if (after) {
|
|
2446
2659
|
where.createdAt = { greater_than: after };
|
|
2447
2660
|
}
|
|
2448
|
-
const messages = await payload.
|
|
2449
|
-
collection: slugs.chatMessages,
|
|
2661
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2450
2662
|
where,
|
|
2451
2663
|
sort: "createdAt",
|
|
2452
2664
|
limit: 100,
|
|
@@ -2489,8 +2701,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2489
2701
|
return Response.json({ error: "Trop de sessions cr\xE9\xE9es. R\xE9essayez plus tard." }, { status: 429 });
|
|
2490
2702
|
}
|
|
2491
2703
|
const sessionId = `chat_${crypto3.randomBytes(16).toString("hex")}`;
|
|
2492
|
-
const systemMsg = await payload.
|
|
2493
|
-
collection: slugs.chatMessages,
|
|
2704
|
+
const systemMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2494
2705
|
data: {
|
|
2495
2706
|
session: sessionId,
|
|
2496
2707
|
client: req.user.id,
|
|
@@ -2510,8 +2721,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2510
2721
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2511
2722
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2512
2723
|
}
|
|
2513
|
-
const existing = await payload.
|
|
2514
|
-
collection: slugs.chatMessages,
|
|
2724
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2515
2725
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2516
2726
|
limit: 1,
|
|
2517
2727
|
overrideAccess: true
|
|
@@ -2519,8 +2729,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2519
2729
|
if (existing.docs.length === 0) {
|
|
2520
2730
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2521
2731
|
}
|
|
2522
|
-
const newMsg = await payload.
|
|
2523
|
-
collection: slugs.chatMessages,
|
|
2732
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2524
2733
|
data: {
|
|
2525
2734
|
session,
|
|
2526
2735
|
client: req.user.id,
|
|
@@ -2533,8 +2742,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2533
2742
|
return Response.json({ message: newMsg });
|
|
2534
2743
|
}
|
|
2535
2744
|
if (action === "close" && session) {
|
|
2536
|
-
const existing = await payload.
|
|
2537
|
-
collection: slugs.chatMessages,
|
|
2745
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2538
2746
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2539
2747
|
limit: 1,
|
|
2540
2748
|
overrideAccess: true
|
|
@@ -2542,8 +2750,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2542
2750
|
if (existing.docs.length === 0) {
|
|
2543
2751
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2544
2752
|
}
|
|
2545
|
-
await payload.
|
|
2546
|
-
collection: slugs.chatMessages,
|
|
2753
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2547
2754
|
data: {
|
|
2548
2755
|
session,
|
|
2549
2756
|
client: req.user.id,
|
|
@@ -2585,8 +2792,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2585
2792
|
if (!sessionId) {
|
|
2586
2793
|
return Response.json({ error: "Missing session parameter" }, { status: 400 });
|
|
2587
2794
|
}
|
|
2588
|
-
const existing = await req.payload.
|
|
2589
|
-
collection: slugs.chatMessages,
|
|
2795
|
+
const existing = await dbFind(req.payload, slugs.chatMessages, {
|
|
2590
2796
|
where: {
|
|
2591
2797
|
session: { equals: sessionId },
|
|
2592
2798
|
client: { equals: userId }
|
|
@@ -2608,8 +2814,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2608
2814
|
);
|
|
2609
2815
|
const interval = setInterval(async () => {
|
|
2610
2816
|
try {
|
|
2611
|
-
const messages = await req.payload.
|
|
2612
|
-
collection: slugs.chatMessages,
|
|
2817
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2613
2818
|
where: {
|
|
2614
2819
|
session: { equals: sessionId },
|
|
2615
2820
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2690,8 +2895,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2690
2895
|
if (session) {
|
|
2691
2896
|
const where = { session: { equals: session } };
|
|
2692
2897
|
if (after) where.createdAt = { greater_than: after };
|
|
2693
|
-
const messages = await payload.
|
|
2694
|
-
collection: slugs.chatMessages,
|
|
2898
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2695
2899
|
where,
|
|
2696
2900
|
sort: "createdAt",
|
|
2697
2901
|
limit: 200,
|
|
@@ -2705,8 +2909,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2705
2909
|
}
|
|
2706
2910
|
});
|
|
2707
2911
|
}
|
|
2708
|
-
const recentMessages = await payload.
|
|
2709
|
-
collection: slugs.chatMessages,
|
|
2912
|
+
const recentMessages = await dbFind(payload, slugs.chatMessages, {
|
|
2710
2913
|
sort: "-createdAt",
|
|
2711
2914
|
limit: 50,
|
|
2712
2915
|
depth: 1,
|
|
@@ -2769,8 +2972,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2769
2972
|
if (!session) {
|
|
2770
2973
|
return Response.json({ error: "Session requise" }, { status: 400 });
|
|
2771
2974
|
}
|
|
2772
|
-
const sessionMsg = await payload.
|
|
2773
|
-
collection: slugs.chatMessages,
|
|
2975
|
+
const sessionMsg = await dbFind(payload, slugs.chatMessages, {
|
|
2774
2976
|
where: { session: { equals: session } },
|
|
2775
2977
|
limit: 1,
|
|
2776
2978
|
depth: 0,
|
|
@@ -2788,8 +2990,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2788
2990
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2789
2991
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2790
2992
|
}
|
|
2791
|
-
const newMsg = await payload.
|
|
2792
|
-
collection: slugs.chatMessages,
|
|
2993
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2793
2994
|
data: {
|
|
2794
2995
|
session,
|
|
2795
2996
|
client: clientId,
|
|
@@ -2801,8 +3002,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2801
3002
|
overrideAccess: true
|
|
2802
3003
|
});
|
|
2803
3004
|
try {
|
|
2804
|
-
const linkedTicket = await payload.
|
|
2805
|
-
collection: slugs.tickets,
|
|
3005
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2806
3006
|
where: { chatSession: { equals: session } },
|
|
2807
3007
|
limit: 1,
|
|
2808
3008
|
depth: 0,
|
|
@@ -2810,8 +3010,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2810
3010
|
});
|
|
2811
3011
|
if (linkedTicket.docs.length > 0) {
|
|
2812
3012
|
const ticketId = linkedTicket.docs[0].id;
|
|
2813
|
-
await payload.
|
|
2814
|
-
collection: slugs.ticketMessages,
|
|
3013
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2815
3014
|
data: {
|
|
2816
3015
|
ticket: ticketId,
|
|
2817
3016
|
body: trimmedMessage,
|
|
@@ -2820,8 +3019,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2820
3019
|
},
|
|
2821
3020
|
overrideAccess: true
|
|
2822
3021
|
});
|
|
2823
|
-
await payload.
|
|
2824
|
-
collection: slugs.chatMessages,
|
|
3022
|
+
await dbUpdate(payload, slugs.chatMessages, {
|
|
2825
3023
|
id: newMsg.id,
|
|
2826
3024
|
data: { ticket: ticketId },
|
|
2827
3025
|
overrideAccess: true
|
|
@@ -2833,8 +3031,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2833
3031
|
return Response.json({ message: newMsg });
|
|
2834
3032
|
}
|
|
2835
3033
|
if (action === "close") {
|
|
2836
|
-
await payload.
|
|
2837
|
-
collection: slugs.chatMessages,
|
|
3034
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2838
3035
|
data: {
|
|
2839
3036
|
session,
|
|
2840
3037
|
client: clientId,
|
|
@@ -2845,16 +3042,14 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2845
3042
|
overrideAccess: true
|
|
2846
3043
|
});
|
|
2847
3044
|
try {
|
|
2848
|
-
const linkedTicket = await payload.
|
|
2849
|
-
collection: slugs.tickets,
|
|
3045
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2850
3046
|
where: { chatSession: { equals: session } },
|
|
2851
3047
|
limit: 1,
|
|
2852
3048
|
depth: 0,
|
|
2853
3049
|
overrideAccess: true
|
|
2854
3050
|
});
|
|
2855
3051
|
if (linkedTicket.docs.length > 0) {
|
|
2856
|
-
await payload.
|
|
2857
|
-
collection: slugs.ticketMessages,
|
|
3052
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2858
3053
|
data: {
|
|
2859
3054
|
ticket: linkedTicket.docs[0].id,
|
|
2860
3055
|
body: "Session de chat termin\xE9e par un agent.",
|
|
@@ -2917,8 +3112,7 @@ function createSessionStream(req, slugs, sessionId) {
|
|
|
2917
3112
|
);
|
|
2918
3113
|
const interval = setInterval(async () => {
|
|
2919
3114
|
try {
|
|
2920
|
-
const messages = await req.payload.
|
|
2921
|
-
collection: slugs.chatMessages,
|
|
3115
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2922
3116
|
where: {
|
|
2923
3117
|
session: { equals: sessionId },
|
|
2924
3118
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2991,8 +3185,7 @@ function createSessionListStream(req, slugs) {
|
|
|
2991
3185
|
);
|
|
2992
3186
|
const interval = setInterval(async () => {
|
|
2993
3187
|
try {
|
|
2994
|
-
const recentMessages = await req.payload.
|
|
2995
|
-
collection: slugs.chatMessages,
|
|
3188
|
+
const recentMessages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2996
3189
|
sort: "-createdAt",
|
|
2997
3190
|
limit: 50,
|
|
2998
3191
|
depth: 1,
|
|
@@ -3076,12 +3269,13 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3076
3269
|
try {
|
|
3077
3270
|
const payload = req.payload;
|
|
3078
3271
|
requireAdmin(req, slugs);
|
|
3272
|
+
const teamId = new URL(req.url).searchParams.get("teamId");
|
|
3273
|
+
const teamAnd = teamId ? [{ team: { equals: Number(teamId) } }] : [];
|
|
3079
3274
|
const statuses = ["open", "waiting_client", "resolved"];
|
|
3080
3275
|
const statusCounts = await Promise.all(
|
|
3081
3276
|
statuses.map(async (status) => {
|
|
3082
|
-
const result = await payload.
|
|
3083
|
-
|
|
3084
|
-
where: { status: { equals: status } },
|
|
3277
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3278
|
+
where: { and: [{ status: { equals: status } }, ...teamAnd] },
|
|
3085
3279
|
overrideAccess: true
|
|
3086
3280
|
});
|
|
3087
3281
|
return [status, result.totalDocs];
|
|
@@ -3092,9 +3286,8 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3092
3286
|
const priorities = ["low", "normal", "high", "urgent"];
|
|
3093
3287
|
const priorityCounts = await Promise.all(
|
|
3094
3288
|
priorities.map(async (priority) => {
|
|
3095
|
-
const result = await payload.
|
|
3096
|
-
|
|
3097
|
-
where: { priority: { equals: priority } },
|
|
3289
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3290
|
+
where: { and: [{ priority: { equals: priority } }, ...teamAnd] },
|
|
3098
3291
|
overrideAccess: true
|
|
3099
3292
|
});
|
|
3100
3293
|
return [priority, result.totalDocs];
|
|
@@ -3104,9 +3297,8 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3104
3297
|
const categories = ["bug", "content", "feature", "question", "hosting"];
|
|
3105
3298
|
const categoryCounts = await Promise.all(
|
|
3106
3299
|
categories.map(async (category) => {
|
|
3107
|
-
const result = await payload.
|
|
3108
|
-
|
|
3109
|
-
where: { category: { equals: category } },
|
|
3300
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3301
|
+
where: { and: [{ category: { equals: category } }, ...teamAnd] },
|
|
3110
3302
|
overrideAccess: true
|
|
3111
3303
|
});
|
|
3112
3304
|
return [category, result.totalDocs];
|
|
@@ -3119,17 +3311,34 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3119
3311
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
|
|
3120
3312
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
3121
3313
|
const [created7, created30] = await Promise.all([
|
|
3122
|
-
payload.
|
|
3123
|
-
collection: slugs.tickets,
|
|
3314
|
+
dbCount(payload, slugs.tickets, {
|
|
3124
3315
|
where: { createdAt: { greater_than_equal: sevenDaysAgo.toISOString() } },
|
|
3125
3316
|
overrideAccess: true
|
|
3126
3317
|
}),
|
|
3127
|
-
payload.
|
|
3128
|
-
collection: slugs.tickets,
|
|
3318
|
+
dbCount(payload, slugs.tickets, {
|
|
3129
3319
|
where: { createdAt: { greater_than_equal: thirtyDaysAgo.toISOString() } },
|
|
3130
3320
|
overrideAccess: true
|
|
3131
3321
|
})
|
|
3132
3322
|
]);
|
|
3323
|
+
const volumeByDay = await Promise.all(
|
|
3324
|
+
Array.from({ length: 7 }, (_, i) => i).map(async (offset) => {
|
|
3325
|
+
const dayStart = new Date(now);
|
|
3326
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
3327
|
+
dayStart.setDate(dayStart.getDate() - (6 - offset));
|
|
3328
|
+
const dayEnd = new Date(dayStart);
|
|
3329
|
+
dayEnd.setDate(dayEnd.getDate() + 1);
|
|
3330
|
+
const r = await dbCount(payload, slugs.tickets, {
|
|
3331
|
+
where: {
|
|
3332
|
+
and: [
|
|
3333
|
+
{ createdAt: { greater_than_equal: dayStart.toISOString() } },
|
|
3334
|
+
{ createdAt: { less_than: dayEnd.toISOString() } }
|
|
3335
|
+
]
|
|
3336
|
+
},
|
|
3337
|
+
overrideAccess: true
|
|
3338
|
+
});
|
|
3339
|
+
return { date: dayStart.toISOString(), count: r.totalDocs };
|
|
3340
|
+
})
|
|
3341
|
+
);
|
|
3133
3342
|
const PAGE_SIZE2 = 100;
|
|
3134
3343
|
const MAX_PAGES2 = 50;
|
|
3135
3344
|
let totalResponseTimeMs = 0;
|
|
@@ -3140,8 +3349,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3140
3349
|
let page = 1;
|
|
3141
3350
|
let hasMore = true;
|
|
3142
3351
|
while (hasMore && page <= MAX_PAGES2) {
|
|
3143
|
-
const batch = await payload.
|
|
3144
|
-
collection: slugs.tickets,
|
|
3352
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3145
3353
|
limit: PAGE_SIZE2,
|
|
3146
3354
|
page,
|
|
3147
3355
|
depth: 0,
|
|
@@ -3174,43 +3382,62 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3174
3382
|
hasMore = batch.hasNextPage ?? false;
|
|
3175
3383
|
page++;
|
|
3176
3384
|
}
|
|
3177
|
-
const surveyCount = await payload.
|
|
3178
|
-
collection: slugs.satisfactionSurveys,
|
|
3385
|
+
const surveyCount = await dbCount(payload, slugs.satisfactionSurveys, {
|
|
3179
3386
|
overrideAccess: true
|
|
3180
3387
|
});
|
|
3181
3388
|
let satisfactionAvg = 0;
|
|
3389
|
+
let satisfactionCount = 0;
|
|
3390
|
+
let npsScore = null;
|
|
3391
|
+
let npsCount = 0;
|
|
3182
3392
|
if (surveyCount.totalDocs > 0) {
|
|
3183
|
-
let
|
|
3393
|
+
let ratingSum = 0;
|
|
3394
|
+
let ratingN = 0;
|
|
3395
|
+
let promoters = 0;
|
|
3396
|
+
let detractors = 0;
|
|
3397
|
+
let npsN = 0;
|
|
3184
3398
|
let surveyPage = 1;
|
|
3185
3399
|
let surveyHasMore = true;
|
|
3186
3400
|
while (surveyHasMore && surveyPage <= MAX_PAGES2) {
|
|
3187
|
-
const batch = await payload.
|
|
3188
|
-
collection: slugs.satisfactionSurveys,
|
|
3401
|
+
const batch = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
3189
3402
|
limit: PAGE_SIZE2,
|
|
3190
3403
|
page: surveyPage,
|
|
3191
3404
|
depth: 0,
|
|
3192
3405
|
overrideAccess: true,
|
|
3193
|
-
select: { rating: true }
|
|
3406
|
+
select: { rating: true, nps: true }
|
|
3194
3407
|
});
|
|
3195
3408
|
for (const s of batch.docs) {
|
|
3196
|
-
|
|
3409
|
+
const r = s.rating;
|
|
3410
|
+
if (typeof r === "number" && r > 0) {
|
|
3411
|
+
ratingSum += r;
|
|
3412
|
+
ratingN++;
|
|
3413
|
+
}
|
|
3414
|
+
const n = s.nps;
|
|
3415
|
+
if (typeof n === "number") {
|
|
3416
|
+
npsN++;
|
|
3417
|
+
if (n >= 9) promoters++;
|
|
3418
|
+
else if (n <= 6) detractors++;
|
|
3419
|
+
}
|
|
3197
3420
|
}
|
|
3198
3421
|
surveyHasMore = batch.hasNextPage ?? false;
|
|
3199
3422
|
surveyPage++;
|
|
3200
3423
|
}
|
|
3201
|
-
satisfactionAvg = Math.round(
|
|
3424
|
+
satisfactionAvg = ratingN > 0 ? Math.round(ratingSum / ratingN * 10) / 10 : 0;
|
|
3425
|
+
satisfactionCount = ratingN;
|
|
3426
|
+
if (npsN > 0) {
|
|
3427
|
+
npsScore = Math.round((promoters - detractors) / npsN * 100);
|
|
3428
|
+
npsCount = npsN;
|
|
3429
|
+
}
|
|
3202
3430
|
}
|
|
3203
|
-
const
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
}),
|
|
3208
|
-
payload.count({
|
|
3209
|
-
collection: slugs.pendingEmails,
|
|
3431
|
+
const clientCount = await dbCount(payload, slugs.supportClients, { overrideAccess: true });
|
|
3432
|
+
let pendingEmailsTotal = 0;
|
|
3433
|
+
try {
|
|
3434
|
+
const pe = await dbCount(payload, slugs.pendingEmails, {
|
|
3210
3435
|
where: { status: { equals: "pending" } },
|
|
3211
3436
|
overrideAccess: true
|
|
3212
|
-
})
|
|
3213
|
-
|
|
3437
|
+
});
|
|
3438
|
+
pendingEmailsTotal = pe.totalDocs;
|
|
3439
|
+
} catch {
|
|
3440
|
+
}
|
|
3214
3441
|
return new Response(JSON.stringify({
|
|
3215
3442
|
total,
|
|
3216
3443
|
byStatus,
|
|
@@ -3218,13 +3445,16 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3218
3445
|
byCategory,
|
|
3219
3446
|
createdLast7Days: created7.totalDocs,
|
|
3220
3447
|
createdLast30Days: created30.totalDocs,
|
|
3448
|
+
volumeByDay,
|
|
3221
3449
|
avgResponseTimeHours: responseTimeCount > 0 ? Math.round(totalResponseTimeMs / responseTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3222
3450
|
avgResolutionTimeHours: resolutionTimeCount > 0 ? Math.round(totalResolutionTimeMs / resolutionTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3223
3451
|
totalTimeMinutes,
|
|
3224
3452
|
satisfactionAvg,
|
|
3225
|
-
satisfactionCount
|
|
3453
|
+
satisfactionCount,
|
|
3454
|
+
npsScore,
|
|
3455
|
+
npsCount,
|
|
3226
3456
|
clientCount: clientCount.totalDocs,
|
|
3227
|
-
pendingEmailsCount:
|
|
3457
|
+
pendingEmailsCount: pendingEmailsTotal
|
|
3228
3458
|
}), {
|
|
3229
3459
|
headers: {
|
|
3230
3460
|
"Content-Type": "application/json",
|
|
@@ -3294,13 +3524,26 @@ function createBillingEndpoint(slugs) {
|
|
|
3294
3524
|
let ticketPage = 1;
|
|
3295
3525
|
let ticketHasMore = true;
|
|
3296
3526
|
while (ticketHasMore && ticketPage <= MAX_PAGES) {
|
|
3297
|
-
const batch = await payload.
|
|
3298
|
-
collection: slugs.tickets,
|
|
3527
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3299
3528
|
where: ticketWhere,
|
|
3300
3529
|
limit: PAGE_SIZE,
|
|
3301
3530
|
page: ticketPage,
|
|
3531
|
+
// depth 2 is required: we read ticket.project.client.company below.
|
|
3302
3532
|
depth: 2,
|
|
3303
|
-
overrideAccess: true
|
|
3533
|
+
overrideAccess: true,
|
|
3534
|
+
// Bound the payload to only the fields the report consumes (skips body,
|
|
3535
|
+
// richText, SLA fields, etc.) while keeping project/client populated.
|
|
3536
|
+
select: {
|
|
3537
|
+
ticketNumber: true,
|
|
3538
|
+
subject: true,
|
|
3539
|
+
status: true,
|
|
3540
|
+
billedAmount: true,
|
|
3541
|
+
aiSummary: true,
|
|
3542
|
+
aiSummaryGeneratedAt: true,
|
|
3543
|
+
aiSummaryStatus: true,
|
|
3544
|
+
project: true,
|
|
3545
|
+
client: true
|
|
3546
|
+
}
|
|
3304
3547
|
});
|
|
3305
3548
|
allTickets.push(...batch.docs);
|
|
3306
3549
|
ticketHasMore = batch.hasNextPage ?? false;
|
|
@@ -3310,8 +3553,7 @@ function createBillingEndpoint(slugs) {
|
|
|
3310
3553
|
let entryPage = 1;
|
|
3311
3554
|
let entryHasMore = true;
|
|
3312
3555
|
while (entryHasMore && entryPage <= MAX_PAGES) {
|
|
3313
|
-
const batch = await payload.
|
|
3314
|
-
collection: slugs.timeEntries,
|
|
3556
|
+
const batch = await dbFind(payload, slugs.timeEntries, {
|
|
3315
3557
|
where: {
|
|
3316
3558
|
and: [
|
|
3317
3559
|
{ date: { greater_than_equal: from } },
|
|
@@ -3409,43 +3651,205 @@ function createBillingEndpoint(slugs) {
|
|
|
3409
3651
|
}
|
|
3410
3652
|
};
|
|
3411
3653
|
}
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
function
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3654
|
+
var fmtAmount = (n) => `${n.toFixed(2)} EUR`;
|
|
3655
|
+
var fmtTime = (min) => `${Math.floor(min / 60)}h${String(min % 60).padStart(2, "0")}`;
|
|
3656
|
+
function generateInvoicePdf(data) {
|
|
3657
|
+
return new Promise((resolve, reject) => {
|
|
3658
|
+
try {
|
|
3659
|
+
const doc = new PDFDocument({ size: "A4", margin: 50 });
|
|
3660
|
+
const chunks = [];
|
|
3661
|
+
doc.on("data", (c) => chunks.push(c));
|
|
3662
|
+
doc.on("end", () => resolve(Buffer.concat(chunks)));
|
|
3663
|
+
doc.on("error", reject);
|
|
3664
|
+
doc.fillColor("#0f172a").fontSize(22).text("Invoice / Pre-billing");
|
|
3665
|
+
doc.moveDown(0.2);
|
|
3666
|
+
doc.fillColor("#64748b").fontSize(10).text(`Period: ${data.from} -> ${data.to}${data.projectId ? ` \xB7 Project #${data.projectId}` : ""}`);
|
|
3667
|
+
doc.moveDown(1);
|
|
3668
|
+
const X = { ticket: 50, subject: 130, client: 320, time: 430, amount: 500 };
|
|
3669
|
+
const drawRow = (r, opts) => {
|
|
3670
|
+
const y = doc.y;
|
|
3671
|
+
doc.fontSize(opts?.header ? 9 : 10).fillColor(opts?.header ? "#475569" : "#0f172a");
|
|
3672
|
+
if (opts?.total) doc.font("Helvetica-Bold");
|
|
3673
|
+
doc.text(r.ticket, X.ticket, y, { width: X.subject - X.ticket - 6 });
|
|
3674
|
+
doc.text(r.subject, X.subject, y, { width: X.client - X.subject - 6 });
|
|
3675
|
+
doc.text(r.client, X.client, y, { width: X.time - X.client - 6 });
|
|
3676
|
+
doc.text(r.time, X.time, y, { width: X.amount - X.time - 6, align: "right" });
|
|
3677
|
+
doc.text(r.amount, X.amount, y, { width: 545 - X.amount, align: "right" });
|
|
3678
|
+
doc.font("Helvetica");
|
|
3679
|
+
doc.moveDown(0.4);
|
|
3680
|
+
};
|
|
3681
|
+
drawRow({ ticket: "Ticket", subject: "Subject", client: "Client", time: "Time", amount: "Amount" }, { header: true });
|
|
3682
|
+
doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor("#e2e8f0").stroke();
|
|
3683
|
+
doc.moveDown(0.3);
|
|
3684
|
+
for (const l of data.lines) {
|
|
3685
|
+
if (doc.y > 760) doc.addPage();
|
|
3686
|
+
drawRow({
|
|
3687
|
+
ticket: l.ticketNumber || "",
|
|
3688
|
+
subject: l.subject || "",
|
|
3689
|
+
client: l.client || "",
|
|
3690
|
+
time: fmtTime(l.minutes),
|
|
3691
|
+
amount: fmtAmount(l.amount)
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor("#0f172a").stroke();
|
|
3695
|
+
doc.moveDown(0.3);
|
|
3696
|
+
drawRow({
|
|
3697
|
+
ticket: "",
|
|
3698
|
+
subject: `Total (${data.lines.length} ticket${data.lines.length > 1 ? "s" : ""})`,
|
|
3699
|
+
client: "",
|
|
3700
|
+
time: fmtTime(data.totalMinutes),
|
|
3701
|
+
amount: fmtAmount(data.totalAmount)
|
|
3702
|
+
}, { total: true });
|
|
3703
|
+
doc.end();
|
|
3704
|
+
} catch (err) {
|
|
3705
|
+
reject(err);
|
|
3706
|
+
}
|
|
3436
3707
|
});
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
// src/endpoints/invoice.ts
|
|
3711
|
+
function createInvoiceEndpoint(slugs) {
|
|
3712
|
+
return {
|
|
3713
|
+
path: "/support/billing/invoice",
|
|
3714
|
+
method: "get",
|
|
3715
|
+
handler: async (req) => {
|
|
3716
|
+
try {
|
|
3717
|
+
requireAdmin(req, slugs);
|
|
3718
|
+
const payload = req.payload;
|
|
3719
|
+
const url = new URL(req.url);
|
|
3720
|
+
const from = url.searchParams.get("from");
|
|
3721
|
+
const to = url.searchParams.get("to");
|
|
3722
|
+
const projectId = url.searchParams.get("projectId");
|
|
3723
|
+
const format = url.searchParams.get("format") === "pdf" ? "pdf" : "html";
|
|
3724
|
+
if (!from || !to) {
|
|
3725
|
+
return Response.json({ error: "Param\xE8tres from et to requis." }, { status: 400 });
|
|
3726
|
+
}
|
|
3727
|
+
const toExclusive = new Date(to);
|
|
3728
|
+
toExclusive.setDate(toExclusive.getDate() + 1);
|
|
3729
|
+
const toIso = toExclusive.toISOString();
|
|
3730
|
+
const where = {
|
|
3731
|
+
and: [
|
|
3732
|
+
{ billable: { equals: true } },
|
|
3733
|
+
...projectId ? [{ project: { equals: Number(projectId) } }] : [],
|
|
3734
|
+
{
|
|
3735
|
+
or: [
|
|
3736
|
+
{ and: [{ updatedAt: { greater_than_equal: from } }, { updatedAt: { less_than: toIso } }] },
|
|
3737
|
+
{ and: [{ createdAt: { greater_than_equal: from } }, { createdAt: { less_than: toIso } }] },
|
|
3738
|
+
{ and: [{ resolvedAt: { greater_than_equal: from } }, { resolvedAt: { less_than: toIso } }] }
|
|
3739
|
+
]
|
|
3740
|
+
}
|
|
3741
|
+
]
|
|
3742
|
+
};
|
|
3743
|
+
const tickets = [];
|
|
3744
|
+
let page = 1;
|
|
3745
|
+
let hasMore = true;
|
|
3746
|
+
while (hasMore && page <= 50) {
|
|
3747
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3748
|
+
where,
|
|
3749
|
+
limit: 500,
|
|
3750
|
+
page,
|
|
3751
|
+
depth: 2,
|
|
3752
|
+
overrideAccess: true,
|
|
3753
|
+
select: { ticketNumber: true, subject: true, billedAmount: true, totalTimeMinutes: true, project: true, client: true }
|
|
3754
|
+
});
|
|
3755
|
+
tickets.push(...batch.docs);
|
|
3756
|
+
hasMore = batch.hasNextPage ?? false;
|
|
3757
|
+
page++;
|
|
3758
|
+
}
|
|
3759
|
+
let totalAmount = 0;
|
|
3760
|
+
let totalMinutes = 0;
|
|
3761
|
+
const lines = tickets.map((t) => {
|
|
3762
|
+
const amount = typeof t.billedAmount === "number" ? t.billedAmount : 0;
|
|
3763
|
+
const minutes = typeof t.totalTimeMinutes === "number" ? t.totalTimeMinutes : 0;
|
|
3764
|
+
totalAmount += amount;
|
|
3765
|
+
totalMinutes += minutes;
|
|
3766
|
+
const client = typeof t.client === "object" && t.client ? t.client.company || "" : "";
|
|
3767
|
+
return { ticketNumber: String(t.ticketNumber || ""), subject: String(t.subject || ""), client, minutes, amount };
|
|
3768
|
+
});
|
|
3769
|
+
if (format === "pdf") {
|
|
3770
|
+
const pdf = await generateInvoicePdf({ from, to, projectId, lines, totalMinutes, totalAmount });
|
|
3771
|
+
return new Response(new Uint8Array(pdf), {
|
|
3772
|
+
status: 200,
|
|
3773
|
+
headers: {
|
|
3774
|
+
"Content-Type": "application/pdf",
|
|
3775
|
+
"Content-Disposition": `inline; filename="invoice-${from}_${to}.pdf"`
|
|
3776
|
+
}
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
const fmtAmount2 = (n) => `${n.toFixed(2)} \u20AC`;
|
|
3780
|
+
const fmtTime2 = (min) => `${Math.floor(min / 60)}h${String(min % 60).padStart(2, "0")}`;
|
|
3781
|
+
const rows = lines.map((l) => `<tr>
|
|
3782
|
+
<td>${escapeHtml(l.ticketNumber)}</td>
|
|
3783
|
+
<td>${escapeHtml(l.subject)}</td>
|
|
3784
|
+
<td>${escapeHtml(l.client)}</td>
|
|
3785
|
+
<td style="text-align:right;">${fmtTime2(l.minutes)}</td>
|
|
3786
|
+
<td style="text-align:right;">${fmtAmount2(l.amount)}</td>
|
|
3787
|
+
</tr>`).join("");
|
|
3788
|
+
const html = `<!doctype html>
|
|
3789
|
+
<html lang="fr"><head><meta charset="utf-8"><title>Facture ${escapeHtml(from)} \u2192 ${escapeHtml(to)}</title>
|
|
3790
|
+
<style>
|
|
3791
|
+
body { font-family: system-ui, -apple-system, sans-serif; color: #1e293b; max-width: 820px; margin: 32px auto; padding: 0 24px; }
|
|
3792
|
+
h1 { font-size: 22px; margin: 0 0 4px; }
|
|
3793
|
+
.period { color: #64748b; margin-bottom: 24px; }
|
|
3794
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
3795
|
+
th, td { padding: 8px 10px; border-bottom: 1px solid #e2e8f0; text-align: left; }
|
|
3796
|
+
th { background: #f8fafc; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: #475569; }
|
|
3797
|
+
tfoot td { font-weight: 700; border-top: 2px solid #0f172a; border-bottom: none; }
|
|
3798
|
+
@media print { body { margin: 0; } }
|
|
3799
|
+
</style></head>
|
|
3800
|
+
<body>
|
|
3801
|
+
<h1>Facture / Pr\xE9-facture</h1>
|
|
3802
|
+
<div class="period">P\xE9riode : ${escapeHtml(from)} \u2192 ${escapeHtml(to)}${projectId ? ` \xB7 Projet #${escapeHtml(projectId)}` : ""}</div>
|
|
3803
|
+
<table>
|
|
3804
|
+
<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>
|
|
3805
|
+
<tbody>${rows || '<tr><td colspan="5" style="color:#94a3b8;">Aucun ticket facturable sur la p\xE9riode.</td></tr>'}</tbody>
|
|
3806
|
+
<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>
|
|
3807
|
+
</table>
|
|
3808
|
+
</body></html>`;
|
|
3809
|
+
return new Response(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
3810
|
+
} catch (error) {
|
|
3811
|
+
const authResponse = handleAuthError(error);
|
|
3812
|
+
if (authResponse) return authResponse;
|
|
3813
|
+
console.error("[invoice] Error:", error);
|
|
3814
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
// src/utils/generateTicketSynthesis.ts
|
|
3821
|
+
function getClient4(aiSettings) {
|
|
3822
|
+
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
3823
|
+
if (aiSettings.provider === "ollama") {
|
|
3824
|
+
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
3825
|
+
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
3826
|
+
}
|
|
3827
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
3828
|
+
}
|
|
3829
|
+
function getModel3(aiSettings) {
|
|
3830
|
+
return aiSettings.model || "claude-haiku-4-5-20251001";
|
|
3831
|
+
}
|
|
3832
|
+
async function generateTicketSynthesis(args) {
|
|
3833
|
+
const { payload, slugs, ticketId } = args;
|
|
3834
|
+
const settings = await readSupportSettings(payload);
|
|
3835
|
+
if (settings.ai.enableSynthesis === false) {
|
|
3836
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "skipped", reason: "synthesis disabled" };
|
|
3837
|
+
}
|
|
3838
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
3839
|
+
id: ticketId,
|
|
3840
|
+
depth: 1,
|
|
3841
|
+
overrideAccess: true
|
|
3842
|
+
});
|
|
3843
|
+
if (!ticket) {
|
|
3844
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "error", reason: "ticket not found" };
|
|
3845
|
+
}
|
|
3846
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3847
|
+
id: ticketId,
|
|
3848
|
+
data: { aiSummaryStatus: "pending" },
|
|
3444
3849
|
overrideAccess: true
|
|
3445
3850
|
}).catch(() => {
|
|
3446
3851
|
});
|
|
3447
|
-
const messagesResult = await payload.
|
|
3448
|
-
collection: slugs.ticketMessages,
|
|
3852
|
+
const messagesResult = await dbFind(payload, slugs.ticketMessages, {
|
|
3449
3853
|
where: { ticket: { equals: ticketId } },
|
|
3450
3854
|
sort: "createdAt",
|
|
3451
3855
|
limit: 500,
|
|
@@ -3455,8 +3859,7 @@ async function generateTicketSynthesis(args) {
|
|
|
3455
3859
|
const messages = messagesResult.docs;
|
|
3456
3860
|
if (messages.length === 0) {
|
|
3457
3861
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3458
|
-
await payload.
|
|
3459
|
-
collection: slugs.tickets,
|
|
3862
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3460
3863
|
id: ticketId,
|
|
3461
3864
|
data: {
|
|
3462
3865
|
aiSummary: "(Aucun message dans ce ticket)",
|
|
@@ -3501,7 +3904,7 @@ Regles strictes :
|
|
|
3501
3904
|
- Si le ticket n'a pas abouti, decris quand meme le travail d'analyse realise
|
|
3502
3905
|
|
|
3503
3906
|
Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
3504
|
-
const anthropic =
|
|
3907
|
+
const anthropic = getClient4(settings.ai);
|
|
3505
3908
|
const model = getModel3(settings.ai);
|
|
3506
3909
|
try {
|
|
3507
3910
|
const res = await anthropic.messages.create({
|
|
@@ -3511,8 +3914,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3511
3914
|
});
|
|
3512
3915
|
const summary = res.content[0]?.type === "text" ? res.content[0].text.trim() : "";
|
|
3513
3916
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3514
|
-
await payload.
|
|
3515
|
-
collection: slugs.tickets,
|
|
3917
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3516
3918
|
id: ticketId,
|
|
3517
3919
|
data: {
|
|
3518
3920
|
aiSummary: summary,
|
|
@@ -3524,8 +3926,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3524
3926
|
return { summary, generatedAt, status: "done" };
|
|
3525
3927
|
} catch (err) {
|
|
3526
3928
|
const message = err instanceof Error ? err.message : String(err);
|
|
3527
|
-
await payload.
|
|
3528
|
-
collection: slugs.tickets,
|
|
3929
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3529
3930
|
id: ticketId,
|
|
3530
3931
|
data: { aiSummaryStatus: "error" },
|
|
3531
3932
|
overrideAccess: true
|
|
@@ -3555,8 +3956,7 @@ function createTicketSynthesisEndpoint(slugs) {
|
|
|
3555
3956
|
return Response.json({ error: "ticketId must be a number" }, { status: 400 });
|
|
3556
3957
|
}
|
|
3557
3958
|
if (!force) {
|
|
3558
|
-
const existing = await payload.
|
|
3559
|
-
collection: slugs.tickets,
|
|
3959
|
+
const existing = await dbFindByID(payload, slugs.tickets, {
|
|
3560
3960
|
id: ticketId,
|
|
3561
3961
|
depth: 0,
|
|
3562
3962
|
overrideAccess: true
|
|
@@ -3600,8 +4000,7 @@ function createEmailStatsEndpoint(slugs) {
|
|
|
3600
4000
|
let page = 1;
|
|
3601
4001
|
let hasMore = true;
|
|
3602
4002
|
while (hasMore && page <= MAX_PAGES2) {
|
|
3603
|
-
const result = await payload.
|
|
3604
|
-
collection: slugs.emailLogs,
|
|
4003
|
+
const result = await dbFind(payload, slugs.emailLogs, {
|
|
3605
4004
|
where: { createdAt: { greater_than: cutoff } },
|
|
3606
4005
|
sort: "-createdAt",
|
|
3607
4006
|
limit: 500,
|
|
@@ -3675,12 +4074,20 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3675
4074
|
const payload = req.payload;
|
|
3676
4075
|
requireClient(req, slugs);
|
|
3677
4076
|
const body = await req.json();
|
|
3678
|
-
const { ticketId, rating, comment } = body;
|
|
3679
|
-
if (!ticketId
|
|
3680
|
-
return Response.json(
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
4077
|
+
const { ticketId, rating, nps, comment } = body;
|
|
4078
|
+
if (!ticketId) {
|
|
4079
|
+
return Response.json({ error: "ticketId est requis." }, { status: 400 });
|
|
4080
|
+
}
|
|
4081
|
+
const hasRating = rating !== void 0 && rating !== null;
|
|
4082
|
+
const hasNps = nps !== void 0 && nps !== null;
|
|
4083
|
+
if (!hasRating && !hasNps) {
|
|
4084
|
+
return Response.json({ error: "Un rating (1-5) et/ou un nps (0-10) est requis." }, { status: 400 });
|
|
4085
|
+
}
|
|
4086
|
+
if (hasRating && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
4087
|
+
return Response.json({ error: "rating doit \xEAtre un entier 1-5." }, { status: 400 });
|
|
4088
|
+
}
|
|
4089
|
+
if (hasNps && (!Number.isInteger(nps) || nps < 0 || nps > 10)) {
|
|
4090
|
+
return Response.json({ error: "nps doit \xEAtre un entier 0-10." }, { status: 400 });
|
|
3684
4091
|
}
|
|
3685
4092
|
if (comment && typeof comment === "string" && comment.length > 5e3) {
|
|
3686
4093
|
return Response.json(
|
|
@@ -3688,8 +4095,7 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3688
4095
|
{ status: 400 }
|
|
3689
4096
|
);
|
|
3690
4097
|
}
|
|
3691
|
-
const ticket = await payload.
|
|
3692
|
-
collection: slugs.tickets,
|
|
4098
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
3693
4099
|
id: ticketId,
|
|
3694
4100
|
depth: 0,
|
|
3695
4101
|
overrideAccess: false,
|
|
@@ -3704,8 +4110,7 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3704
4110
|
{ status: 400 }
|
|
3705
4111
|
);
|
|
3706
4112
|
}
|
|
3707
|
-
const existing = await payload.
|
|
3708
|
-
collection: slugs.satisfactionSurveys,
|
|
4113
|
+
const existing = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
3709
4114
|
where: { ticket: { equals: ticketId } },
|
|
3710
4115
|
limit: 1,
|
|
3711
4116
|
depth: 0,
|
|
@@ -3717,13 +4122,13 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3717
4122
|
{ status: 409 }
|
|
3718
4123
|
);
|
|
3719
4124
|
}
|
|
3720
|
-
const survey = await payload.
|
|
3721
|
-
collection: slugs.satisfactionSurveys,
|
|
4125
|
+
const survey = await dbCreate(payload, slugs.satisfactionSurveys, {
|
|
3722
4126
|
data: {
|
|
3723
4127
|
source: "ticket",
|
|
3724
4128
|
ticket: ticketId,
|
|
3725
4129
|
client: req.user.id,
|
|
3726
|
-
rating: Math.round(rating),
|
|
4130
|
+
...hasRating ? { rating: Math.round(rating) } : {},
|
|
4131
|
+
...hasNps ? { nps: Math.round(nps) } : {},
|
|
3727
4132
|
...comment ? { comment: comment.trim() } : {}
|
|
3728
4133
|
},
|
|
3729
4134
|
overrideAccess: true
|
|
@@ -3738,124 +4143,6 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3738
4143
|
}
|
|
3739
4144
|
};
|
|
3740
4145
|
}
|
|
3741
|
-
var TRANSPARENT_GIF = Buffer.from(
|
|
3742
|
-
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
3743
|
-
"base64"
|
|
3744
|
-
);
|
|
3745
|
-
function generateTrackingToken(ticketId, messageId, secret) {
|
|
3746
|
-
return createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex").substring(0, 16);
|
|
3747
|
-
}
|
|
3748
|
-
function createTrackOpenEndpoint(slugs) {
|
|
3749
|
-
return {
|
|
3750
|
-
path: "/support/track-open",
|
|
3751
|
-
method: "get",
|
|
3752
|
-
handler: async (req) => {
|
|
3753
|
-
const url = new URL(req.url);
|
|
3754
|
-
const ticketId = url.searchParams.get("t");
|
|
3755
|
-
const messageId = url.searchParams.get("m");
|
|
3756
|
-
const sig = url.searchParams.get("sig");
|
|
3757
|
-
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
3758
|
-
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
3759
|
-
const secret = process.env.PAYLOAD_SECRET || "";
|
|
3760
|
-
if (secret && ticketId && messageId && sig) {
|
|
3761
|
-
const expected = generateTrackingToken(ticketId, messageId, secret);
|
|
3762
|
-
if (sig !== expected) {
|
|
3763
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3764
|
-
status: 200,
|
|
3765
|
-
headers: {
|
|
3766
|
-
"Content-Type": "image/gif",
|
|
3767
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3768
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3769
|
-
}
|
|
3770
|
-
});
|
|
3771
|
-
}
|
|
3772
|
-
} else if (secret && (!sig || !ticketId || !messageId)) {
|
|
3773
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3774
|
-
status: 200,
|
|
3775
|
-
headers: {
|
|
3776
|
-
"Content-Type": "image/gif",
|
|
3777
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3778
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3779
|
-
}
|
|
3780
|
-
});
|
|
3781
|
-
}
|
|
3782
|
-
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
3783
|
-
try {
|
|
3784
|
-
const payload = req.payload;
|
|
3785
|
-
const ticket = await payload.findByID({
|
|
3786
|
-
collection: slugs.tickets,
|
|
3787
|
-
id: parsedId,
|
|
3788
|
-
depth: 0,
|
|
3789
|
-
overrideAccess: true,
|
|
3790
|
-
select: { lastClientReadAt: true }
|
|
3791
|
-
});
|
|
3792
|
-
if (ticket) {
|
|
3793
|
-
const lastRead = ticket.lastClientReadAt ? new Date(ticket.lastClientReadAt).getTime() : 0;
|
|
3794
|
-
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
|
|
3795
|
-
if (lastRead < fiveMinAgo) {
|
|
3796
|
-
await payload.update({
|
|
3797
|
-
collection: slugs.tickets,
|
|
3798
|
-
id: parsedId,
|
|
3799
|
-
data: { lastClientReadAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3800
|
-
overrideAccess: true
|
|
3801
|
-
});
|
|
3802
|
-
}
|
|
3803
|
-
}
|
|
3804
|
-
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
3805
|
-
const msg = await payload.findByID({
|
|
3806
|
-
collection: slugs.ticketMessages,
|
|
3807
|
-
id: parsedMsgId,
|
|
3808
|
-
depth: 0,
|
|
3809
|
-
overrideAccess: true,
|
|
3810
|
-
select: { emailOpenedAt: true }
|
|
3811
|
-
});
|
|
3812
|
-
if (msg && !msg.emailOpenedAt) {
|
|
3813
|
-
await payload.update({
|
|
3814
|
-
collection: slugs.ticketMessages,
|
|
3815
|
-
id: parsedMsgId,
|
|
3816
|
-
data: { emailOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3817
|
-
overrideAccess: true
|
|
3818
|
-
});
|
|
3819
|
-
const ticketInfo = await payload.findByID({
|
|
3820
|
-
collection: slugs.tickets,
|
|
3821
|
-
id: parsedId,
|
|
3822
|
-
depth: 1,
|
|
3823
|
-
overrideAccess: true,
|
|
3824
|
-
select: { ticketNumber: true, subject: true, client: true }
|
|
3825
|
-
});
|
|
3826
|
-
const clientName = typeof ticketInfo?.client === "object" ? ticketInfo.client?.firstName || "Client" : "Client";
|
|
3827
|
-
try {
|
|
3828
|
-
await payload.create({
|
|
3829
|
-
collection: "admin-notifications",
|
|
3830
|
-
data: {
|
|
3831
|
-
title: `Email ouvert \u2014 ${ticketInfo?.ticketNumber || "TK-????"}`,
|
|
3832
|
-
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || "ticket"}"`,
|
|
3833
|
-
type: "email_opened",
|
|
3834
|
-
link: `/admin/ticket?id=${parsedId}`
|
|
3835
|
-
},
|
|
3836
|
-
overrideAccess: true
|
|
3837
|
-
});
|
|
3838
|
-
} catch {
|
|
3839
|
-
}
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
} catch (err) {
|
|
3843
|
-
console.error("[track-open] Error:", err);
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3847
|
-
status: 200,
|
|
3848
|
-
headers: {
|
|
3849
|
-
"Content-Type": "image/gif",
|
|
3850
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3851
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
3852
|
-
"Pragma": "no-cache",
|
|
3853
|
-
"Expires": "0"
|
|
3854
|
-
}
|
|
3855
|
-
});
|
|
3856
|
-
}
|
|
3857
|
-
};
|
|
3858
|
-
}
|
|
3859
4146
|
|
|
3860
4147
|
// src/endpoints/export-csv.ts
|
|
3861
4148
|
function createExportCsvEndpoint(slugs) {
|
|
@@ -3871,8 +4158,7 @@ function createExportCsvEndpoint(slugs) {
|
|
|
3871
4158
|
let hasMore = true;
|
|
3872
4159
|
const allDocs = [];
|
|
3873
4160
|
while (hasMore) {
|
|
3874
|
-
const batch = await payload.
|
|
3875
|
-
collection: slugs.tickets,
|
|
4161
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3876
4162
|
limit: PAGE_SIZE2,
|
|
3877
4163
|
page,
|
|
3878
4164
|
depth: 1,
|
|
@@ -3948,21 +4234,18 @@ function createExportDataEndpoint(slugs) {
|
|
|
3948
4234
|
const payload = req.payload;
|
|
3949
4235
|
requireClient(req, slugs);
|
|
3950
4236
|
const [clientData, ticketsResult, messagesResult, surveysResult] = await Promise.all([
|
|
3951
|
-
payload.
|
|
3952
|
-
collection: slugs.supportClients,
|
|
4237
|
+
dbFindByID(payload, slugs.supportClients, {
|
|
3953
4238
|
id: req.user.id,
|
|
3954
4239
|
depth: 0,
|
|
3955
4240
|
overrideAccess: true
|
|
3956
4241
|
}),
|
|
3957
|
-
payload.
|
|
3958
|
-
collection: slugs.tickets,
|
|
4242
|
+
dbFind(payload, slugs.tickets, {
|
|
3959
4243
|
where: { client: { equals: req.user.id } },
|
|
3960
4244
|
limit: 1e3,
|
|
3961
4245
|
depth: 0,
|
|
3962
4246
|
overrideAccess: true
|
|
3963
4247
|
}),
|
|
3964
|
-
payload.
|
|
3965
|
-
collection: slugs.ticketMessages,
|
|
4248
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
3966
4249
|
where: {
|
|
3967
4250
|
"ticket.client": { equals: req.user.id },
|
|
3968
4251
|
authorType: { equals: "client" }
|
|
@@ -3971,8 +4254,7 @@ function createExportDataEndpoint(slugs) {
|
|
|
3971
4254
|
depth: 0,
|
|
3972
4255
|
overrideAccess: true
|
|
3973
4256
|
}),
|
|
3974
|
-
payload.
|
|
3975
|
-
collection: slugs.satisfactionSurveys,
|
|
4257
|
+
dbFind(payload, slugs.satisfactionSurveys, {
|
|
3976
4258
|
where: { client: { equals: req.user.id } },
|
|
3977
4259
|
limit: 500,
|
|
3978
4260
|
depth: 0,
|
|
@@ -4046,8 +4328,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4046
4328
|
if (!["create_ticket", "add_to_ticket", "ignore"].includes(action)) {
|
|
4047
4329
|
return Response.json({ error: "Invalid action" }, { status: 400 });
|
|
4048
4330
|
}
|
|
4049
|
-
const pendingEmail = await payload.
|
|
4050
|
-
collection: slugs.pendingEmails,
|
|
4331
|
+
const pendingEmail = await dbFindByID(payload, slugs.pendingEmails, {
|
|
4051
4332
|
id: Number(id),
|
|
4052
4333
|
depth: 1,
|
|
4053
4334
|
overrideAccess: true
|
|
@@ -4061,14 +4342,12 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4061
4342
|
let clientId = overrideClientId || (typeof pendingEmail.client === "object" ? pendingEmail.client?.id : pendingEmail.client);
|
|
4062
4343
|
let clientDoc = typeof pendingEmail.client === "object" ? pendingEmail.client : null;
|
|
4063
4344
|
if (overrideClientId && overrideClientId !== clientDoc?.id) {
|
|
4064
|
-
clientDoc = await payload.
|
|
4065
|
-
collection: slugs.supportClients,
|
|
4345
|
+
clientDoc = await dbFindByID(payload, slugs.supportClients, {
|
|
4066
4346
|
id: overrideClientId,
|
|
4067
4347
|
depth: 0,
|
|
4068
4348
|
overrideAccess: true
|
|
4069
4349
|
});
|
|
4070
|
-
await payload.
|
|
4071
|
-
collection: slugs.pendingEmails,
|
|
4350
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4072
4351
|
id: Number(id),
|
|
4073
4352
|
data: { client: overrideClientId },
|
|
4074
4353
|
overrideAccess: true
|
|
@@ -4085,8 +4364,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4085
4364
|
file: typeof a.file === "object" ? a.file.id : a.file
|
|
4086
4365
|
})) || [];
|
|
4087
4366
|
if (action === "ignore") {
|
|
4088
|
-
await payload.
|
|
4089
|
-
collection: slugs.pendingEmails,
|
|
4367
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4090
4368
|
id: Number(id),
|
|
4091
4369
|
data: {
|
|
4092
4370
|
status: "ignored",
|
|
@@ -4098,8 +4376,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4098
4376
|
return Response.json({ action: "ignored", pendingEmailId: Number(id) });
|
|
4099
4377
|
}
|
|
4100
4378
|
if (action === "create_ticket") {
|
|
4101
|
-
const newTicket = await payload.
|
|
4102
|
-
collection: slugs.tickets,
|
|
4379
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
4103
4380
|
data: {
|
|
4104
4381
|
subject: pendingEmail.subject,
|
|
4105
4382
|
client: clientId,
|
|
@@ -4110,8 +4387,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4110
4387
|
},
|
|
4111
4388
|
overrideAccess: true
|
|
4112
4389
|
});
|
|
4113
|
-
await payload.
|
|
4114
|
-
collection: slugs.ticketMessages,
|
|
4390
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
4115
4391
|
data: {
|
|
4116
4392
|
ticket: newTicket.id,
|
|
4117
4393
|
body: pendingEmail.body,
|
|
@@ -4138,8 +4414,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4138
4414
|
} catch (err) {
|
|
4139
4415
|
console.error("[pending-email-process] Failed to send notification:", err);
|
|
4140
4416
|
}
|
|
4141
|
-
await payload.
|
|
4142
|
-
collection: slugs.pendingEmails,
|
|
4417
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4143
4418
|
id: Number(id),
|
|
4144
4419
|
data: {
|
|
4145
4420
|
status: "processed",
|
|
@@ -4159,8 +4434,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4159
4434
|
if (!ticketId) {
|
|
4160
4435
|
return Response.json({ error: "ticketId is required for add_to_ticket" }, { status: 400 });
|
|
4161
4436
|
}
|
|
4162
|
-
const targetTicket = await payload.
|
|
4163
|
-
collection: slugs.tickets,
|
|
4437
|
+
const targetTicket = await dbFindByID(payload, slugs.tickets, {
|
|
4164
4438
|
id: ticketId,
|
|
4165
4439
|
depth: 0,
|
|
4166
4440
|
overrideAccess: true
|
|
@@ -4168,8 +4442,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4168
4442
|
if (!targetTicket) {
|
|
4169
4443
|
return Response.json({ error: "Target ticket not found" }, { status: 404 });
|
|
4170
4444
|
}
|
|
4171
|
-
await payload.
|
|
4172
|
-
collection: slugs.ticketMessages,
|
|
4445
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
4173
4446
|
data: {
|
|
4174
4447
|
ticket: ticketId,
|
|
4175
4448
|
body: pendingEmail.body,
|
|
@@ -4181,15 +4454,13 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4181
4454
|
overrideAccess: true
|
|
4182
4455
|
});
|
|
4183
4456
|
if (targetTicket.status === "resolved") {
|
|
4184
|
-
await payload.
|
|
4185
|
-
collection: slugs.tickets,
|
|
4457
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
4186
4458
|
id: ticketId,
|
|
4187
4459
|
data: { status: "open" },
|
|
4188
4460
|
overrideAccess: true
|
|
4189
4461
|
});
|
|
4190
4462
|
}
|
|
4191
|
-
await payload.
|
|
4192
|
-
collection: slugs.pendingEmails,
|
|
4463
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4193
4464
|
id: Number(id),
|
|
4194
4465
|
data: {
|
|
4195
4466
|
status: "processed",
|
|
@@ -4242,8 +4513,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4242
4513
|
if (!messageId) {
|
|
4243
4514
|
return Response.json({ error: "messageId requis" }, { status: 400 });
|
|
4244
4515
|
}
|
|
4245
|
-
const message = await payload.
|
|
4246
|
-
collection: slugs.ticketMessages,
|
|
4516
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
4247
4517
|
id: messageId,
|
|
4248
4518
|
depth: 0,
|
|
4249
4519
|
overrideAccess: true
|
|
@@ -4252,8 +4522,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4252
4522
|
return Response.json({ error: "Message introuvable" }, { status: 404 });
|
|
4253
4523
|
}
|
|
4254
4524
|
const ticketId = typeof message.ticket === "object" ? message.ticket.id : message.ticket;
|
|
4255
|
-
const ticket = await payload.
|
|
4256
|
-
collection: slugs.tickets,
|
|
4525
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
4257
4526
|
id: ticketId,
|
|
4258
4527
|
depth: 1,
|
|
4259
4528
|
overrideAccess: true
|
|
@@ -4379,8 +4648,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4379
4648
|
let created = 0;
|
|
4380
4649
|
let skipped = 0;
|
|
4381
4650
|
for (const article of KB_ARTICLES) {
|
|
4382
|
-
const existing = await payload.
|
|
4383
|
-
collection: slugs.knowledgeBase,
|
|
4651
|
+
const existing = await dbFind(payload, slugs.knowledgeBase, {
|
|
4384
4652
|
where: { slug: { equals: article.slug } },
|
|
4385
4653
|
limit: 1,
|
|
4386
4654
|
depth: 0,
|
|
@@ -4407,8 +4675,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4407
4675
|
version: 1
|
|
4408
4676
|
}
|
|
4409
4677
|
};
|
|
4410
|
-
await payload.
|
|
4411
|
-
collection: slugs.knowledgeBase,
|
|
4678
|
+
await dbCreate(payload, slugs.knowledgeBase, {
|
|
4412
4679
|
data: {
|
|
4413
4680
|
title: article.title,
|
|
4414
4681
|
slug: article.slug,
|
|
@@ -4463,8 +4730,7 @@ function createLoginEndpoint(slugs) {
|
|
|
4463
4730
|
collection: slugs.supportClients,
|
|
4464
4731
|
data: { email, password }
|
|
4465
4732
|
});
|
|
4466
|
-
payload.
|
|
4467
|
-
collection: slugs.authLogs,
|
|
4733
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4468
4734
|
data: { email, success: true, action: "login", ipAddress: ip, userAgent },
|
|
4469
4735
|
overrideAccess: true
|
|
4470
4736
|
}).catch(() => {
|
|
@@ -4488,12 +4754,14 @@ function createLoginEndpoint(slugs) {
|
|
|
4488
4754
|
);
|
|
4489
4755
|
} catch (err) {
|
|
4490
4756
|
const errorMessage = err instanceof Error ? err.message : "Erreur inconnue";
|
|
4757
|
+
if (errorMessage.includes("2FA_REQUIRED")) {
|
|
4758
|
+
return Response.json({ requires2FA: true }, { status: 200 });
|
|
4759
|
+
}
|
|
4491
4760
|
let errorReason = "Identifiants incorrects";
|
|
4492
4761
|
if (errorMessage.includes("locked") || errorMessage.includes("verrouill\xE9") || errorMessage.includes("Too many")) {
|
|
4493
4762
|
errorReason = "Compte verrouill\xE9 (trop de tentatives)";
|
|
4494
4763
|
}
|
|
4495
|
-
payload.
|
|
4496
|
-
collection: slugs.authLogs,
|
|
4764
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4497
4765
|
data: { email, success: false, action: "login", errorReason, ipAddress: ip, userAgent },
|
|
4498
4766
|
overrideAccess: true
|
|
4499
4767
|
}).catch(() => {
|
|
@@ -4514,7 +4782,10 @@ function generateSecureCode() {
|
|
|
4514
4782
|
return String(num);
|
|
4515
4783
|
}
|
|
4516
4784
|
function hashCode(code) {
|
|
4517
|
-
const secret = process.env.PAYLOAD_SECRET
|
|
4785
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
4786
|
+
if (!secret) {
|
|
4787
|
+
throw new Error("[support][2fa] PAYLOAD_SECRET is not set \u2014 refusing to operate 2FA with an insecure fallback secret");
|
|
4788
|
+
}
|
|
4518
4789
|
return createHmac("sha256", secret).update(code).digest("hex");
|
|
4519
4790
|
}
|
|
4520
4791
|
function createAuth2faEndpoint(slugs) {
|
|
@@ -4539,8 +4810,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4539
4810
|
if (sendLimiter.check(email)) {
|
|
4540
4811
|
return Response.json(genericSendResponse);
|
|
4541
4812
|
}
|
|
4542
|
-
const clients = await payload.
|
|
4543
|
-
collection: slugs.supportClients,
|
|
4813
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4544
4814
|
where: { email: { equals: email } },
|
|
4545
4815
|
limit: 1,
|
|
4546
4816
|
depth: 0,
|
|
@@ -4553,8 +4823,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4553
4823
|
const plainCode = generateSecureCode();
|
|
4554
4824
|
const twoFactorCode = hashCode(plainCode);
|
|
4555
4825
|
const twoFactorExpiry = new Date(Date.now() + 10 * 60 * 1e3).toISOString();
|
|
4556
|
-
await payload.
|
|
4557
|
-
collection: slugs.supportClients,
|
|
4826
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4558
4827
|
id: client.id,
|
|
4559
4828
|
data: { twoFactorCode, twoFactorExpiry },
|
|
4560
4829
|
overrideAccess: true
|
|
@@ -4585,8 +4854,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4585
4854
|
{ status: 429 }
|
|
4586
4855
|
);
|
|
4587
4856
|
}
|
|
4588
|
-
const clients = await payload.
|
|
4589
|
-
collection: slugs.supportClients,
|
|
4857
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4590
4858
|
where: { email: { equals: email } },
|
|
4591
4859
|
limit: 1,
|
|
4592
4860
|
depth: 0,
|
|
@@ -4602,8 +4870,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4602
4870
|
return Response.json({ error: "Aucun code en attente" }, { status: 400 });
|
|
4603
4871
|
}
|
|
4604
4872
|
if (/* @__PURE__ */ new Date() > new Date(storedExpiry)) {
|
|
4605
|
-
await payload.
|
|
4606
|
-
collection: slugs.supportClients,
|
|
4873
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4607
4874
|
id: client.id,
|
|
4608
4875
|
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4609
4876
|
overrideAccess: true
|
|
@@ -4617,10 +4884,9 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4617
4884
|
if (submittedBuffer.length !== storedBuffer.length || !crypto3.timingSafeEqual(submittedBuffer, storedBuffer)) {
|
|
4618
4885
|
return Response.json({ error: "Code incorrect" }, { status: 400 });
|
|
4619
4886
|
}
|
|
4620
|
-
await payload.
|
|
4621
|
-
collection: slugs.supportClients,
|
|
4887
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4622
4888
|
id: client.id,
|
|
4623
|
-
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4889
|
+
data: { twoFactorCode: "", twoFactorExpiry: "", twoFactorVerifiedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4624
4890
|
overrideAccess: true
|
|
4625
4891
|
});
|
|
4626
4892
|
verifyLimiter.reset(email);
|
|
@@ -4694,15 +4960,33 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4694
4960
|
if (!profile.email) {
|
|
4695
4961
|
return Response.json({ error: "no_email" }, { status: 400 });
|
|
4696
4962
|
}
|
|
4963
|
+
if (profile.verified_email !== true) {
|
|
4964
|
+
return Response.json({ error: "email_not_verified" }, { status: 403 });
|
|
4965
|
+
}
|
|
4697
4966
|
const payload = req.payload;
|
|
4698
|
-
const
|
|
4699
|
-
|
|
4700
|
-
where: { email: { equals: profile.email } },
|
|
4967
|
+
const byGoogle = await dbFind(payload, slugs.supportClients, {
|
|
4968
|
+
where: { googleId: { equals: profile.id } },
|
|
4701
4969
|
limit: 1,
|
|
4702
4970
|
depth: 0,
|
|
4703
4971
|
overrideAccess: true
|
|
4704
4972
|
});
|
|
4705
|
-
let clientDoc =
|
|
4973
|
+
let clientDoc = byGoogle.docs[0];
|
|
4974
|
+
if (!clientDoc) {
|
|
4975
|
+
const byEmail = await dbFind(payload, slugs.supportClients, {
|
|
4976
|
+
where: { email: { equals: profile.email } },
|
|
4977
|
+
limit: 1,
|
|
4978
|
+
depth: 0,
|
|
4979
|
+
overrideAccess: true
|
|
4980
|
+
});
|
|
4981
|
+
clientDoc = byEmail.docs[0];
|
|
4982
|
+
if (clientDoc && profile.id) {
|
|
4983
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4984
|
+
id: clientDoc.id,
|
|
4985
|
+
data: { googleId: profile.id },
|
|
4986
|
+
overrideAccess: true
|
|
4987
|
+
});
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4706
4990
|
if (!clientDoc) {
|
|
4707
4991
|
const allowedDomains = options?.allowedEmailDomains;
|
|
4708
4992
|
if (allowedDomains && allowedDomains.length > 0) {
|
|
@@ -4720,43 +5004,64 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4720
5004
|
const autoPassword = crypto3.randomBytes(48).toString("base64url");
|
|
4721
5005
|
const fullName = profile.name || profile.email.split("@")[0];
|
|
4722
5006
|
const nameParts = fullName.split(" ");
|
|
4723
|
-
clientDoc = await payload.
|
|
4724
|
-
collection: slugs.supportClients,
|
|
5007
|
+
clientDoc = await dbCreate(payload, slugs.supportClients, {
|
|
4725
5008
|
data: {
|
|
4726
5009
|
email: profile.email,
|
|
4727
5010
|
firstName: nameParts[0] || fullName,
|
|
4728
5011
|
lastName: nameParts.slice(1).join(" ") || "-",
|
|
4729
5012
|
company: fullName,
|
|
5013
|
+
googleId: profile.id,
|
|
4730
5014
|
password: autoPassword
|
|
4731
5015
|
},
|
|
4732
5016
|
overrideAccess: true
|
|
4733
5017
|
});
|
|
4734
5018
|
}
|
|
4735
|
-
const
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
id: clientDoc.id,
|
|
4739
|
-
data: { password: tempPassword },
|
|
4740
|
-
overrideAccess: true
|
|
4741
|
-
});
|
|
4742
|
-
const loginResult = await payload.login({
|
|
4743
|
-
collection: slugs.supportClients,
|
|
4744
|
-
data: { email: profile.email, password: tempPassword }
|
|
4745
|
-
});
|
|
4746
|
-
const postLoginPassword = crypto3.randomBytes(48).toString("base64url");
|
|
4747
|
-
await payload.update({
|
|
4748
|
-
collection: slugs.supportClients,
|
|
4749
|
-
id: clientDoc.id,
|
|
4750
|
-
data: { password: postLoginPassword },
|
|
4751
|
-
overrideAccess: true
|
|
4752
|
-
});
|
|
4753
|
-
if (!loginResult.token) {
|
|
4754
|
-
return Response.json({ error: "login_failed" }, { status: 400 });
|
|
5019
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
5020
|
+
if (!secret) {
|
|
5021
|
+
return Response.json({ error: "server_misconfigured" }, { status: 500 });
|
|
4755
5022
|
}
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
5023
|
+
const collectionConfig = payload.collections[slugs.supportClients].config;
|
|
5024
|
+
const tokenExpiration = collectionConfig.auth?.tokenExpiration ?? 7200;
|
|
5025
|
+
let sid;
|
|
5026
|
+
if (collectionConfig.auth?.useSessions) {
|
|
5027
|
+
sid = crypto3.randomUUID();
|
|
5028
|
+
const now = /* @__PURE__ */ new Date();
|
|
5029
|
+
const expiresAt = new Date(now.getTime() + tokenExpiration * 1e3);
|
|
5030
|
+
const fresh = await dbFindByID(payload, slugs.supportClients, {
|
|
5031
|
+
id: clientDoc.id,
|
|
5032
|
+
depth: 0,
|
|
5033
|
+
overrideAccess: true,
|
|
5034
|
+
showHiddenFields: true
|
|
5035
|
+
});
|
|
5036
|
+
const kept = Array.isArray(fresh?.sessions) ? fresh.sessions.filter((s) => new Date(s.expiresAt) > now) : [];
|
|
5037
|
+
await payload.db.updateOne({
|
|
5038
|
+
collection: slugs.supportClients,
|
|
5039
|
+
id: clientDoc.id,
|
|
5040
|
+
data: {
|
|
5041
|
+
sessions: [
|
|
5042
|
+
...kept,
|
|
5043
|
+
{ id: sid, createdAt: now.toISOString(), expiresAt: expiresAt.toISOString() }
|
|
5044
|
+
]
|
|
5045
|
+
},
|
|
5046
|
+
returning: false
|
|
5047
|
+
});
|
|
5048
|
+
}
|
|
5049
|
+
const fieldsToSign = getFieldsToSign({
|
|
5050
|
+
collectionConfig,
|
|
5051
|
+
email: clientDoc.email,
|
|
5052
|
+
sid,
|
|
5053
|
+
user: { ...clientDoc, collection: slugs.supportClients }
|
|
5054
|
+
});
|
|
5055
|
+
const { token, exp } = await jwtSign({ fieldsToSign, secret, tokenExpiration });
|
|
5056
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
5057
|
+
const cookieSecure = process.env.NODE_ENV === "production";
|
|
5058
|
+
headers.append(
|
|
5059
|
+
"Set-Cookie",
|
|
5060
|
+
`payload-token=${token}; HttpOnly; ${cookieSecure ? "Secure; " : ""}SameSite=Lax; Path=/; Max-Age=${tokenExpiration}`
|
|
5061
|
+
);
|
|
5062
|
+
return new Response(JSON.stringify({ token, user: clientDoc, exp }), {
|
|
5063
|
+
status: 200,
|
|
5064
|
+
headers
|
|
4760
5065
|
});
|
|
4761
5066
|
}
|
|
4762
5067
|
return Response.json({ error: "Action invalide" }, { status: 400 });
|
|
@@ -4797,8 +5102,7 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4797
5102
|
);
|
|
4798
5103
|
}
|
|
4799
5104
|
const clientId = req.user.id;
|
|
4800
|
-
const tickets = await payload.
|
|
4801
|
-
collection: slugs.tickets,
|
|
5105
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4802
5106
|
where: { client: { equals: clientId } },
|
|
4803
5107
|
limit: 1e4,
|
|
4804
5108
|
depth: 0,
|
|
@@ -4807,39 +5111,32 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4807
5111
|
});
|
|
4808
5112
|
const ticketIds = tickets.docs.map((t) => t.id);
|
|
4809
5113
|
if (ticketIds.length > 0) {
|
|
4810
|
-
await payload.
|
|
4811
|
-
collection: slugs.ticketMessages,
|
|
5114
|
+
await dbDelete(payload, slugs.ticketMessages, {
|
|
4812
5115
|
where: { ticket: { in: ticketIds } },
|
|
4813
5116
|
overrideAccess: true
|
|
4814
5117
|
});
|
|
4815
|
-
await payload.
|
|
4816
|
-
collection: slugs.ticketActivityLog,
|
|
5118
|
+
await dbDelete(payload, slugs.ticketActivityLog, {
|
|
4817
5119
|
where: { ticket: { in: ticketIds } },
|
|
4818
5120
|
overrideAccess: true
|
|
4819
5121
|
});
|
|
4820
|
-
await payload.
|
|
4821
|
-
collection: slugs.timeEntries,
|
|
5122
|
+
await dbDelete(payload, slugs.timeEntries, {
|
|
4822
5123
|
where: { ticket: { in: ticketIds } },
|
|
4823
5124
|
overrideAccess: true
|
|
4824
5125
|
});
|
|
4825
|
-
await payload.
|
|
4826
|
-
collection: slugs.tickets,
|
|
5126
|
+
await dbDelete(payload, slugs.tickets, {
|
|
4827
5127
|
where: { client: { equals: clientId } },
|
|
4828
5128
|
overrideAccess: true
|
|
4829
5129
|
});
|
|
4830
5130
|
}
|
|
4831
|
-
await payload.
|
|
4832
|
-
collection: slugs.satisfactionSurveys,
|
|
5131
|
+
await dbDelete(payload, slugs.satisfactionSurveys, {
|
|
4833
5132
|
where: { client: { equals: clientId } },
|
|
4834
5133
|
overrideAccess: true
|
|
4835
5134
|
});
|
|
4836
|
-
await payload.
|
|
4837
|
-
collection: slugs.chatMessages,
|
|
5135
|
+
await dbDelete(payload, slugs.chatMessages, {
|
|
4838
5136
|
where: { client: { equals: clientId } },
|
|
4839
5137
|
overrideAccess: true
|
|
4840
5138
|
});
|
|
4841
|
-
await payload.
|
|
4842
|
-
collection: slugs.supportClients,
|
|
5139
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4843
5140
|
id: clientId,
|
|
4844
5141
|
overrideAccess: true
|
|
4845
5142
|
});
|
|
@@ -4880,8 +5177,8 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4880
5177
|
return Response.json({ error: "sourceId and targetId are required and must be different" }, { status: 400 });
|
|
4881
5178
|
}
|
|
4882
5179
|
const [source, target] = await Promise.all([
|
|
4883
|
-
payload
|
|
4884
|
-
payload
|
|
5180
|
+
dbFindByID(payload, slugs.supportClients, { id: sourceId, depth: 0, overrideAccess: true }),
|
|
5181
|
+
dbFindByID(payload, slugs.supportClients, { id: targetId, depth: 0, overrideAccess: true })
|
|
4885
5182
|
]);
|
|
4886
5183
|
if (!source || !target) {
|
|
4887
5184
|
return Response.json({ error: "Source or target client not found" }, { status: 404 });
|
|
@@ -4893,63 +5190,57 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4893
5190
|
pendingEmails: 0,
|
|
4894
5191
|
satisfactionSurveys: 0
|
|
4895
5192
|
};
|
|
4896
|
-
const tickets = await payload.
|
|
4897
|
-
collection: slugs.tickets,
|
|
5193
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4898
5194
|
where: { client: { equals: sourceId } },
|
|
4899
5195
|
limit: 500,
|
|
4900
5196
|
depth: 0,
|
|
4901
5197
|
overrideAccess: true
|
|
4902
5198
|
});
|
|
4903
5199
|
for (const ticket of tickets.docs) {
|
|
4904
|
-
await payload
|
|
5200
|
+
await dbUpdate(payload, slugs.tickets, { id: ticket.id, data: { client: targetId }, overrideAccess: true });
|
|
4905
5201
|
results.tickets++;
|
|
4906
5202
|
}
|
|
4907
|
-
const messages = await payload.
|
|
4908
|
-
collection: slugs.ticketMessages,
|
|
5203
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
4909
5204
|
where: { authorClient: { equals: sourceId } },
|
|
4910
5205
|
limit: 1e3,
|
|
4911
5206
|
depth: 0,
|
|
4912
5207
|
overrideAccess: true
|
|
4913
5208
|
});
|
|
4914
5209
|
for (const msg of messages.docs) {
|
|
4915
|
-
await payload
|
|
5210
|
+
await dbUpdate(payload, slugs.ticketMessages, { id: msg.id, data: { authorClient: targetId }, overrideAccess: true });
|
|
4916
5211
|
results.ticketMessages++;
|
|
4917
5212
|
}
|
|
4918
|
-
const chats = await payload.
|
|
4919
|
-
collection: slugs.chatMessages,
|
|
5213
|
+
const chats = await dbFind(payload, slugs.chatMessages, {
|
|
4920
5214
|
where: { client: { equals: sourceId } },
|
|
4921
5215
|
limit: 1e3,
|
|
4922
5216
|
depth: 0,
|
|
4923
5217
|
overrideAccess: true
|
|
4924
5218
|
});
|
|
4925
5219
|
for (const chat of chats.docs) {
|
|
4926
|
-
await payload
|
|
5220
|
+
await dbUpdate(payload, slugs.chatMessages, { id: chat.id, data: { client: targetId }, overrideAccess: true });
|
|
4927
5221
|
results.chatMessages++;
|
|
4928
5222
|
}
|
|
4929
|
-
const pendingEmails = await payload.
|
|
4930
|
-
collection: slugs.pendingEmails,
|
|
5223
|
+
const pendingEmails = await dbFind(payload, slugs.pendingEmails, {
|
|
4931
5224
|
where: { client: { equals: sourceId } },
|
|
4932
5225
|
limit: 500,
|
|
4933
5226
|
depth: 0,
|
|
4934
5227
|
overrideAccess: true
|
|
4935
5228
|
});
|
|
4936
5229
|
for (const pe of pendingEmails.docs) {
|
|
4937
|
-
await payload
|
|
5230
|
+
await dbUpdate(payload, slugs.pendingEmails, { id: pe.id, data: { client: targetId }, overrideAccess: true });
|
|
4938
5231
|
results.pendingEmails++;
|
|
4939
5232
|
}
|
|
4940
|
-
const surveys = await payload.
|
|
4941
|
-
collection: slugs.satisfactionSurveys,
|
|
5233
|
+
const surveys = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
4942
5234
|
where: { client: { equals: sourceId } },
|
|
4943
5235
|
limit: 500,
|
|
4944
5236
|
depth: 0,
|
|
4945
5237
|
overrideAccess: true
|
|
4946
5238
|
});
|
|
4947
5239
|
for (const survey of surveys.docs) {
|
|
4948
|
-
await payload
|
|
5240
|
+
await dbUpdate(payload, slugs.satisfactionSurveys, { id: survey.id, data: { client: targetId }, overrideAccess: true });
|
|
4949
5241
|
results.satisfactionSurveys++;
|
|
4950
5242
|
}
|
|
4951
|
-
await payload.
|
|
4952
|
-
collection: slugs.supportClients,
|
|
5243
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4953
5244
|
id: sourceId,
|
|
4954
5245
|
overrideAccess: true
|
|
4955
5246
|
});
|
|
@@ -5143,8 +5434,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5143
5434
|
if (blockedEmails.includes(conversation.client.email)) {
|
|
5144
5435
|
return Response.json({ error: "Cannot create ticket from system email address" }, { status: 400 });
|
|
5145
5436
|
}
|
|
5146
|
-
const clientResult = await payload.
|
|
5147
|
-
collection: slugs.supportClients,
|
|
5437
|
+
const clientResult = await dbFind(payload, slugs.supportClients, {
|
|
5148
5438
|
where: { email: { equals: conversation.client.email } },
|
|
5149
5439
|
limit: 1,
|
|
5150
5440
|
depth: 0,
|
|
@@ -5155,8 +5445,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5155
5445
|
if (!client) {
|
|
5156
5446
|
const nameParts = conversation.client.name.split(" ");
|
|
5157
5447
|
const randomPassword = crypto3.randomBytes(48).toString("base64url");
|
|
5158
|
-
client = await payload.
|
|
5159
|
-
collection: slugs.supportClients,
|
|
5448
|
+
client = await dbCreate(payload, slugs.supportClients, {
|
|
5160
5449
|
data: {
|
|
5161
5450
|
email: conversation.client.email,
|
|
5162
5451
|
password: randomPassword,
|
|
@@ -5168,15 +5457,13 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5168
5457
|
});
|
|
5169
5458
|
isNewClient = true;
|
|
5170
5459
|
}
|
|
5171
|
-
const adminUsers = await payload.
|
|
5172
|
-
collection: slugs.users,
|
|
5460
|
+
const adminUsers = await dbFind(payload, slugs.users, {
|
|
5173
5461
|
limit: 1,
|
|
5174
5462
|
depth: 0,
|
|
5175
5463
|
overrideAccess: true
|
|
5176
5464
|
});
|
|
5177
5465
|
const adminUserId = adminUsers.docs[0]?.id;
|
|
5178
|
-
const ticket = await payload.
|
|
5179
|
-
collection: slugs.tickets,
|
|
5466
|
+
const ticket = await dbCreate(payload, slugs.tickets, {
|
|
5180
5467
|
data: {
|
|
5181
5468
|
subject: conversation.subject,
|
|
5182
5469
|
client: client.id,
|
|
@@ -5189,8 +5476,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5189
5476
|
let importedCount = 0;
|
|
5190
5477
|
for (const msg of conversation.messages) {
|
|
5191
5478
|
const isAdmin = msg.from === "admin";
|
|
5192
|
-
await payload.
|
|
5193
|
-
collection: slugs.ticketMessages,
|
|
5479
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
5194
5480
|
data: {
|
|
5195
5481
|
ticket: ticket.id,
|
|
5196
5482
|
body: msg.content,
|
|
@@ -5226,8 +5512,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5226
5512
|
// src/utils/fireWebhooks.ts
|
|
5227
5513
|
async function fireWebhooks(payload, slugs, event, data) {
|
|
5228
5514
|
try {
|
|
5229
|
-
const endpoints = await payload.
|
|
5230
|
-
collection: slugs.webhookEndpoints,
|
|
5515
|
+
const endpoints = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5231
5516
|
where: {
|
|
5232
5517
|
active: { equals: true },
|
|
5233
5518
|
events: { contains: event }
|
|
@@ -5251,8 +5536,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5251
5536
|
signal: AbortSignal.timeout(1e4)
|
|
5252
5537
|
});
|
|
5253
5538
|
try {
|
|
5254
|
-
await payload.
|
|
5255
|
-
collection: slugs.webhookEndpoints,
|
|
5539
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5256
5540
|
id: endpoint.id,
|
|
5257
5541
|
data: {
|
|
5258
5542
|
lastTriggeredAt: timestamp,
|
|
@@ -5265,8 +5549,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5265
5549
|
} catch (error) {
|
|
5266
5550
|
console.warn(`[support] Webhook delivery failed: ${endpoint.url}`, error);
|
|
5267
5551
|
try {
|
|
5268
|
-
await payload.
|
|
5269
|
-
collection: slugs.webhookEndpoints,
|
|
5552
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5270
5553
|
id: endpoint.id,
|
|
5271
5554
|
data: {
|
|
5272
5555
|
lastTriggeredAt: timestamp,
|
|
@@ -5301,8 +5584,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5301
5584
|
const settings = await readSupportSettings(payload);
|
|
5302
5585
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
|
|
5303
5586
|
const results = { processed: 0, errors: 0 };
|
|
5304
|
-
const scheduled = await payload.
|
|
5305
|
-
collection: slugs.ticketMessages,
|
|
5587
|
+
const scheduled = await dbFind(payload, slugs.ticketMessages, {
|
|
5306
5588
|
where: {
|
|
5307
5589
|
and: [
|
|
5308
5590
|
{ scheduledAt: { less_than_equal: now.toISOString() } },
|
|
@@ -5322,8 +5604,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5322
5604
|
try {
|
|
5323
5605
|
const msg = message;
|
|
5324
5606
|
const ticketId = typeof msg.ticket === "object" ? msg.ticket.id : msg.ticket;
|
|
5325
|
-
const ticket = await payload.
|
|
5326
|
-
collection: slugs.tickets,
|
|
5607
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5327
5608
|
id: ticketId,
|
|
5328
5609
|
depth: 1,
|
|
5329
5610
|
overrideAccess: true
|
|
@@ -5346,8 +5627,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5346
5627
|
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
5347
5628
|
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>`
|
|
5348
5629
|
});
|
|
5349
|
-
await payload.
|
|
5350
|
-
collection: slugs.ticketMessages,
|
|
5630
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5351
5631
|
id: msg.id,
|
|
5352
5632
|
data: {
|
|
5353
5633
|
scheduledSent: true,
|
|
@@ -5357,16 +5637,14 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5357
5637
|
overrideAccess: true
|
|
5358
5638
|
});
|
|
5359
5639
|
} else {
|
|
5360
|
-
await payload.
|
|
5361
|
-
collection: slugs.ticketMessages,
|
|
5640
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5362
5641
|
id: msg.id,
|
|
5363
5642
|
data: { scheduledSent: true },
|
|
5364
5643
|
overrideAccess: true
|
|
5365
5644
|
});
|
|
5366
5645
|
}
|
|
5367
5646
|
if (msg.authorType === "admin" && !msg.isInternal) {
|
|
5368
|
-
await payload.
|
|
5369
|
-
collection: slugs.tickets,
|
|
5647
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
5370
5648
|
id: ticketId,
|
|
5371
5649
|
data: { status: "waiting_client" },
|
|
5372
5650
|
overrideAccess: true
|
|
@@ -5398,6 +5676,296 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5398
5676
|
};
|
|
5399
5677
|
}
|
|
5400
5678
|
|
|
5679
|
+
// src/endpoints/process-snooze.ts
|
|
5680
|
+
function createProcessSnoozeEndpoint(slugs) {
|
|
5681
|
+
return {
|
|
5682
|
+
path: "/support/process-snooze",
|
|
5683
|
+
method: "post",
|
|
5684
|
+
handler: async (req) => {
|
|
5685
|
+
const secret = req.headers.get("x-cron-secret");
|
|
5686
|
+
const expectedSecret = process.env.CRON_SECRET;
|
|
5687
|
+
if (!expectedSecret || secret !== expectedSecret) {
|
|
5688
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5689
|
+
}
|
|
5690
|
+
try {
|
|
5691
|
+
const payload = req.payload;
|
|
5692
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
5693
|
+
const results = { processed: 0, errors: 0 };
|
|
5694
|
+
const due = await dbFind(payload, slugs.tickets, {
|
|
5695
|
+
where: {
|
|
5696
|
+
and: [
|
|
5697
|
+
{ snoozeUntil: { exists: true } },
|
|
5698
|
+
{ snoozeUntil: { less_than_equal: nowIso } }
|
|
5699
|
+
]
|
|
5700
|
+
},
|
|
5701
|
+
limit: 500,
|
|
5702
|
+
depth: 0,
|
|
5703
|
+
overrideAccess: true,
|
|
5704
|
+
select: { id: true, ticketNumber: true }
|
|
5705
|
+
});
|
|
5706
|
+
for (const ticket of due.docs) {
|
|
5707
|
+
try {
|
|
5708
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
5709
|
+
id: ticket.id,
|
|
5710
|
+
data: { snoozeUntil: null },
|
|
5711
|
+
overrideAccess: true
|
|
5712
|
+
});
|
|
5713
|
+
try {
|
|
5714
|
+
await dbCreate(payload, slugs.ticketActivityLog, {
|
|
5715
|
+
data: {
|
|
5716
|
+
ticket: ticket.id,
|
|
5717
|
+
action: "snooze_expired",
|
|
5718
|
+
detail: "Snooze \xE9chu \u2014 ticket r\xE9activ\xE9",
|
|
5719
|
+
actorType: "system"
|
|
5720
|
+
},
|
|
5721
|
+
overrideAccess: true
|
|
5722
|
+
});
|
|
5723
|
+
} catch {
|
|
5724
|
+
}
|
|
5725
|
+
results.processed++;
|
|
5726
|
+
} catch (err) {
|
|
5727
|
+
console.error("[support] Failed to wake snoozed ticket:", err);
|
|
5728
|
+
results.errors++;
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
return Response.json({ ok: true, ...results });
|
|
5732
|
+
} catch (err) {
|
|
5733
|
+
console.error("[process-snooze] Error:", err);
|
|
5734
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
};
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5740
|
+
// src/endpoints/process-digests.ts
|
|
5741
|
+
function createProcessDigestsEndpoint(slugs) {
|
|
5742
|
+
return {
|
|
5743
|
+
path: "/support/process-digests",
|
|
5744
|
+
method: "post",
|
|
5745
|
+
handler: async (req) => {
|
|
5746
|
+
const secret = req.headers.get("x-cron-secret");
|
|
5747
|
+
if (!process.env.CRON_SECRET || secret !== process.env.CRON_SECRET) {
|
|
5748
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5749
|
+
}
|
|
5750
|
+
try {
|
|
5751
|
+
const payload = req.payload;
|
|
5752
|
+
let body = {};
|
|
5753
|
+
try {
|
|
5754
|
+
body = await req.json();
|
|
5755
|
+
} catch {
|
|
5756
|
+
}
|
|
5757
|
+
const frequency = body.frequency === "weekly" ? "weekly" : "daily";
|
|
5758
|
+
const settings = await readSupportSettings(payload);
|
|
5759
|
+
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
|
|
5760
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
5761
|
+
const itemsByClient = /* @__PURE__ */ new Map();
|
|
5762
|
+
let page = 1;
|
|
5763
|
+
let hasMore = true;
|
|
5764
|
+
while (hasMore && page <= 50) {
|
|
5765
|
+
const batch = await dbFind(payload, slugs.notificationQueue, { limit: 200, page, depth: 0, overrideAccess: true, sort: "createdAt" });
|
|
5766
|
+
for (const it of batch.docs) {
|
|
5767
|
+
const raw = it;
|
|
5768
|
+
const clientId = typeof raw.client === "object" ? raw.client?.id : raw.client;
|
|
5769
|
+
if (clientId === void 0 || clientId === null) continue;
|
|
5770
|
+
const key = String(clientId);
|
|
5771
|
+
if (!itemsByClient.has(key)) itemsByClient.set(key, []);
|
|
5772
|
+
itemsByClient.get(key).push({ id: raw.id, title: raw.title, message: raw.message });
|
|
5773
|
+
}
|
|
5774
|
+
hasMore = batch.hasNextPage ?? false;
|
|
5775
|
+
page++;
|
|
5776
|
+
}
|
|
5777
|
+
const results = { clients: 0, items: 0, errors: 0 };
|
|
5778
|
+
for (const [clientId, items] of itemsByClient) {
|
|
5779
|
+
try {
|
|
5780
|
+
const client = await dbFindByID(payload, slugs.supportClients, { id: clientId, depth: 0, overrideAccess: true });
|
|
5781
|
+
if (!client || client.notificationFrequency !== frequency) continue;
|
|
5782
|
+
if (client.email) {
|
|
5783
|
+
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("");
|
|
5784
|
+
const html = emailWrapper("Recapitulatif de vos tickets", [
|
|
5785
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
|
|
5786
|
+
emailParagraph(`Voici le recapitulatif de l'activite sur vos tickets (${items.length} notification${items.length > 1 ? "s" : ""}) :`),
|
|
5787
|
+
`<table cellpadding="0" cellspacing="0" border="0" width="100%">${rows}</table>`,
|
|
5788
|
+
...baseUrl ? [emailParagraph(`<a href="${baseUrl}/support/dashboard">Acceder a votre espace support</a>`)] : []
|
|
5789
|
+
].join(""), { kind: "system" });
|
|
5790
|
+
await payload.sendEmail({ to: client.email, ...replyTo ? { replyTo } : {}, subject: `Recapitulatif support (${items.length})`, html });
|
|
5791
|
+
}
|
|
5792
|
+
for (const i of items) {
|
|
5793
|
+
try {
|
|
5794
|
+
await dbDelete(payload, slugs.notificationQueue, { id: i.id, overrideAccess: true });
|
|
5795
|
+
} catch {
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
results.clients++;
|
|
5799
|
+
results.items += items.length;
|
|
5800
|
+
} catch (err) {
|
|
5801
|
+
console.error("[process-digests] client error:", err);
|
|
5802
|
+
results.errors++;
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
return Response.json({ ok: true, frequency, ...results });
|
|
5806
|
+
} catch (err) {
|
|
5807
|
+
console.error("[process-digests] Error:", err);
|
|
5808
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
};
|
|
5812
|
+
}
|
|
5813
|
+
var PROVIDERS = ["whatsapp", "messenger"];
|
|
5814
|
+
function createChannelsWebhookEndpoint(slugs) {
|
|
5815
|
+
return {
|
|
5816
|
+
path: "/support/channels/webhook",
|
|
5817
|
+
method: "post",
|
|
5818
|
+
handler: async (req) => {
|
|
5819
|
+
const secret = req.headers.get("x-channel-secret");
|
|
5820
|
+
if (!process.env.CHANNELS_WEBHOOK_SECRET || secret !== process.env.CHANNELS_WEBHOOK_SECRET) {
|
|
5821
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5822
|
+
}
|
|
5823
|
+
let body;
|
|
5824
|
+
try {
|
|
5825
|
+
body = await req.json();
|
|
5826
|
+
} catch {
|
|
5827
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5828
|
+
}
|
|
5829
|
+
const provider = body.provider;
|
|
5830
|
+
const from = (body.from || "").trim();
|
|
5831
|
+
const text = (body.text || "").trim();
|
|
5832
|
+
if (!PROVIDERS.includes(provider) || !from || !text) {
|
|
5833
|
+
return Response.json({ error: "provider, from et text requis." }, { status: 400 });
|
|
5834
|
+
}
|
|
5835
|
+
try {
|
|
5836
|
+
const payload = req.payload;
|
|
5837
|
+
const email = `${from}@${provider}.channel`;
|
|
5838
|
+
const existing = await dbFind(payload, slugs.supportClients, { where: { email: { equals: email } }, limit: 1, depth: 0, overrideAccess: true });
|
|
5839
|
+
let clientId;
|
|
5840
|
+
if (existing.docs.length > 0) {
|
|
5841
|
+
clientId = existing.docs[0].id;
|
|
5842
|
+
} else {
|
|
5843
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
5844
|
+
data: { email, firstName: body.name || provider, lastName: from.slice(0, 40), company: provider, password: randomBytes(16).toString("hex") },
|
|
5845
|
+
overrideAccess: true
|
|
5846
|
+
});
|
|
5847
|
+
clientId = created.id;
|
|
5848
|
+
}
|
|
5849
|
+
const open = await dbFind(payload, slugs.tickets, {
|
|
5850
|
+
where: { and: [{ client: { equals: clientId } }, { source: { equals: provider } }, { status: { in: ["open", "waiting_client"] } }] },
|
|
5851
|
+
sort: "-createdAt",
|
|
5852
|
+
limit: 1,
|
|
5853
|
+
depth: 0,
|
|
5854
|
+
overrideAccess: true
|
|
5855
|
+
});
|
|
5856
|
+
let ticketId;
|
|
5857
|
+
let createdTicket = false;
|
|
5858
|
+
if (open.docs.length > 0) {
|
|
5859
|
+
ticketId = open.docs[0].id;
|
|
5860
|
+
} else {
|
|
5861
|
+
const t = await dbCreate(payload, slugs.tickets, {
|
|
5862
|
+
data: { subject: text.slice(0, 80), client: clientId, status: "open", priority: "normal", source: provider },
|
|
5863
|
+
overrideAccess: true
|
|
5864
|
+
});
|
|
5865
|
+
ticketId = t.id;
|
|
5866
|
+
createdTicket = true;
|
|
5867
|
+
}
|
|
5868
|
+
const msg = await dbCreate(payload, slugs.ticketMessages, {
|
|
5869
|
+
data: { ticket: ticketId, body: text, authorType: "client", authorClient: clientId },
|
|
5870
|
+
overrideAccess: true
|
|
5871
|
+
});
|
|
5872
|
+
return Response.json({ ok: true, ticketId, createdTicket, messageId: msg.id });
|
|
5873
|
+
} catch (err) {
|
|
5874
|
+
console.error("[channels] Error:", err);
|
|
5875
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5878
|
+
};
|
|
5879
|
+
}
|
|
5880
|
+
var configured = false;
|
|
5881
|
+
function ensureVapid() {
|
|
5882
|
+
if (configured) return true;
|
|
5883
|
+
const publicKey = process.env.VAPID_PUBLIC_KEY;
|
|
5884
|
+
const privateKey = process.env.VAPID_PRIVATE_KEY;
|
|
5885
|
+
if (!publicKey || !privateKey) return false;
|
|
5886
|
+
webpush.setVapidDetails(process.env.VAPID_SUBJECT || "mailto:support@example.com", publicKey, privateKey);
|
|
5887
|
+
configured = true;
|
|
5888
|
+
return true;
|
|
5889
|
+
}
|
|
5890
|
+
function getVapidPublicKey() {
|
|
5891
|
+
return process.env.VAPID_PUBLIC_KEY || null;
|
|
5892
|
+
}
|
|
5893
|
+
async function sendPushToUser(payload, slugs, userId, notification) {
|
|
5894
|
+
if (!ensureVapid()) return { sent: 0 };
|
|
5895
|
+
let sent = 0;
|
|
5896
|
+
try {
|
|
5897
|
+
const subs = await dbFind(payload, slugs.pushSubscriptions, { where: { user: { equals: userId } }, limit: 100, depth: 0, overrideAccess: true });
|
|
5898
|
+
for (const s of subs.docs) {
|
|
5899
|
+
const row = s;
|
|
5900
|
+
if (!row.endpoint || !row.p256dh || !row.auth) continue;
|
|
5901
|
+
try {
|
|
5902
|
+
await webpush.sendNotification(
|
|
5903
|
+
{ endpoint: row.endpoint, keys: { p256dh: row.p256dh, auth: row.auth } },
|
|
5904
|
+
JSON.stringify(notification)
|
|
5905
|
+
);
|
|
5906
|
+
sent++;
|
|
5907
|
+
} catch (err) {
|
|
5908
|
+
const code = err?.statusCode;
|
|
5909
|
+
if (code === 404 || code === 410) {
|
|
5910
|
+
try {
|
|
5911
|
+
await dbDelete(payload, slugs.pushSubscriptions, { id: row.id, overrideAccess: true });
|
|
5912
|
+
} catch {
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
} catch (err) {
|
|
5918
|
+
console.error("[support] Push send failed:", err);
|
|
5919
|
+
}
|
|
5920
|
+
return { sent };
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5923
|
+
// src/endpoints/push.ts
|
|
5924
|
+
function createVapidKeyEndpoint() {
|
|
5925
|
+
return {
|
|
5926
|
+
path: "/support/push/vapid-public-key",
|
|
5927
|
+
method: "get",
|
|
5928
|
+
handler: async () => Response.json({ publicKey: getVapidPublicKey() })
|
|
5929
|
+
};
|
|
5930
|
+
}
|
|
5931
|
+
function createPushSubscribeEndpoint(slugs) {
|
|
5932
|
+
return {
|
|
5933
|
+
path: "/support/push/subscribe",
|
|
5934
|
+
method: "post",
|
|
5935
|
+
handler: async (req) => {
|
|
5936
|
+
try {
|
|
5937
|
+
requireAdmin(req, slugs);
|
|
5938
|
+
let body;
|
|
5939
|
+
try {
|
|
5940
|
+
body = await req.json();
|
|
5941
|
+
} catch {
|
|
5942
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5943
|
+
}
|
|
5944
|
+
const sub = body?.subscription;
|
|
5945
|
+
const endpoint = sub?.endpoint;
|
|
5946
|
+
const p256dh = sub?.keys?.p256dh;
|
|
5947
|
+
const auth = sub?.keys?.auth;
|
|
5948
|
+
if (!endpoint || !p256dh || !auth) {
|
|
5949
|
+
return Response.json({ error: "subscription invalide (endpoint + keys requis)." }, { status: 400 });
|
|
5950
|
+
}
|
|
5951
|
+
const data = { user: req.user.id, endpoint, p256dh, auth, userAgent: req.headers.get("user-agent") || "" };
|
|
5952
|
+
const existing = await dbFind(req.payload, slugs.pushSubscriptions, { where: { endpoint: { equals: endpoint } }, limit: 1, depth: 0, overrideAccess: true });
|
|
5953
|
+
if (existing.docs.length > 0) {
|
|
5954
|
+
await dbUpdate(req.payload, slugs.pushSubscriptions, { id: existing.docs[0].id, data, overrideAccess: true });
|
|
5955
|
+
} else {
|
|
5956
|
+
await dbCreate(req.payload, slugs.pushSubscriptions, { data, overrideAccess: true });
|
|
5957
|
+
}
|
|
5958
|
+
return Response.json({ ok: true });
|
|
5959
|
+
} catch (error) {
|
|
5960
|
+
const authResponse = handleAuthError(error);
|
|
5961
|
+
if (authResponse) return authResponse;
|
|
5962
|
+
console.error("[push] Error:", error);
|
|
5963
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
};
|
|
5967
|
+
}
|
|
5968
|
+
|
|
5401
5969
|
// src/endpoints/user-prefs.ts
|
|
5402
5970
|
var PREF_KEY_PREFIX = "support-user-prefs";
|
|
5403
5971
|
var DEFAULT_USER_PREFS2 = {
|
|
@@ -5413,8 +5981,7 @@ function createUserPrefsGetEndpoint(slugs) {
|
|
|
5413
5981
|
const payload = req.payload;
|
|
5414
5982
|
requireAdmin(req, slugs);
|
|
5415
5983
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5416
|
-
const prefs = await payload
|
|
5417
|
-
collection: "payload-preferences",
|
|
5984
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
5418
5985
|
where: { key: { equals: key } },
|
|
5419
5986
|
limit: 1,
|
|
5420
5987
|
depth: 0,
|
|
@@ -5448,8 +6015,7 @@ function createUserPrefsPostEndpoint(slugs) {
|
|
|
5448
6015
|
requireAdmin(req, slugs);
|
|
5449
6016
|
const body = await req.json();
|
|
5450
6017
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5451
|
-
const existing = await payload
|
|
5452
|
-
collection: "payload-preferences",
|
|
6018
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
5453
6019
|
where: { key: { equals: key } },
|
|
5454
6020
|
limit: 1,
|
|
5455
6021
|
depth: 0,
|
|
@@ -5510,8 +6076,7 @@ function createEscalateEndpoint(slugs) {
|
|
|
5510
6076
|
if (Number.isNaN(ticketId)) {
|
|
5511
6077
|
return Response.json({ error: "invalid-id" }, { status: 400 });
|
|
5512
6078
|
}
|
|
5513
|
-
const ticket = await req.payload.
|
|
5514
|
-
collection: slugs.tickets,
|
|
6079
|
+
const ticket = await dbUpdate(req.payload, slugs.tickets, {
|
|
5515
6080
|
id: ticketId,
|
|
5516
6081
|
data: { status: "escalated" },
|
|
5517
6082
|
overrideAccess: true
|
|
@@ -5559,8 +6124,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5559
6124
|
{ status: 400 }
|
|
5560
6125
|
);
|
|
5561
6126
|
}
|
|
5562
|
-
const ticket = await payload.
|
|
5563
|
-
collection: slugs.tickets,
|
|
6127
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5564
6128
|
id: ticketId,
|
|
5565
6129
|
depth: 0,
|
|
5566
6130
|
overrideAccess: true
|
|
@@ -5572,8 +6136,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5572
6136
|
if (ticketClientId !== req.user.id) {
|
|
5573
6137
|
return Response.json({ error: "forbidden" }, { status: 403 });
|
|
5574
6138
|
}
|
|
5575
|
-
const existing = await payload
|
|
5576
|
-
collection: "ticket-feedback",
|
|
6139
|
+
const existing = await dbFind(payload, "ticket-feedback", {
|
|
5577
6140
|
where: { ticket: { equals: ticketId } },
|
|
5578
6141
|
limit: 1,
|
|
5579
6142
|
depth: 0,
|
|
@@ -5585,8 +6148,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5585
6148
|
{ status: 409 }
|
|
5586
6149
|
);
|
|
5587
6150
|
}
|
|
5588
|
-
const feedback = await payload
|
|
5589
|
-
collection: "ticket-feedback",
|
|
6151
|
+
const feedback = await dbCreate(payload, "ticket-feedback", {
|
|
5590
6152
|
data: {
|
|
5591
6153
|
ticket: ticketId,
|
|
5592
6154
|
client: req.user.id,
|
|
@@ -5610,6 +6172,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5610
6172
|
// src/endpoints/transfer-ticket.ts
|
|
5611
6173
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5612
6174
|
var MAX_TRANSFERS_PER_DAY = 5;
|
|
6175
|
+
var transferLimiter = new RateLimiter(24 * 60 * 60 * 1e3, MAX_TRANSFERS_PER_DAY);
|
|
5613
6176
|
function createTransferTicketEndpoint(slugs) {
|
|
5614
6177
|
return {
|
|
5615
6178
|
path: "/support/tickets/:id/transfer",
|
|
@@ -5634,8 +6197,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5634
6197
|
const includeAttachments = body.includeAttachments !== false;
|
|
5635
6198
|
const customMessage = typeof body.message === "string" ? body.message.slice(0, 1e3) : "";
|
|
5636
6199
|
const payload = req.payload;
|
|
5637
|
-
const ticket = await payload.
|
|
5638
|
-
collection: slugs.tickets,
|
|
6200
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5639
6201
|
id: ticketId,
|
|
5640
6202
|
depth: 1,
|
|
5641
6203
|
overrideAccess: true
|
|
@@ -5646,10 +6208,15 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5646
6208
|
if (!isAdmin && !isOwner) {
|
|
5647
6209
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5648
6210
|
}
|
|
6211
|
+
if (transferLimiter.check(`${req.user.id}:${ticketId}`)) {
|
|
6212
|
+
return Response.json(
|
|
6213
|
+
{ error: `Limite atteinte (${MAX_TRANSFERS_PER_DAY} transferts par 24h sur ce ticket)` },
|
|
6214
|
+
{ status: 429 }
|
|
6215
|
+
);
|
|
6216
|
+
}
|
|
5649
6217
|
try {
|
|
5650
6218
|
const since = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
5651
|
-
const existing = await payload.
|
|
5652
|
-
collection: slugs.emailLogs,
|
|
6219
|
+
const existing = await dbCount(payload, slugs.emailLogs, {
|
|
5653
6220
|
where: {
|
|
5654
6221
|
and: [
|
|
5655
6222
|
{ action: { equals: "transfer" } },
|
|
@@ -5667,8 +6234,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5667
6234
|
}
|
|
5668
6235
|
} catch {
|
|
5669
6236
|
}
|
|
5670
|
-
const messages = await payload.
|
|
5671
|
-
collection: slugs.ticketMessages,
|
|
6237
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
5672
6238
|
where: {
|
|
5673
6239
|
and: [
|
|
5674
6240
|
{ ticket: { equals: ticketId } },
|
|
@@ -5748,8 +6314,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5748
6314
|
html
|
|
5749
6315
|
});
|
|
5750
6316
|
try {
|
|
5751
|
-
await payload.
|
|
5752
|
-
collection: slugs.emailLogs,
|
|
6317
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5753
6318
|
data: {
|
|
5754
6319
|
status: "success",
|
|
5755
6320
|
action: "transfer",
|
|
@@ -5789,6 +6354,8 @@ function statusToLabel(status) {
|
|
|
5789
6354
|
}
|
|
5790
6355
|
}
|
|
5791
6356
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6357
|
+
var MAX_COLLABORATORS_PER_TICKET = 20;
|
|
6358
|
+
var inviteLimiter = new RateLimiter(60 * 60 * 1e3, 10);
|
|
5792
6359
|
function createInviteCollaboratorEndpoint(slugs) {
|
|
5793
6360
|
return {
|
|
5794
6361
|
path: "/support/tickets/:id/invite",
|
|
@@ -5812,8 +6379,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5812
6379
|
}
|
|
5813
6380
|
const role = body.role === "collaborator" ? "collaborator" : "viewer";
|
|
5814
6381
|
const payload = req.payload;
|
|
5815
|
-
const ticket = await payload.
|
|
5816
|
-
collection: slugs.tickets,
|
|
6382
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5817
6383
|
id: ticketId,
|
|
5818
6384
|
depth: 1,
|
|
5819
6385
|
overrideAccess: true
|
|
@@ -5825,13 +6391,22 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5825
6391
|
if (!isAdmin && !isOwner) {
|
|
5826
6392
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5827
6393
|
}
|
|
6394
|
+
if (inviteLimiter.check(String(req.user.id))) {
|
|
6395
|
+
return Response.json({ error: "Trop d'invitations. R\xE9essayez plus tard." }, { status: 429 });
|
|
6396
|
+
}
|
|
6397
|
+
const collabCount = await dbCount(payload, "ticket-collaborators", { where: { ticket: { equals: ticketId } }, overrideAccess: true }).catch(() => ({ totalDocs: 0 }));
|
|
6398
|
+
if (collabCount.totalDocs >= MAX_COLLABORATORS_PER_TICKET) {
|
|
6399
|
+
return Response.json(
|
|
6400
|
+
{ error: `Limite de ${MAX_COLLABORATORS_PER_TICKET} collaborateurs atteinte sur ce ticket` },
|
|
6401
|
+
{ status: 429 }
|
|
6402
|
+
);
|
|
6403
|
+
}
|
|
5828
6404
|
const ownerEmail = typeof ticket.client === "object" ? ticket.client?.email : void 0;
|
|
5829
6405
|
if (ownerEmail && ownerEmail.toLowerCase() === email) {
|
|
5830
6406
|
return Response.json({ error: "Ce client est d\xE9j\xE0 propri\xE9taire du ticket" }, { status: 400 });
|
|
5831
6407
|
}
|
|
5832
6408
|
let inviteeId = null;
|
|
5833
|
-
const existing = await payload.
|
|
5834
|
-
collection: slugs.supportClients,
|
|
6409
|
+
const existing = await dbFind(payload, slugs.supportClients, {
|
|
5835
6410
|
where: { email: { equals: email } },
|
|
5836
6411
|
limit: 1,
|
|
5837
6412
|
depth: 0,
|
|
@@ -5841,8 +6416,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5841
6416
|
inviteeId = existing.docs[0].id;
|
|
5842
6417
|
} else {
|
|
5843
6418
|
const tempPassword = randomBytes(16).toString("hex");
|
|
5844
|
-
const created = await payload.
|
|
5845
|
-
collection: slugs.supportClients,
|
|
6419
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
5846
6420
|
data: {
|
|
5847
6421
|
email,
|
|
5848
6422
|
firstName: "Invit\xE9",
|
|
@@ -5859,8 +6433,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5859
6433
|
if (!inviteeId) {
|
|
5860
6434
|
return Response.json({ error: "Impossible de cr\xE9er le client invit\xE9" }, { status: 500 });
|
|
5861
6435
|
}
|
|
5862
|
-
const dupe = await payload
|
|
5863
|
-
collection: "ticket-collaborators",
|
|
6436
|
+
const dupe = await dbFind(payload, "ticket-collaborators", {
|
|
5864
6437
|
where: {
|
|
5865
6438
|
and: [
|
|
5866
6439
|
{ ticket: { equals: ticketId } },
|
|
@@ -5873,15 +6446,13 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5873
6446
|
});
|
|
5874
6447
|
const invitationToken = randomBytes(24).toString("hex");
|
|
5875
6448
|
if (dupe.docs.length > 0) {
|
|
5876
|
-
await payload
|
|
5877
|
-
collection: "ticket-collaborators",
|
|
6449
|
+
await dbUpdate(payload, "ticket-collaborators", {
|
|
5878
6450
|
id: dupe.docs[0].id,
|
|
5879
6451
|
data: { role, invitationToken },
|
|
5880
6452
|
overrideAccess: true
|
|
5881
6453
|
});
|
|
5882
6454
|
} else {
|
|
5883
|
-
await payload
|
|
5884
|
-
collection: "ticket-collaborators",
|
|
6455
|
+
await dbCreate(payload, "ticket-collaborators", {
|
|
5885
6456
|
data: {
|
|
5886
6457
|
ticket: ticketId,
|
|
5887
6458
|
client: inviteeId,
|
|
@@ -5924,8 +6495,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5924
6495
|
)
|
|
5925
6496
|
});
|
|
5926
6497
|
try {
|
|
5927
|
-
await payload.
|
|
5928
|
-
collection: slugs.emailLogs,
|
|
6498
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5929
6499
|
data: {
|
|
5930
6500
|
status: "success",
|
|
5931
6501
|
action: "invite",
|
|
@@ -5978,6 +6548,7 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5978
6548
|
];
|
|
5979
6549
|
if (!f || f.ai !== false) {
|
|
5980
6550
|
endpoints.push(createAiEndpoint(slugs));
|
|
6551
|
+
endpoints.push(createAiAgentEndpoint(slugs));
|
|
5981
6552
|
endpoints.push(...createClientIntelligenceEndpoint(slugs));
|
|
5982
6553
|
endpoints.push(createTicketSynthesisEndpoint(slugs));
|
|
5983
6554
|
}
|
|
@@ -6008,21 +6579,28 @@ function createSupportEndpoints(slugs, options) {
|
|
|
6008
6579
|
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs));
|
|
6009
6580
|
endpoints.push(createAdminChatStreamEndpoint(slugs));
|
|
6010
6581
|
}
|
|
6011
|
-
if (!f || f.timeTracking !== false)
|
|
6582
|
+
if (!f || f.timeTracking !== false) {
|
|
6583
|
+
endpoints.push(createBillingEndpoint(slugs));
|
|
6584
|
+
endpoints.push(createInvoiceEndpoint(slugs));
|
|
6585
|
+
}
|
|
6012
6586
|
if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs));
|
|
6013
6587
|
if (!f || f.emailTracking !== false) {
|
|
6014
6588
|
endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs));
|
|
6015
6589
|
}
|
|
6016
6590
|
if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs));
|
|
6017
6591
|
if (!f || f.scheduledReplies !== false) endpoints.push(createProcessScheduledEndpoint(slugs));
|
|
6592
|
+
if (!f || f.snooze !== false) endpoints.push(createProcessSnoozeEndpoint(slugs));
|
|
6593
|
+
endpoints.push(createProcessDigestsEndpoint(slugs));
|
|
6594
|
+
endpoints.push(createChannelsWebhookEndpoint(slugs));
|
|
6595
|
+
endpoints.push(createVapidKeyEndpoint());
|
|
6596
|
+
endpoints.push(createPushSubscribeEndpoint(slugs));
|
|
6018
6597
|
return endpoints;
|
|
6019
6598
|
}
|
|
6020
6599
|
|
|
6021
6600
|
// src/utils/adminNotification.ts
|
|
6022
6601
|
async function createAdminNotification(payload, data, collectionSlug = "admin-notifications") {
|
|
6023
6602
|
try {
|
|
6024
|
-
await payload
|
|
6025
|
-
collection: collectionSlug,
|
|
6603
|
+
await dbCreate(payload, collectionSlug, {
|
|
6026
6604
|
data: {
|
|
6027
6605
|
title: data.title,
|
|
6028
6606
|
message: data.message,
|
|
@@ -6042,8 +6620,7 @@ function dispatchWebhook(data, event, payload, slugs) {
|
|
|
6042
6620
|
}
|
|
6043
6621
|
async function _dispatch(data, event, payload, slugs) {
|
|
6044
6622
|
try {
|
|
6045
|
-
const { docs: endpoints } = await payload.
|
|
6046
|
-
collection: slugs.webhookEndpoints,
|
|
6623
|
+
const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
|
|
6047
6624
|
where: {
|
|
6048
6625
|
and: [
|
|
6049
6626
|
{ active: { equals: true } },
|
|
@@ -6080,8 +6657,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
6080
6657
|
signal: AbortSignal.timeout(1e4)
|
|
6081
6658
|
// 10s timeout
|
|
6082
6659
|
});
|
|
6083
|
-
await payload.
|
|
6084
|
-
collection: slugs.webhookEndpoints,
|
|
6660
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6085
6661
|
id: endpoint.id,
|
|
6086
6662
|
data: {
|
|
6087
6663
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6095,8 +6671,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
6095
6671
|
} catch (err) {
|
|
6096
6672
|
console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
|
|
6097
6673
|
try {
|
|
6098
|
-
await payload.
|
|
6099
|
-
collection: slugs.webhookEndpoints,
|
|
6674
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6100
6675
|
id: endpoint.id,
|
|
6101
6676
|
data: {
|
|
6102
6677
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6330,8 +6905,7 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6330
6905
|
if (typeof policyRef === "object" && policyRef.firstResponseTime) {
|
|
6331
6906
|
return policyRef;
|
|
6332
6907
|
}
|
|
6333
|
-
return await payload.
|
|
6334
|
-
collection: slugs.slaPolicies,
|
|
6908
|
+
return await dbFindByID(payload, slugs.slaPolicies, {
|
|
6335
6909
|
id: policyId,
|
|
6336
6910
|
depth: 0,
|
|
6337
6911
|
overrideAccess: true
|
|
@@ -6341,9 +6915,22 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6341
6915
|
}
|
|
6342
6916
|
}
|
|
6343
6917
|
const priority = field(ticket, "priority") || "normal";
|
|
6918
|
+
const teamRef = field(ticket, "team");
|
|
6919
|
+
const teamId = typeof teamRef === "object" && teamRef !== null ? teamRef.id : teamRef;
|
|
6920
|
+
if (teamId) {
|
|
6921
|
+
try {
|
|
6922
|
+
const teamPolicies = await dbFind(payload, slugs.slaPolicies, {
|
|
6923
|
+
where: { and: [{ team: { equals: teamId } }, { priority: { equals: priority } }] },
|
|
6924
|
+
limit: 1,
|
|
6925
|
+
depth: 0,
|
|
6926
|
+
overrideAccess: true
|
|
6927
|
+
});
|
|
6928
|
+
if (teamPolicies.docs.length > 0) return teamPolicies.docs[0];
|
|
6929
|
+
} catch {
|
|
6930
|
+
}
|
|
6931
|
+
}
|
|
6344
6932
|
try {
|
|
6345
|
-
const defaults = await payload.
|
|
6346
|
-
collection: slugs.slaPolicies,
|
|
6933
|
+
const defaults = await dbFind(payload, slugs.slaPolicies, {
|
|
6347
6934
|
where: {
|
|
6348
6935
|
and: [
|
|
6349
6936
|
{ isDefault: { equals: true } },
|
|
@@ -6380,8 +6967,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6380
6967
|
if (typeof policyRef === "object" && policyRef.escalateOnBreach !== void 0) {
|
|
6381
6968
|
policy = policyRef;
|
|
6382
6969
|
} else {
|
|
6383
|
-
policy = await payload.
|
|
6384
|
-
collection: slugs.slaPolicies,
|
|
6970
|
+
policy = await dbFindByID(payload, slugs.slaPolicies, {
|
|
6385
6971
|
id: policyId,
|
|
6386
6972
|
depth: 0,
|
|
6387
6973
|
overrideAccess: true
|
|
@@ -6394,8 +6980,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6394
6980
|
try {
|
|
6395
6981
|
const escalateToId = typeof policy.escalateTo === "object" ? policy.escalateTo.id : policy.escalateTo;
|
|
6396
6982
|
if (!escalateToId) return;
|
|
6397
|
-
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await payload.
|
|
6398
|
-
collection: slugs.users,
|
|
6983
|
+
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await dbFindByID(payload, slugs.users, {
|
|
6399
6984
|
id: escalateToId,
|
|
6400
6985
|
depth: 0,
|
|
6401
6986
|
overrideAccess: true
|
|
@@ -6443,8 +7028,7 @@ function createAssignSlaDeadlines(slugs, notificationSlug = "admin-notifications
|
|
|
6443
7028
|
if (!field(doc, "slaPolicy") && policy.id) {
|
|
6444
7029
|
updateData.slaPolicy = policy.id;
|
|
6445
7030
|
}
|
|
6446
|
-
await payload.
|
|
6447
|
-
collection: slugs.tickets,
|
|
7031
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6448
7032
|
id: doc.id,
|
|
6449
7033
|
data: updateData,
|
|
6450
7034
|
overrideAccess: true
|
|
@@ -6467,8 +7051,7 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6467
7051
|
const now = /* @__PURE__ */ new Date();
|
|
6468
7052
|
const deadline = new Date(doc.slaResolutionDue);
|
|
6469
7053
|
const breached = now > deadline;
|
|
6470
|
-
await payload.
|
|
6471
|
-
collection: slugs.tickets,
|
|
7054
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6472
7055
|
id: doc.id,
|
|
6473
7056
|
data: { slaResolutionBreached: breached },
|
|
6474
7057
|
overrideAccess: true
|
|
@@ -6482,6 +7065,35 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6482
7065
|
return doc;
|
|
6483
7066
|
};
|
|
6484
7067
|
}
|
|
7068
|
+
function createPauseSlaOnHold(slugs) {
|
|
7069
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
7070
|
+
if (operation !== "update" || !previousDoc) return doc;
|
|
7071
|
+
if (previousDoc.status === doc.status) return doc;
|
|
7072
|
+
if (!doc.slaResolutionDue) return doc;
|
|
7073
|
+
const wasWaiting = previousDoc.status === "waiting_client";
|
|
7074
|
+
const isWaiting = doc.status === "waiting_client";
|
|
7075
|
+
try {
|
|
7076
|
+
if (!wasWaiting && isWaiting && !doc.slaPausedAt && doc.status !== "resolved") {
|
|
7077
|
+
await req.payload.update({
|
|
7078
|
+
collection: slugs.tickets,
|
|
7079
|
+
id: doc.id,
|
|
7080
|
+
data: { slaPausedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
7081
|
+
overrideAccess: true
|
|
7082
|
+
});
|
|
7083
|
+
} else if (wasWaiting && !isWaiting && doc.slaPausedAt) {
|
|
7084
|
+
const pausedMs = Date.now() - new Date(doc.slaPausedAt).getTime();
|
|
7085
|
+
const data = { slaPausedAt: null };
|
|
7086
|
+
if (pausedMs > 0) {
|
|
7087
|
+
data.slaResolutionDue = new Date(new Date(doc.slaResolutionDue).getTime() + pausedMs).toISOString();
|
|
7088
|
+
}
|
|
7089
|
+
await req.payload.update({ collection: slugs.tickets, id: doc.id, data, overrideAccess: true });
|
|
7090
|
+
}
|
|
7091
|
+
} catch (err) {
|
|
7092
|
+
console.error("[sla] Failed to pause/resume SLA on hold:", err);
|
|
7093
|
+
}
|
|
7094
|
+
return doc;
|
|
7095
|
+
};
|
|
7096
|
+
}
|
|
6485
7097
|
function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications") {
|
|
6486
7098
|
return async ({ doc, operation, req }) => {
|
|
6487
7099
|
if (operation !== "create") return doc;
|
|
@@ -6489,8 +7101,7 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6489
7101
|
try {
|
|
6490
7102
|
const { payload } = req;
|
|
6491
7103
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
6492
|
-
const ticket = await payload.
|
|
6493
|
-
collection: slugs.tickets,
|
|
7104
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
6494
7105
|
id: ticketId,
|
|
6495
7106
|
depth: 1,
|
|
6496
7107
|
overrideAccess: true
|
|
@@ -6501,22 +7112,123 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6501
7112
|
const now = /* @__PURE__ */ new Date();
|
|
6502
7113
|
const deadline = new Date(ticket.slaFirstResponseDue);
|
|
6503
7114
|
const breached = now > deadline;
|
|
6504
|
-
await payload.
|
|
6505
|
-
collection: slugs.tickets,
|
|
7115
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6506
7116
|
id: ticketId,
|
|
6507
7117
|
data: { slaFirstResponseBreached: breached },
|
|
6508
7118
|
overrideAccess: true
|
|
6509
7119
|
});
|
|
6510
|
-
if (breached) {
|
|
6511
|
-
await handleSlaBreach(payload, ticket, "first_response", slugs, notificationSlug);
|
|
7120
|
+
if (breached) {
|
|
7121
|
+
await handleSlaBreach(payload, ticket, "first_response", slugs, notificationSlug);
|
|
7122
|
+
}
|
|
7123
|
+
} catch (err) {
|
|
7124
|
+
console.error("[sla] Failed to check SLA on reply:", err);
|
|
7125
|
+
}
|
|
7126
|
+
return doc;
|
|
7127
|
+
};
|
|
7128
|
+
}
|
|
7129
|
+
|
|
7130
|
+
// src/hooks/automationRules.ts
|
|
7131
|
+
function evaluateConditions(doc, rule) {
|
|
7132
|
+
const conds = Array.isArray(rule.conditions) ? rule.conditions : [];
|
|
7133
|
+
if (conds.length === 0) return true;
|
|
7134
|
+
const results = conds.map((c) => {
|
|
7135
|
+
const fieldVal = String(doc[c.field] ?? "");
|
|
7136
|
+
const v = String(c.value ?? "");
|
|
7137
|
+
switch (c.operator) {
|
|
7138
|
+
case "equals":
|
|
7139
|
+
return fieldVal === v;
|
|
7140
|
+
case "not_equals":
|
|
7141
|
+
return fieldVal !== v;
|
|
7142
|
+
case "contains":
|
|
7143
|
+
return fieldVal.toLowerCase().includes(v.toLowerCase());
|
|
7144
|
+
default:
|
|
7145
|
+
return false;
|
|
7146
|
+
}
|
|
7147
|
+
});
|
|
7148
|
+
return rule.matchType === "any" ? results.some(Boolean) : results.every(Boolean);
|
|
7149
|
+
}
|
|
7150
|
+
function createApplyAutomationRules(slugs) {
|
|
7151
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
7152
|
+
if (req.context?.skipAutomation) return doc;
|
|
7153
|
+
const events = [];
|
|
7154
|
+
if (operation === "create") events.push("ticket_created");
|
|
7155
|
+
if (operation === "update") {
|
|
7156
|
+
events.push("ticket_updated");
|
|
7157
|
+
if (previousDoc && previousDoc.status !== doc.status) events.push("ticket_status_changed");
|
|
7158
|
+
}
|
|
7159
|
+
if (events.length === 0) return doc;
|
|
7160
|
+
try {
|
|
7161
|
+
const rules = await dbFind(req.payload, slugs.automationRules, {
|
|
7162
|
+
where: { and: [{ enabled: { equals: true } }, { event: { in: events } }] },
|
|
7163
|
+
sort: "order",
|
|
7164
|
+
limit: 200,
|
|
7165
|
+
depth: 0,
|
|
7166
|
+
overrideAccess: true
|
|
7167
|
+
});
|
|
7168
|
+
const changes = {};
|
|
7169
|
+
const tags = Array.isArray(doc.tags) ? [...doc.tags] : [];
|
|
7170
|
+
let tagsTouched = false;
|
|
7171
|
+
for (const rule of rules.docs) {
|
|
7172
|
+
if (!evaluateConditions(doc, rule)) continue;
|
|
7173
|
+
for (const a of Array.isArray(rule.actions) ? rule.actions : []) {
|
|
7174
|
+
if (!a.value) continue;
|
|
7175
|
+
switch (a.type) {
|
|
7176
|
+
case "set_status":
|
|
7177
|
+
changes.status = a.value;
|
|
7178
|
+
break;
|
|
7179
|
+
case "set_priority":
|
|
7180
|
+
changes.priority = a.value;
|
|
7181
|
+
break;
|
|
7182
|
+
case "set_category":
|
|
7183
|
+
changes.category = a.value;
|
|
7184
|
+
break;
|
|
7185
|
+
case "assign":
|
|
7186
|
+
changes.assignedTo = a.value;
|
|
7187
|
+
break;
|
|
7188
|
+
case "add_tag":
|
|
7189
|
+
if (!tags.includes(a.value)) {
|
|
7190
|
+
tags.push(a.value);
|
|
7191
|
+
tagsTouched = true;
|
|
7192
|
+
}
|
|
7193
|
+
break;
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
7196
|
+
}
|
|
7197
|
+
if (tagsTouched) changes.tags = tags;
|
|
7198
|
+
if (Object.keys(changes).length > 0) {
|
|
7199
|
+
await req.payload.update({
|
|
7200
|
+
collection: slugs.tickets,
|
|
7201
|
+
id: doc.id,
|
|
7202
|
+
data: changes,
|
|
7203
|
+
overrideAccess: true,
|
|
7204
|
+
context: { skipAutomation: true }
|
|
7205
|
+
});
|
|
6512
7206
|
}
|
|
6513
7207
|
} catch (err) {
|
|
6514
|
-
console.error("[
|
|
7208
|
+
console.error("[support] Failed to apply automation rules:", err);
|
|
6515
7209
|
}
|
|
6516
7210
|
return doc;
|
|
6517
7211
|
};
|
|
6518
7212
|
}
|
|
6519
7213
|
|
|
7214
|
+
// src/utils/teamAccess.ts
|
|
7215
|
+
function isTeamScopingEnabled() {
|
|
7216
|
+
return process.env.SUPPORT_TEAM_SCOPING === "1" || process.env.SUPPORT_TEAM_SCOPING === "true";
|
|
7217
|
+
}
|
|
7218
|
+
async function resolveAgentTeamIds(payload, slugs, userId) {
|
|
7219
|
+
try {
|
|
7220
|
+
const teams = await dbFind(payload, slugs.supportTeams, {
|
|
7221
|
+
where: { members: { in: [userId] } },
|
|
7222
|
+
limit: 200,
|
|
7223
|
+
depth: 0,
|
|
7224
|
+
overrideAccess: true
|
|
7225
|
+
});
|
|
7226
|
+
return teams.docs.map((t) => t.id);
|
|
7227
|
+
} catch {
|
|
7228
|
+
return [];
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
|
|
6520
7232
|
// src/collections/Tickets.ts
|
|
6521
7233
|
function createAssignTicketNumber(slugs) {
|
|
6522
7234
|
return async ({ data, operation, req }) => {
|
|
@@ -6779,56 +7491,6 @@ function createNotifyClientOnResolve(slugs) {
|
|
|
6779
7491
|
return doc;
|
|
6780
7492
|
};
|
|
6781
7493
|
}
|
|
6782
|
-
function createAutoCalculateSLA(slugs) {
|
|
6783
|
-
return async ({ doc, operation, req }) => {
|
|
6784
|
-
if (operation !== "create") return doc;
|
|
6785
|
-
try {
|
|
6786
|
-
const { payload } = req;
|
|
6787
|
-
const ticketPriority = doc.priority || "normal";
|
|
6788
|
-
let policy = null;
|
|
6789
|
-
const byPriority = await payload.find({
|
|
6790
|
-
collection: slugs.slaPolicies,
|
|
6791
|
-
where: { priority: { equals: ticketPriority } },
|
|
6792
|
-
limit: 1,
|
|
6793
|
-
depth: 0,
|
|
6794
|
-
overrideAccess: true
|
|
6795
|
-
});
|
|
6796
|
-
if (byPriority.docs.length > 0) {
|
|
6797
|
-
policy = byPriority.docs[0];
|
|
6798
|
-
} else {
|
|
6799
|
-
const defaults = await payload.find({
|
|
6800
|
-
collection: slugs.slaPolicies,
|
|
6801
|
-
where: { isDefault: { equals: true } },
|
|
6802
|
-
limit: 1,
|
|
6803
|
-
depth: 0,
|
|
6804
|
-
overrideAccess: true
|
|
6805
|
-
});
|
|
6806
|
-
if (defaults.docs.length > 0) {
|
|
6807
|
-
policy = defaults.docs[0];
|
|
6808
|
-
}
|
|
6809
|
-
}
|
|
6810
|
-
if (!policy) return doc;
|
|
6811
|
-
const now = /* @__PURE__ */ new Date();
|
|
6812
|
-
const firstResponseMinutes = policy.firstResponseTime || 240;
|
|
6813
|
-
const resolutionMinutes = policy.resolutionTime || 1440;
|
|
6814
|
-
const firstResponseDue = new Date(now.getTime() + firstResponseMinutes * 6e4);
|
|
6815
|
-
const resolutionDue = new Date(now.getTime() + resolutionMinutes * 6e4);
|
|
6816
|
-
await payload.update({
|
|
6817
|
-
collection: slugs.tickets,
|
|
6818
|
-
id: doc.id,
|
|
6819
|
-
data: {
|
|
6820
|
-
slaPolicy: policy.id,
|
|
6821
|
-
slaFirstResponseDue: firstResponseDue.toISOString(),
|
|
6822
|
-
slaResolutionDue: resolutionDue.toISOString()
|
|
6823
|
-
},
|
|
6824
|
-
overrideAccess: true
|
|
6825
|
-
});
|
|
6826
|
-
} catch (err) {
|
|
6827
|
-
console.error("[support] Failed to auto-calculate SLA:", err);
|
|
6828
|
-
}
|
|
6829
|
-
return doc;
|
|
6830
|
-
};
|
|
6831
|
-
}
|
|
6832
7494
|
function createFireTicketWebhooks(slugs) {
|
|
6833
7495
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
6834
7496
|
const { payload } = req;
|
|
@@ -6920,8 +7582,7 @@ function createCascadeDelete(slugs) {
|
|
|
6920
7582
|
return async ({ id, req }) => {
|
|
6921
7583
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys];
|
|
6922
7584
|
for (const slug of collections) {
|
|
6923
|
-
await req.payload
|
|
6924
|
-
collection: slug,
|
|
7585
|
+
await dbDelete(req.payload, slug, {
|
|
6925
7586
|
where: { ticket: { equals: id } },
|
|
6926
7587
|
overrideAccess: true
|
|
6927
7588
|
});
|
|
@@ -7046,6 +7707,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7046
7707
|
{
|
|
7047
7708
|
name: "status",
|
|
7048
7709
|
type: "select",
|
|
7710
|
+
index: true,
|
|
7049
7711
|
defaultValue: "open",
|
|
7050
7712
|
label: "Statut",
|
|
7051
7713
|
options: [
|
|
@@ -7060,6 +7722,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7060
7722
|
{
|
|
7061
7723
|
name: "priority",
|
|
7062
7724
|
type: "select",
|
|
7725
|
+
index: true,
|
|
7063
7726
|
defaultValue: "normal",
|
|
7064
7727
|
label: "Priorite",
|
|
7065
7728
|
options: [
|
|
@@ -7075,7 +7738,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7075
7738
|
{
|
|
7076
7739
|
type: "row",
|
|
7077
7740
|
fields: [
|
|
7078
|
-
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, label: "Client", admin: { width: "50%" } }
|
|
7741
|
+
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, index: true, label: "Client", admin: { width: "50%" } }
|
|
7079
7742
|
]
|
|
7080
7743
|
},
|
|
7081
7744
|
// Billing collapsible
|
|
@@ -7144,15 +7807,16 @@ function createTicketsCollection(slugs, options) {
|
|
|
7144
7807
|
{
|
|
7145
7808
|
type: "row",
|
|
7146
7809
|
fields: [
|
|
7147
|
-
{ name: "slaFirstResponseDue", type: "date", label: "Echeance 1ere reponse (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7148
|
-
{ name: "slaResolutionDue", type: "date", label: "Echeance resolution (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
7810
|
+
{ name: "slaFirstResponseDue", type: "date", index: true, label: "Echeance 1ere reponse (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7811
|
+
{ name: "slaResolutionDue", type: "date", index: true, label: "Echeance resolution (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
7149
7812
|
]
|
|
7150
7813
|
},
|
|
7151
7814
|
{
|
|
7152
7815
|
type: "row",
|
|
7153
7816
|
fields: [
|
|
7154
7817
|
{ name: "slaFirstResponseBreached", type: "checkbox", label: "1ere reponse depassee", admin: { readOnly: true, width: "50%" } },
|
|
7155
|
-
{ name: "slaResolutionBreached", type: "checkbox", label: "Resolution depassee", admin: { readOnly: true, width: "50%" } }
|
|
7818
|
+
{ name: "slaResolutionBreached", type: "checkbox", label: "Resolution depassee", admin: { readOnly: true, width: "50%" } },
|
|
7819
|
+
{ name: "slaPausedAt", type: "date", label: "SLA en pause depuis", admin: { hidden: true } }
|
|
7156
7820
|
]
|
|
7157
7821
|
}
|
|
7158
7822
|
]
|
|
@@ -7174,7 +7838,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7174
7838
|
},
|
|
7175
7839
|
{ name: "mergedInto", type: "relationship", relationTo: slugs.tickets, label: "Fusionne dans", admin: { readOnly: true } },
|
|
7176
7840
|
{ name: "autoCloseRemindedAt", type: "date", label: "Rappel auto-close envoye", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7177
|
-
{ 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." } }
|
|
7841
|
+
{ 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." } }
|
|
7178
7842
|
]
|
|
7179
7843
|
},
|
|
7180
7844
|
// Sidebar
|
|
@@ -7183,6 +7847,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7183
7847
|
{
|
|
7184
7848
|
name: "category",
|
|
7185
7849
|
type: "select",
|
|
7850
|
+
index: true,
|
|
7186
7851
|
label: "Categorie",
|
|
7187
7852
|
options: [
|
|
7188
7853
|
{ label: "Bug / Dysfonctionnement", value: "bug" },
|
|
@@ -7193,7 +7858,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7193
7858
|
],
|
|
7194
7859
|
admin: { position: "sidebar" }
|
|
7195
7860
|
},
|
|
7196
|
-
{ name: "assignedTo", type: "relationship", relationTo: slugs.users, label: "Assigne a", admin: { position: "sidebar" } },
|
|
7861
|
+
{ name: "assignedTo", type: "relationship", relationTo: slugs.users, index: true, label: "Assigne a", admin: { position: "sidebar" } },
|
|
7862
|
+
{ name: "team", type: "relationship", relationTo: slugs.supportTeams, index: true, label: "Equipe", admin: { position: "sidebar" } },
|
|
7197
7863
|
{
|
|
7198
7864
|
name: "source",
|
|
7199
7865
|
type: "select",
|
|
@@ -7203,7 +7869,9 @@ function createTicketsCollection(slugs, options) {
|
|
|
7203
7869
|
{ label: "Portail client", value: "portal" },
|
|
7204
7870
|
{ label: "Chat en direct", value: "live-chat" },
|
|
7205
7871
|
{ label: "Email", value: "email" },
|
|
7206
|
-
{ label: "Admin", value: "admin" }
|
|
7872
|
+
{ label: "Admin", value: "admin" },
|
|
7873
|
+
{ label: "WhatsApp", value: "whatsapp" },
|
|
7874
|
+
{ label: "Messenger", value: "messenger" }
|
|
7207
7875
|
],
|
|
7208
7876
|
admin: { position: "sidebar" }
|
|
7209
7877
|
},
|
|
@@ -7254,8 +7922,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7254
7922
|
afterChange: [
|
|
7255
7923
|
createTrackSLA(slugs),
|
|
7256
7924
|
createTrackAiSummaryOnResolve(slugs),
|
|
7257
|
-
createAutoCalculateSLA(slugs),
|
|
7258
7925
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
7926
|
+
createPauseSlaOnHold(slugs),
|
|
7259
7927
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
7260
7928
|
createLogTicketActivity(slugs),
|
|
7261
7929
|
createNotifyOnAssignment(slugs),
|
|
@@ -7263,7 +7931,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7263
7931
|
createTicketStatusEmail(slugs),
|
|
7264
7932
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
7265
7933
|
createFireTicketWebhooks(slugs),
|
|
7266
|
-
createDispatchWebhookOnTicket(slugs)
|
|
7934
|
+
createDispatchWebhookOnTicket(slugs),
|
|
7935
|
+
createApplyAutomationRules(slugs)
|
|
7267
7936
|
],
|
|
7268
7937
|
beforeDelete: [createCascadeDelete(slugs)]
|
|
7269
7938
|
},
|
|
@@ -7274,12 +7943,20 @@ function createTicketsCollection(slugs, options) {
|
|
|
7274
7943
|
return false;
|
|
7275
7944
|
},
|
|
7276
7945
|
read: async ({ req }) => {
|
|
7277
|
-
if (req.user?.collection === slugs.users)
|
|
7946
|
+
if (req.user?.collection === slugs.users) {
|
|
7947
|
+
if (!isTeamScopingEnabled()) return true;
|
|
7948
|
+
const teamIds = await resolveAgentTeamIds(req.payload, slugs, req.user.id);
|
|
7949
|
+
return {
|
|
7950
|
+
or: [
|
|
7951
|
+
{ team: { in: teamIds.length > 0 ? teamIds : [-1] } },
|
|
7952
|
+
{ team: { exists: false } }
|
|
7953
|
+
]
|
|
7954
|
+
};
|
|
7955
|
+
}
|
|
7278
7956
|
if (req.user?.collection === slugs.supportClients) {
|
|
7279
7957
|
let collabTicketIds = [];
|
|
7280
7958
|
try {
|
|
7281
|
-
const rows = await req.payload
|
|
7282
|
-
collection: "ticket-collaborators",
|
|
7959
|
+
const rows = await dbFind(req.payload, "ticket-collaborators", {
|
|
7283
7960
|
where: { client: { equals: req.user.id } },
|
|
7284
7961
|
limit: 1e3,
|
|
7285
7962
|
depth: 0,
|
|
@@ -7314,6 +7991,125 @@ function createTicketsCollection(slugs, options) {
|
|
|
7314
7991
|
};
|
|
7315
7992
|
}
|
|
7316
7993
|
|
|
7994
|
+
// src/utils/ticketAccess.ts
|
|
7995
|
+
async function resolveAccessibleTicketIds(payload, slugs, clientId) {
|
|
7996
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7997
|
+
try {
|
|
7998
|
+
const owned = await dbFind(payload, slugs.tickets, {
|
|
7999
|
+
where: { client: { equals: clientId } },
|
|
8000
|
+
limit: 5e3,
|
|
8001
|
+
depth: 0,
|
|
8002
|
+
overrideAccess: true
|
|
8003
|
+
});
|
|
8004
|
+
for (const t of owned.docs) ids.add(t.id);
|
|
8005
|
+
} catch {
|
|
8006
|
+
}
|
|
8007
|
+
try {
|
|
8008
|
+
const collab = await dbFind(payload, "ticket-collaborators", {
|
|
8009
|
+
where: { client: { equals: clientId } },
|
|
8010
|
+
limit: 1e3,
|
|
8011
|
+
depth: 0,
|
|
8012
|
+
overrideAccess: true
|
|
8013
|
+
});
|
|
8014
|
+
for (const r of collab.docs) {
|
|
8015
|
+
const row = r;
|
|
8016
|
+
const tid = typeof row.ticket === "object" ? row.ticket?.id : row.ticket;
|
|
8017
|
+
if (tid !== void 0 && tid !== null) ids.add(tid);
|
|
8018
|
+
}
|
|
8019
|
+
} catch {
|
|
8020
|
+
}
|
|
8021
|
+
return Array.from(ids);
|
|
8022
|
+
}
|
|
8023
|
+
var OPTIONS = {
|
|
8024
|
+
allowedTags: [
|
|
8025
|
+
"p",
|
|
8026
|
+
"br",
|
|
8027
|
+
"b",
|
|
8028
|
+
"strong",
|
|
8029
|
+
"i",
|
|
8030
|
+
"em",
|
|
8031
|
+
"u",
|
|
8032
|
+
"s",
|
|
8033
|
+
"strike",
|
|
8034
|
+
"del",
|
|
8035
|
+
"ins",
|
|
8036
|
+
"mark",
|
|
8037
|
+
"a",
|
|
8038
|
+
"ul",
|
|
8039
|
+
"ol",
|
|
8040
|
+
"li",
|
|
8041
|
+
"blockquote",
|
|
8042
|
+
"code",
|
|
8043
|
+
"pre",
|
|
8044
|
+
"kbd",
|
|
8045
|
+
"span",
|
|
8046
|
+
"div",
|
|
8047
|
+
"h1",
|
|
8048
|
+
"h2",
|
|
8049
|
+
"h3",
|
|
8050
|
+
"h4",
|
|
8051
|
+
"h5",
|
|
8052
|
+
"h6",
|
|
8053
|
+
"hr",
|
|
8054
|
+
"img",
|
|
8055
|
+
"table",
|
|
8056
|
+
"thead",
|
|
8057
|
+
"tbody",
|
|
8058
|
+
"tr",
|
|
8059
|
+
"th",
|
|
8060
|
+
"td"
|
|
8061
|
+
],
|
|
8062
|
+
allowedAttributes: {
|
|
8063
|
+
a: ["href", "title", "target", "rel"],
|
|
8064
|
+
img: ["src", "alt", "title", "width", "height"],
|
|
8065
|
+
code: ["class"],
|
|
8066
|
+
pre: ["class"],
|
|
8067
|
+
span: ["class"],
|
|
8068
|
+
"*": ["style"]
|
|
8069
|
+
},
|
|
8070
|
+
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
8071
|
+
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
|
8072
|
+
allowProtocolRelative: false,
|
|
8073
|
+
disallowedTagsMode: "discard",
|
|
8074
|
+
// Force safe rel on links opening a new tab (defense against tabnabbing).
|
|
8075
|
+
transformTags: {
|
|
8076
|
+
a: sanitizeHtml.simpleTransform("a", { rel: "noopener noreferrer nofollow" }, true)
|
|
8077
|
+
},
|
|
8078
|
+
// Only allow CSS that cannot carry a script payload.
|
|
8079
|
+
allowedStyles: {
|
|
8080
|
+
"*": {
|
|
8081
|
+
color: [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}\s*\)$/i, /^[a-z-]+$/i],
|
|
8082
|
+
"background-color": [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}\s*\)$/i, /^[a-z-]+$/i],
|
|
8083
|
+
"text-align": [/^(left|right|center|justify)$/],
|
|
8084
|
+
"font-weight": [/^(normal|bold|[1-9]00)$/],
|
|
8085
|
+
"font-style": [/^(normal|italic)$/],
|
|
8086
|
+
"text-decoration": [/^(underline|line-through|none)$/]
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
};
|
|
8090
|
+
function sanitizeMessageHtml(html) {
|
|
8091
|
+
if (!html) return html;
|
|
8092
|
+
return sanitizeHtml(html, OPTIONS);
|
|
8093
|
+
}
|
|
8094
|
+
|
|
8095
|
+
// src/utils/notificationQueue.ts
|
|
8096
|
+
async function queueClientNotification(payload, slugs, item) {
|
|
8097
|
+
try {
|
|
8098
|
+
await dbCreate(payload, slugs.notificationQueue, {
|
|
8099
|
+
data: {
|
|
8100
|
+
client: item.client,
|
|
8101
|
+
type: item.type,
|
|
8102
|
+
title: item.title,
|
|
8103
|
+
message: item.message,
|
|
8104
|
+
...item.ticket ? { ticket: item.ticket } : {}
|
|
8105
|
+
},
|
|
8106
|
+
overrideAccess: true
|
|
8107
|
+
});
|
|
8108
|
+
} catch (err) {
|
|
8109
|
+
console.error("[support] Failed to queue client notification:", err);
|
|
8110
|
+
}
|
|
8111
|
+
}
|
|
8112
|
+
|
|
7317
8113
|
// src/collections/TicketMessages.ts
|
|
7318
8114
|
function createAssignAuthor(slugs) {
|
|
7319
8115
|
return async ({ data, operation, req }) => {
|
|
@@ -7376,29 +8172,46 @@ function createNotifyClient(slugs) {
|
|
|
7376
8172
|
const internalBcc = process.env.SUPPORT_CLIENT_BCC || void 0;
|
|
7377
8173
|
const rawContent = doc.bodyHtml ? emailRichContent(doc.bodyHtml) : emailQuote(doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body);
|
|
7378
8174
|
const replyPreview = (doc.body || "").replace(/\s+/g, " ").trim().slice(0, 90);
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
8175
|
+
const freq = client.notificationFrequency;
|
|
8176
|
+
if (freq === "daily" || freq === "weekly") {
|
|
8177
|
+
await queueClientNotification(req.payload, slugs, {
|
|
8178
|
+
client: client.id,
|
|
8179
|
+
type: "reply",
|
|
8180
|
+
title: `Nouvelle reponse \u2014 ${ticketNumber}`,
|
|
8181
|
+
message: replyPreview || `Reponse a votre ticket ${ticketNumber}`,
|
|
8182
|
+
ticket: ticketId
|
|
8183
|
+
});
|
|
8184
|
+
return doc;
|
|
8185
|
+
}
|
|
8186
|
+
void (async () => {
|
|
8187
|
+
try {
|
|
8188
|
+
await req.payload.sendEmail({
|
|
8189
|
+
to: client.email,
|
|
8190
|
+
...supportEmail ? { replyTo: supportEmail } : {},
|
|
8191
|
+
...internalBcc ? { bcc: internalBcc } : {},
|
|
8192
|
+
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
8193
|
+
html: emailWrapper(`Nouvelle reponse \u2014 ${ticketNumber}`, [
|
|
8194
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
|
|
8195
|
+
emailParagraph(`Notre equipe a apporte une reponse a votre ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(subject)}</em>.`),
|
|
8196
|
+
rawContent,
|
|
8197
|
+
emailButton("Consulter le ticket", portalUrl),
|
|
8198
|
+
emailParagraph('<span style="font-size: 13px; color: #6b7280;">Vous pouvez egalement repondre directement a cet email. Votre message sera automatiquement ajoute au ticket.</span>'),
|
|
8199
|
+
emailTrackingPixel(ticketId, doc.id)
|
|
8200
|
+
].join(""), {
|
|
8201
|
+
kind: "reply",
|
|
8202
|
+
preheader: replyPreview ? `${replyPreview}\u2026` : `Nouvelle reponse a votre ticket ${ticketNumber}`
|
|
8203
|
+
})
|
|
8204
|
+
});
|
|
8205
|
+
await req.payload.update({
|
|
8206
|
+
collection: slugs.ticketMessages,
|
|
8207
|
+
id: doc.id,
|
|
8208
|
+
data: { emailSentAt: (/* @__PURE__ */ new Date()).toISOString(), emailSentTo: client.email },
|
|
8209
|
+
overrideAccess: true
|
|
8210
|
+
});
|
|
8211
|
+
} catch (err) {
|
|
8212
|
+
console.error("[support] Failed to notify client (async):", err);
|
|
8213
|
+
}
|
|
8214
|
+
})();
|
|
7402
8215
|
} catch (err) {
|
|
7403
8216
|
console.error("[support] Failed to notify client:", err);
|
|
7404
8217
|
}
|
|
@@ -7436,8 +8249,7 @@ function createSyncTicketReplyToChat(slugs) {
|
|
|
7436
8249
|
if (!ticket?.chatSession) return doc;
|
|
7437
8250
|
if (doc.skipNotification) return doc;
|
|
7438
8251
|
const clientId = typeof ticket.client === "object" ? ticket.client.id : ticket.client;
|
|
7439
|
-
await payload.
|
|
7440
|
-
collection: slugs.chatMessages,
|
|
8252
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
7441
8253
|
data: {
|
|
7442
8254
|
session: ticket.chatSession,
|
|
7443
8255
|
client: clientId,
|
|
@@ -7499,6 +8311,15 @@ function createNotifyAdminOnClientMessage(slugs, notificationSlug) {
|
|
|
7499
8311
|
}
|
|
7500
8312
|
const preview = doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body;
|
|
7501
8313
|
const headerTitle = isNewTicket ? `Nouveau ticket ${ticketNumber}` : `Nouveau message \u2014 ${ticketNumber}`;
|
|
8314
|
+
const assignedId = assignedAdmin?.id ?? (ticket.assignedTo && typeof ticket.assignedTo !== "object" ? ticket.assignedTo : void 0);
|
|
8315
|
+
if (assignedId) {
|
|
8316
|
+
void sendPushToUser(payload, slugs, assignedId, {
|
|
8317
|
+
title: headerTitle,
|
|
8318
|
+
body: `${clientName}: ${preview || subject}`,
|
|
8319
|
+
url: `/admin/collections/${slugs.tickets}/${ticketId}`
|
|
8320
|
+
}).catch(() => {
|
|
8321
|
+
});
|
|
8322
|
+
}
|
|
7502
8323
|
if (primaryEmail) {
|
|
7503
8324
|
await payload.sendEmail({
|
|
7504
8325
|
// INTERNAL email (to the support team) — recipients are the contact
|
|
@@ -7554,6 +8375,57 @@ function createDispatchWebhookOnReply(slugs) {
|
|
|
7554
8375
|
return doc;
|
|
7555
8376
|
};
|
|
7556
8377
|
}
|
|
8378
|
+
function createSanitizeMessageHtml() {
|
|
8379
|
+
return ({ data }) => {
|
|
8380
|
+
if (data && typeof data.bodyHtml === "string" && data.bodyHtml) {
|
|
8381
|
+
data.bodyHtml = sanitizeMessageHtml(data.bodyHtml);
|
|
8382
|
+
}
|
|
8383
|
+
return data;
|
|
8384
|
+
};
|
|
8385
|
+
}
|
|
8386
|
+
var MENTION_RE = /@([^\s@]+@[^\s@]+\.[^\s@]+)/g;
|
|
8387
|
+
function createResolveMentions(slugs) {
|
|
8388
|
+
return async ({ data, operation, req }) => {
|
|
8389
|
+
if (operation !== "create") return data;
|
|
8390
|
+
const body = typeof data?.body === "string" ? data.body : "";
|
|
8391
|
+
const emails = Array.from(new Set([...body.matchAll(MENTION_RE)].map((m) => m[1].toLowerCase())));
|
|
8392
|
+
if (emails.length === 0) return data;
|
|
8393
|
+
try {
|
|
8394
|
+
const users = await dbFind(req.payload, slugs.users, { where: { email: { in: emails } }, limit: 50, depth: 0, overrideAccess: true });
|
|
8395
|
+
const ids = users.docs.map((u) => u.id);
|
|
8396
|
+
if (ids.length > 0) data.mentions = ids;
|
|
8397
|
+
} catch {
|
|
8398
|
+
}
|
|
8399
|
+
return data;
|
|
8400
|
+
};
|
|
8401
|
+
}
|
|
8402
|
+
function createNotifyMentions(slugs) {
|
|
8403
|
+
return async ({ doc, operation, req }) => {
|
|
8404
|
+
if (operation !== "create") return doc;
|
|
8405
|
+
const mentions = Array.isArray(doc.mentions) ? doc.mentions : [];
|
|
8406
|
+
if (mentions.length === 0) return doc;
|
|
8407
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8408
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
8409
|
+
for (const m of mentions) {
|
|
8410
|
+
const userId = typeof m === "object" ? m.id : m;
|
|
8411
|
+
try {
|
|
8412
|
+
const user = await dbFindByID(req.payload, slugs.users, { id: userId, depth: 0, overrideAccess: true });
|
|
8413
|
+
if (!user?.email) continue;
|
|
8414
|
+
await req.payload.sendEmail({
|
|
8415
|
+
to: user.email,
|
|
8416
|
+
subject: `Vous avez ete mentionne \u2014 ticket #${ticketId}`,
|
|
8417
|
+
html: emailWrapper("Mention", [
|
|
8418
|
+
emailParagraph(`Vous avez ete mentionne dans un message du ticket <strong>#${escapeHtml(String(ticketId))}</strong>.`),
|
|
8419
|
+
...baseUrl ? [emailButton("Ouvrir le ticket", `${baseUrl}/admin/collections/${slugs.tickets}/${ticketId}`, "dark")] : []
|
|
8420
|
+
].join(""), { kind: "system" })
|
|
8421
|
+
});
|
|
8422
|
+
} catch (err) {
|
|
8423
|
+
console.error("[support] Failed to notify mention:", err);
|
|
8424
|
+
}
|
|
8425
|
+
}
|
|
8426
|
+
return doc;
|
|
8427
|
+
};
|
|
8428
|
+
}
|
|
7557
8429
|
function createTicketMessagesCollection(slugs, options) {
|
|
7558
8430
|
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
7559
8431
|
return {
|
|
@@ -7561,7 +8433,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7561
8433
|
labels: { singular: "Message", plural: "Messages" },
|
|
7562
8434
|
admin: { hidden: true, group: "Support", defaultColumns: ["ticket", "authorType", "createdAt"] },
|
|
7563
8435
|
fields: [
|
|
7564
|
-
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, required: true, label: "Ticket" },
|
|
8436
|
+
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, required: true, index: true, label: "Ticket" },
|
|
7565
8437
|
{ name: "body", type: "textarea", required: true, label: "Message" },
|
|
7566
8438
|
{ name: "bodyHtml", type: "textarea", label: "Message HTML", admin: { hidden: true } },
|
|
7567
8439
|
{
|
|
@@ -7570,6 +8442,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7570
8442
|
{
|
|
7571
8443
|
name: "authorType",
|
|
7572
8444
|
type: "select",
|
|
8445
|
+
index: true,
|
|
7573
8446
|
label: "Type d'auteur",
|
|
7574
8447
|
defaultValue: "admin",
|
|
7575
8448
|
options: [
|
|
@@ -7605,8 +8478,9 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7605
8478
|
},
|
|
7606
8479
|
{ name: "isInternal", type: "checkbox", defaultValue: false, label: "Note interne", admin: { position: "sidebar" } },
|
|
7607
8480
|
{ name: "isSolution", type: "checkbox", defaultValue: false, label: "Reponse solution", admin: { position: "sidebar" } },
|
|
8481
|
+
{ name: "mentions", type: "relationship", relationTo: slugs.users, hasMany: true, label: "Agents mentionnes", admin: { hidden: true } },
|
|
7608
8482
|
{ name: "skipNotification", type: "checkbox", defaultValue: false, label: "Sans notification", admin: { position: "sidebar", condition: (data) => data?.skipNotification === true } },
|
|
7609
|
-
{ name: "scheduledAt", type: "date", label: "Programme pour", admin: { date: { pickerAppearance: "dayAndTime" }, position: "sidebar", condition: (data) => !!data?.scheduledAt } },
|
|
8483
|
+
{ name: "scheduledAt", type: "date", index: true, label: "Programme pour", admin: { date: { pickerAppearance: "dayAndTime" }, position: "sidebar", condition: (data) => !!data?.scheduledAt } },
|
|
7610
8484
|
{ name: "scheduledSent", type: "checkbox", defaultValue: false, admin: { hidden: true } },
|
|
7611
8485
|
{ name: "editedAt", type: "date", label: "Modifie le", admin: { hidden: true } },
|
|
7612
8486
|
{ name: "deletedAt", type: "date", label: "Supprime le", admin: { hidden: true } },
|
|
@@ -7615,7 +8489,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7615
8489
|
{ name: "emailOpenedAt", type: "date", label: "Email ouvert le", admin: { hidden: true } }
|
|
7616
8490
|
],
|
|
7617
8491
|
hooks: {
|
|
7618
|
-
beforeChange: [createAssignAuthor(slugs)],
|
|
8492
|
+
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
7619
8493
|
afterChange: [
|
|
7620
8494
|
createAutoUpdateStatus(slugs),
|
|
7621
8495
|
createNotifyClient(slugs),
|
|
@@ -7624,17 +8498,19 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7624
8498
|
createSyncTicketReplyToChat(slugs),
|
|
7625
8499
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
7626
8500
|
createFireMessageWebhooks(slugs),
|
|
7627
|
-
createDispatchWebhookOnReply(slugs)
|
|
8501
|
+
createDispatchWebhookOnReply(slugs),
|
|
8502
|
+
createNotifyMentions(slugs)
|
|
7628
8503
|
]
|
|
7629
8504
|
},
|
|
7630
8505
|
access: {
|
|
7631
8506
|
create: ({ req }) => req.user?.collection === slugs.users || req.user?.collection === slugs.supportClients,
|
|
7632
|
-
read: ({ req }) => {
|
|
8507
|
+
read: async ({ req }) => {
|
|
7633
8508
|
if (req.user?.collection === slugs.users) return true;
|
|
7634
8509
|
if (req.user?.collection === slugs.supportClients) {
|
|
8510
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
7635
8511
|
return {
|
|
7636
8512
|
and: [
|
|
7637
|
-
{
|
|
8513
|
+
{ ticket: { in: ids.length > 0 ? ids : [-1] } },
|
|
7638
8514
|
{ isInternal: { equals: false } },
|
|
7639
8515
|
{ or: [{ scheduledAt: { exists: false } }, { scheduledSent: { equals: true } }] }
|
|
7640
8516
|
]
|
|
@@ -7654,8 +8530,6 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7654
8530
|
timestamps: true
|
|
7655
8531
|
};
|
|
7656
8532
|
}
|
|
7657
|
-
|
|
7658
|
-
// src/collections/SupportClients.ts
|
|
7659
8533
|
function createSendInvitationOnCreate(slugs) {
|
|
7660
8534
|
return async ({ doc, operation, req }) => {
|
|
7661
8535
|
if (operation !== "create") return doc;
|
|
@@ -7695,6 +8569,26 @@ function createSendInvitationOnCreate(slugs) {
|
|
|
7695
8569
|
return doc;
|
|
7696
8570
|
};
|
|
7697
8571
|
}
|
|
8572
|
+
var TWO_FA_WINDOW_MS = 5 * 60 * 1e3;
|
|
8573
|
+
function createEnforce2FA(slugs) {
|
|
8574
|
+
return async ({ req, user }) => {
|
|
8575
|
+
if (!user?.twoFactorEnabled) return user;
|
|
8576
|
+
const raw = user.twoFactorVerifiedAt;
|
|
8577
|
+
const verifiedAt = raw ? new Date(raw).getTime() : 0;
|
|
8578
|
+
if (!(verifiedAt > Date.now() - TWO_FA_WINDOW_MS)) {
|
|
8579
|
+
throw new APIError("2FA_REQUIRED", 401);
|
|
8580
|
+
}
|
|
8581
|
+
await req.payload.update({
|
|
8582
|
+
collection: slugs.supportClients,
|
|
8583
|
+
id: user.id,
|
|
8584
|
+
data: { twoFactorVerifiedAt: null },
|
|
8585
|
+
req,
|
|
8586
|
+
overrideAccess: true
|
|
8587
|
+
});
|
|
8588
|
+
user.twoFactorVerifiedAt = null;
|
|
8589
|
+
return user;
|
|
8590
|
+
};
|
|
8591
|
+
}
|
|
7698
8592
|
function createSupportClientsCollection(slugs) {
|
|
7699
8593
|
return {
|
|
7700
8594
|
slug: slugs.supportClients,
|
|
@@ -7823,6 +8717,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7823
8717
|
name: "opportunities",
|
|
7824
8718
|
type: "textarea",
|
|
7825
8719
|
label: "Opportunites commerciales",
|
|
8720
|
+
access: {
|
|
8721
|
+
// Admin-only: never serialize commercial notes onto the client's own doc.
|
|
8722
|
+
read: ({ req }) => req.user?.collection === slugs.users
|
|
8723
|
+
},
|
|
7826
8724
|
admin: {
|
|
7827
8725
|
description: "Notes commerciales (visible uniquement par les admins)"
|
|
7828
8726
|
}
|
|
@@ -7866,6 +8764,19 @@ function createSupportClientsCollection(slugs) {
|
|
|
7866
8764
|
type: "date",
|
|
7867
8765
|
admin: { hidden: true }
|
|
7868
8766
|
},
|
|
8767
|
+
{
|
|
8768
|
+
// Set by /support/2fa "verify"; gates login via the beforeLogin hook.
|
|
8769
|
+
name: "twoFactorVerifiedAt",
|
|
8770
|
+
type: "date",
|
|
8771
|
+
admin: { hidden: true }
|
|
8772
|
+
},
|
|
8773
|
+
{
|
|
8774
|
+
// Google OAuth subject id — accounts are linked by this, not email alone.
|
|
8775
|
+
name: "googleId",
|
|
8776
|
+
type: "text",
|
|
8777
|
+
index: true,
|
|
8778
|
+
admin: { hidden: true }
|
|
8779
|
+
},
|
|
7869
8780
|
{
|
|
7870
8781
|
name: "notifyOnReply",
|
|
7871
8782
|
type: "checkbox",
|
|
@@ -7932,6 +8843,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7932
8843
|
name: "notes",
|
|
7933
8844
|
type: "textarea",
|
|
7934
8845
|
label: "Notes internes",
|
|
8846
|
+
access: {
|
|
8847
|
+
// Admin-only: never serialize internal notes onto the client's own doc.
|
|
8848
|
+
read: ({ req }) => req.user?.collection === slugs.users
|
|
8849
|
+
},
|
|
7935
8850
|
admin: {
|
|
7936
8851
|
description: "Visible uniquement par les admins",
|
|
7937
8852
|
position: "sidebar"
|
|
@@ -7939,6 +8854,7 @@ function createSupportClientsCollection(slugs) {
|
|
|
7939
8854
|
}
|
|
7940
8855
|
],
|
|
7941
8856
|
hooks: {
|
|
8857
|
+
beforeLogin: [createEnforce2FA(slugs)],
|
|
7942
8858
|
afterChange: [createSendInvitationOnCreate(slugs)]
|
|
7943
8859
|
},
|
|
7944
8860
|
access: {
|
|
@@ -8152,6 +9068,7 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
8152
9068
|
type: "relationship",
|
|
8153
9069
|
relationTo: slugs.tickets,
|
|
8154
9070
|
required: true,
|
|
9071
|
+
index: true,
|
|
8155
9072
|
label: "Ticket"
|
|
8156
9073
|
},
|
|
8157
9074
|
{
|
|
@@ -8197,10 +9114,11 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
8197
9114
|
access: {
|
|
8198
9115
|
create: () => false,
|
|
8199
9116
|
// Created only by hooks via overrideAccess
|
|
8200
|
-
read: ({ req }) => {
|
|
9117
|
+
read: async ({ req }) => {
|
|
8201
9118
|
if (req.user?.collection === slugs.users) return true;
|
|
8202
9119
|
if (req.user?.collection === slugs.supportClients) {
|
|
8203
|
-
|
|
9120
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
9121
|
+
return { ticket: { in: ids.length > 0 ? ids : [-1] } };
|
|
8204
9122
|
}
|
|
8205
9123
|
return false;
|
|
8206
9124
|
},
|
|
@@ -8269,12 +9187,21 @@ function createSatisfactionSurveysCollection(slugs) {
|
|
|
8269
9187
|
{
|
|
8270
9188
|
name: "rating",
|
|
8271
9189
|
type: "number",
|
|
8272
|
-
required: true,
|
|
8273
9190
|
min: 1,
|
|
8274
9191
|
max: 5,
|
|
8275
9192
|
label: "Note (1-5)",
|
|
8276
9193
|
admin: {
|
|
8277
|
-
description: "1 = Tr\xE8s insatisfait, 5 = Tr\xE8s satisfait"
|
|
9194
|
+
description: "1 = Tr\xE8s insatisfait, 5 = Tr\xE8s satisfait (CSAT)"
|
|
9195
|
+
}
|
|
9196
|
+
},
|
|
9197
|
+
{
|
|
9198
|
+
name: "nps",
|
|
9199
|
+
type: "number",
|
|
9200
|
+
min: 0,
|
|
9201
|
+
max: 10,
|
|
9202
|
+
label: "NPS (0-10)",
|
|
9203
|
+
admin: {
|
|
9204
|
+
description: "\xAB Recommanderiez-vous ? \xBB \u2014 promoteur 9-10, passif 7-8, d\xE9tracteur 0-6"
|
|
8278
9205
|
}
|
|
8279
9206
|
},
|
|
8280
9207
|
{
|
|
@@ -8996,6 +9923,16 @@ function createSlaPoliciesCollection(slugs) {
|
|
|
8996
9923
|
admin: {
|
|
8997
9924
|
description: "Utiliser cette politique pour les tickets sans SLA sp\xE9cifique (par priorit\xE9)"
|
|
8998
9925
|
}
|
|
9926
|
+
},
|
|
9927
|
+
{
|
|
9928
|
+
name: "team",
|
|
9929
|
+
type: "relationship",
|
|
9930
|
+
relationTo: slugs.supportTeams,
|
|
9931
|
+
index: true,
|
|
9932
|
+
label: "\xC9quipe (optionnel)",
|
|
9933
|
+
admin: {
|
|
9934
|
+
description: "Limiter cette politique \xE0 une \xE9quipe \u2014 prioritaire sur la politique par d\xE9faut"
|
|
9935
|
+
}
|
|
8999
9936
|
}
|
|
9000
9937
|
],
|
|
9001
9938
|
access: {
|
|
@@ -9520,6 +10457,148 @@ function createTicketCollaboratorsCollection(slugs) {
|
|
|
9520
10457
|
};
|
|
9521
10458
|
}
|
|
9522
10459
|
|
|
10460
|
+
// src/collections/NotificationQueue.ts
|
|
10461
|
+
function createNotificationQueueCollection(slugs) {
|
|
10462
|
+
return {
|
|
10463
|
+
slug: slugs.notificationQueue,
|
|
10464
|
+
labels: { singular: "Notification en file", plural: "Notifications en file" },
|
|
10465
|
+
admin: { hidden: true, group: "Support", defaultColumns: ["client", "type", "title", "createdAt"] },
|
|
10466
|
+
access: {
|
|
10467
|
+
// Internal: written/drained only via overrideAccess (hooks + cron).
|
|
10468
|
+
create: () => false,
|
|
10469
|
+
update: () => false,
|
|
10470
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10471
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10472
|
+
},
|
|
10473
|
+
fields: [
|
|
10474
|
+
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, index: true, label: "Client" },
|
|
10475
|
+
{ name: "type", type: "text", label: "Type" },
|
|
10476
|
+
{ name: "title", type: "text", label: "Titre" },
|
|
10477
|
+
{ name: "message", type: "textarea", label: "Message" },
|
|
10478
|
+
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, label: "Ticket" }
|
|
10479
|
+
],
|
|
10480
|
+
timestamps: true
|
|
10481
|
+
};
|
|
10482
|
+
}
|
|
10483
|
+
|
|
10484
|
+
// src/collections/AutomationRules.ts
|
|
10485
|
+
var CONDITION_FIELDS = ["status", "priority", "category", "source", "subject"];
|
|
10486
|
+
var ACTION_TYPES = [
|
|
10487
|
+
{ label: "D\xE9finir le statut", value: "set_status" },
|
|
10488
|
+
{ label: "D\xE9finir la priorit\xE9", value: "set_priority" },
|
|
10489
|
+
{ label: "D\xE9finir la cat\xE9gorie", value: "set_category" },
|
|
10490
|
+
{ label: "Assigner \xE0 (id agent)", value: "assign" },
|
|
10491
|
+
{ label: "Ajouter un tag", value: "add_tag" }
|
|
10492
|
+
];
|
|
10493
|
+
function createAutomationRulesCollection(slugs) {
|
|
10494
|
+
return {
|
|
10495
|
+
slug: slugs.automationRules,
|
|
10496
|
+
labels: { singular: "R\xE8gle d'automatisation", plural: "R\xE8gles d'automatisation" },
|
|
10497
|
+
admin: { useAsTitle: "name", group: "Support", defaultColumns: ["name", "event", "enabled", "order"] },
|
|
10498
|
+
access: {
|
|
10499
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10500
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
10501
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
10502
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10503
|
+
},
|
|
10504
|
+
fields: [
|
|
10505
|
+
{ name: "name", type: "text", required: true, label: "Nom" },
|
|
10506
|
+
{ name: "enabled", type: "checkbox", defaultValue: true, label: "Active", index: true },
|
|
10507
|
+
{
|
|
10508
|
+
name: "event",
|
|
10509
|
+
type: "select",
|
|
10510
|
+
defaultValue: "ticket_created",
|
|
10511
|
+
index: true,
|
|
10512
|
+
label: "D\xE9clencheur",
|
|
10513
|
+
options: [
|
|
10514
|
+
{ label: "Ticket cr\xE9\xE9", value: "ticket_created" },
|
|
10515
|
+
{ label: "Ticket mis \xE0 jour", value: "ticket_updated" },
|
|
10516
|
+
{ label: "Changement de statut", value: "ticket_status_changed" }
|
|
10517
|
+
]
|
|
10518
|
+
},
|
|
10519
|
+
{
|
|
10520
|
+
name: "matchType",
|
|
10521
|
+
type: "select",
|
|
10522
|
+
defaultValue: "all",
|
|
10523
|
+
label: "Correspondance",
|
|
10524
|
+
options: [{ label: "Toutes les conditions (ET)", value: "all" }, { label: "Au moins une (OU)", value: "any" }]
|
|
10525
|
+
},
|
|
10526
|
+
{ name: "order", type: "number", defaultValue: 0, label: "Ordre d'\xE9valuation" },
|
|
10527
|
+
{
|
|
10528
|
+
name: "conditions",
|
|
10529
|
+
type: "array",
|
|
10530
|
+
label: "Conditions",
|
|
10531
|
+
fields: [
|
|
10532
|
+
{ name: "field", type: "select", required: true, options: CONDITION_FIELDS.map((v) => ({ label: v, value: v })) },
|
|
10533
|
+
{
|
|
10534
|
+
name: "operator",
|
|
10535
|
+
type: "select",
|
|
10536
|
+
defaultValue: "equals",
|
|
10537
|
+
required: true,
|
|
10538
|
+
options: [{ label: "= \xE9gal", value: "equals" }, { label: "\u2260 diff\xE9rent", value: "not_equals" }, { label: "contient", value: "contains" }]
|
|
10539
|
+
},
|
|
10540
|
+
{ name: "value", type: "text", required: true }
|
|
10541
|
+
]
|
|
10542
|
+
},
|
|
10543
|
+
{
|
|
10544
|
+
name: "actions",
|
|
10545
|
+
type: "array",
|
|
10546
|
+
label: "Actions",
|
|
10547
|
+
fields: [
|
|
10548
|
+
{ name: "type", type: "select", required: true, options: ACTION_TYPES },
|
|
10549
|
+
{ name: "value", type: "text", required: true }
|
|
10550
|
+
]
|
|
10551
|
+
}
|
|
10552
|
+
],
|
|
10553
|
+
timestamps: true
|
|
10554
|
+
};
|
|
10555
|
+
}
|
|
10556
|
+
|
|
10557
|
+
// src/collections/SupportTeam.ts
|
|
10558
|
+
function createSupportTeamCollection(slugs) {
|
|
10559
|
+
return {
|
|
10560
|
+
slug: slugs.supportTeams,
|
|
10561
|
+
labels: { singular: "\xC9quipe support", plural: "\xC9quipes support" },
|
|
10562
|
+
admin: { useAsTitle: "name", group: "Support", defaultColumns: ["name", "members"] },
|
|
10563
|
+
access: {
|
|
10564
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10565
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
10566
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
10567
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10568
|
+
},
|
|
10569
|
+
fields: [
|
|
10570
|
+
{ name: "name", type: "text", required: true, label: "Nom" },
|
|
10571
|
+
{ name: "description", type: "textarea", label: "Description" },
|
|
10572
|
+
{ name: "members", type: "relationship", relationTo: slugs.users, hasMany: true, index: true, label: "Membres (agents)" }
|
|
10573
|
+
],
|
|
10574
|
+
timestamps: true
|
|
10575
|
+
};
|
|
10576
|
+
}
|
|
10577
|
+
|
|
10578
|
+
// src/collections/PushSubscription.ts
|
|
10579
|
+
function createPushSubscriptionCollection(slugs) {
|
|
10580
|
+
return {
|
|
10581
|
+
slug: slugs.pushSubscriptions,
|
|
10582
|
+
labels: { singular: "Abonnement push", plural: "Abonnements push" },
|
|
10583
|
+
admin: { hidden: true, group: "Support", defaultColumns: ["user", "endpoint", "createdAt"] },
|
|
10584
|
+
access: {
|
|
10585
|
+
create: () => false,
|
|
10586
|
+
// written via the subscribe endpoint (overrideAccess)
|
|
10587
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10588
|
+
update: () => false,
|
|
10589
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10590
|
+
},
|
|
10591
|
+
fields: [
|
|
10592
|
+
{ name: "user", type: "relationship", relationTo: slugs.users, index: true, label: "Agent" },
|
|
10593
|
+
{ name: "endpoint", type: "text", required: true, index: true, label: "Endpoint" },
|
|
10594
|
+
{ name: "p256dh", type: "text", label: "Cl\xE9 p256dh" },
|
|
10595
|
+
{ name: "auth", type: "text", label: "Cl\xE9 auth" },
|
|
10596
|
+
{ name: "userAgent", type: "text", label: "User-Agent" }
|
|
10597
|
+
],
|
|
10598
|
+
timestamps: true
|
|
10599
|
+
};
|
|
10600
|
+
}
|
|
10601
|
+
|
|
9523
10602
|
// src/plugin.ts
|
|
9524
10603
|
function viewConfig(component, path) {
|
|
9525
10604
|
return { Component: component, path };
|
|
@@ -9555,7 +10634,11 @@ function supportPlugin(config) {
|
|
|
9555
10634
|
createSatisfactionSurveysCollection(slugs),
|
|
9556
10635
|
createKnowledgeBaseCollection(slugs),
|
|
9557
10636
|
createTicketFeedbackCollection(slugs, { notificationSlug: config?.notificationSlug }),
|
|
9558
|
-
createTicketCollaboratorsCollection(slugs)
|
|
10637
|
+
createTicketCollaboratorsCollection(slugs),
|
|
10638
|
+
createNotificationQueueCollection(slugs),
|
|
10639
|
+
createAutomationRulesCollection(slugs),
|
|
10640
|
+
createSupportTeamCollection(slugs),
|
|
10641
|
+
createPushSubscriptionCollection(slugs)
|
|
9559
10642
|
];
|
|
9560
10643
|
if (features.authLogs !== false) supportCollections.push(createAuthLogsCollection(slugs));
|
|
9561
10644
|
if (features.timeTracking !== false) supportCollections.push(createTimeEntriesCollection(slugs));
|