@consilioweb/payload-support 0.16.0 → 1.0.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 +347 -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 +1604 -671
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1602 -671
- 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 +23 -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/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/SatisfactionSurveys.ts +11 -2
- 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 +129 -31
- package/src/collections/Tickets.ts +31 -73
- package/src/collections/index.ts +3 -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 +70 -32
- 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 +18 -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/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 +48 -16
- package/src/plugin.ts +6 -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/readSettings.ts +22 -6
- package/src/utils/sanitizeHtml.ts +57 -0
- package/src/utils/slugs.ts +6 -0
- package/src/utils/teamAccess.ts +27 -0
- package/src/utils/ticketAccess.ts +53 -0
- package/src/utils/webhookDispatcher.ts +4 -6
- package/src/views/BillingView/client.tsx +49 -37
- package/src/views/ChatView/client.tsx +4 -3
- package/src/views/CrmView/client.tsx +7 -7
- package/src/views/EmailTrackingView/client.tsx +7 -6
- package/src/views/ImportConversationView/client.tsx +4 -4
- package/src/views/LogsView/client.tsx +3 -2
- package/src/views/NewTicketView/client.tsx +1 -1
- package/src/views/PendingEmailsView/client.tsx +7 -6
- package/src/views/SupportDashboardView/client.tsx +21 -21
- package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
- package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
- package/src/views/TicketDetailView/client.tsx +54 -179
- package/src/views/TicketDetailView/constants.ts +23 -0
- package/src/views/TicketDetailView/helpers.ts +17 -0
- package/src/views/TicketDetailView/types.ts +10 -0
- package/src/views/TicketInboxView/client.tsx +16 -10
- package/src/views/TicketingSettingsView/client.tsx +90 -87
- package/src/views/TimeDashboardView/client.tsx +2 -2
- package/src/views/shared/StatusPill.tsx +24 -12
- package/src/views/shared/dateLocale.ts +5 -0
- package/src/views/shared/locales/en.json +708 -0
- package/src/views/shared/locales/fr.json +708 -0
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var crypto3 = require('crypto');
|
|
4
|
+
var PDFDocument = require('pdfkit');
|
|
5
|
+
var payload = require('payload');
|
|
6
|
+
var sanitizeHtml = require('sanitize-html');
|
|
4
7
|
|
|
5
8
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
9
|
|
|
7
10
|
var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
|
|
11
|
+
var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
|
|
12
|
+
var sanitizeHtml__default = /*#__PURE__*/_interopDefault(sanitizeHtml);
|
|
8
13
|
|
|
9
14
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
10
15
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -64,6 +69,9 @@ var DEFAULT_SLUGS = {
|
|
|
64
69
|
macros: "macros",
|
|
65
70
|
ticketStatuses: "ticket-statuses",
|
|
66
71
|
ticketFeedback: "ticket-feedback",
|
|
72
|
+
notificationQueue: "notification-queue",
|
|
73
|
+
automationRules: "automation-rules",
|
|
74
|
+
supportTeams: "support-teams",
|
|
67
75
|
users: "users",
|
|
68
76
|
media: "media"
|
|
69
77
|
};
|
|
@@ -95,6 +103,26 @@ function handleAuthError(error) {
|
|
|
95
103
|
return null;
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
// src/utils/db.ts
|
|
107
|
+
function dbFind(payload, slug, options = {}) {
|
|
108
|
+
return payload.find({ collection: slug, ...options });
|
|
109
|
+
}
|
|
110
|
+
function dbFindByID(payload, slug, options) {
|
|
111
|
+
return payload.findByID({ collection: slug, ...options });
|
|
112
|
+
}
|
|
113
|
+
function dbCreate(payload, slug, options) {
|
|
114
|
+
return payload.create({ collection: slug, ...options });
|
|
115
|
+
}
|
|
116
|
+
function dbUpdate(payload, slug, options) {
|
|
117
|
+
return payload.update({ collection: slug, ...options });
|
|
118
|
+
}
|
|
119
|
+
function dbCount(payload, slug, options = {}) {
|
|
120
|
+
return payload.count({ collection: slug, ...options });
|
|
121
|
+
}
|
|
122
|
+
function dbDelete(payload, slug, options) {
|
|
123
|
+
return payload.delete({ collection: slug, ...options });
|
|
124
|
+
}
|
|
125
|
+
|
|
98
126
|
// src/utils/readSettings.ts
|
|
99
127
|
var PREF_KEY = "support-settings";
|
|
100
128
|
var USER_PREFS_KEY_PREFIX = "support-user-prefs";
|
|
@@ -108,10 +136,18 @@ var DEFAULT_USER_PREFS = {
|
|
|
108
136
|
locale: "fr",
|
|
109
137
|
signature: ""
|
|
110
138
|
};
|
|
139
|
+
var settingsCache = null;
|
|
140
|
+
var SETTINGS_TTL_MS = 6e4;
|
|
141
|
+
function invalidateSupportSettingsCache() {
|
|
142
|
+
settingsCache = null;
|
|
143
|
+
}
|
|
111
144
|
async function readSupportSettings(payload) {
|
|
145
|
+
if (settingsCache && Date.now() - settingsCache.ts < SETTINGS_TTL_MS) {
|
|
146
|
+
return settingsCache.value;
|
|
147
|
+
}
|
|
148
|
+
let value = { ...DEFAULT_SETTINGS };
|
|
112
149
|
try {
|
|
113
|
-
const prefs = await payload
|
|
114
|
-
collection: "payload-preferences",
|
|
150
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
115
151
|
where: { key: { equals: PREF_KEY } },
|
|
116
152
|
limit: 1,
|
|
117
153
|
depth: 0,
|
|
@@ -119,7 +155,7 @@ async function readSupportSettings(payload) {
|
|
|
119
155
|
});
|
|
120
156
|
if (prefs.docs.length > 0) {
|
|
121
157
|
const stored = prefs.docs[0].value;
|
|
122
|
-
|
|
158
|
+
value = {
|
|
123
159
|
email: { ...DEFAULT_SETTINGS.email, ...stored.email },
|
|
124
160
|
ai: { ...DEFAULT_SETTINGS.ai, ...stored.ai },
|
|
125
161
|
sla: { ...DEFAULT_SETTINGS.sla, ...stored.sla },
|
|
@@ -128,13 +164,13 @@ async function readSupportSettings(payload) {
|
|
|
128
164
|
}
|
|
129
165
|
} catch {
|
|
130
166
|
}
|
|
131
|
-
|
|
167
|
+
settingsCache = { value, ts: Date.now() };
|
|
168
|
+
return value;
|
|
132
169
|
}
|
|
133
170
|
async function readUserPrefs(payload, userId) {
|
|
134
171
|
try {
|
|
135
172
|
const key = `${USER_PREFS_KEY_PREFIX}-${userId}`;
|
|
136
|
-
const prefs = await payload
|
|
137
|
-
collection: "payload-preferences",
|
|
173
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
138
174
|
where: { key: { equals: key } },
|
|
139
175
|
limit: 1,
|
|
140
176
|
depth: 0,
|
|
@@ -320,8 +356,130 @@ ${text}`;
|
|
|
320
356
|
};
|
|
321
357
|
}
|
|
322
358
|
|
|
323
|
-
// src/
|
|
359
|
+
// src/utils/aiAgent.ts
|
|
360
|
+
async function applyAgentDecision(payload, slugs, ticketId, decision) {
|
|
361
|
+
if (decision.action === "reply" && decision.reply && decision.reply.trim()) {
|
|
362
|
+
const msg = await dbCreate(payload, slugs.ticketMessages, {
|
|
363
|
+
data: {
|
|
364
|
+
ticket: ticketId,
|
|
365
|
+
body: decision.reply.trim(),
|
|
366
|
+
authorType: "admin",
|
|
367
|
+
isInternal: false,
|
|
368
|
+
fromAlias: "Assistant IA"
|
|
369
|
+
},
|
|
370
|
+
overrideAccess: true
|
|
371
|
+
});
|
|
372
|
+
return { acted: "reply", messageId: msg.id, confidence: decision.confidence };
|
|
373
|
+
}
|
|
374
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
375
|
+
data: {
|
|
376
|
+
ticket: ticketId,
|
|
377
|
+
body: `[Agent IA] Escalade vers un humain. Raison : ${decision.reason || "non d\xE9termin\xE9e"} (confiance ${Math.round((decision.confidence ?? 0) * 100)} %).`,
|
|
378
|
+
authorType: "admin",
|
|
379
|
+
isInternal: true,
|
|
380
|
+
skipNotification: true
|
|
381
|
+
},
|
|
382
|
+
overrideAccess: true
|
|
383
|
+
});
|
|
384
|
+
await dbUpdate(payload, slugs.tickets, { id: ticketId, data: { status: "escalated" }, overrideAccess: true });
|
|
385
|
+
return { acted: "escalate", confidence: decision.confidence };
|
|
386
|
+
}
|
|
324
387
|
function getClient2(aiSettings) {
|
|
388
|
+
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
389
|
+
if (aiSettings.provider === "ollama") {
|
|
390
|
+
return new Anthropic({ apiKey: "ollama", baseURL: process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1" });
|
|
391
|
+
}
|
|
392
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
393
|
+
}
|
|
394
|
+
function stripHtml(s) {
|
|
395
|
+
return (s || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
396
|
+
}
|
|
397
|
+
function parseDecision(text) {
|
|
398
|
+
try {
|
|
399
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
400
|
+
if (match) {
|
|
401
|
+
const obj = JSON.parse(match[0]);
|
|
402
|
+
return {
|
|
403
|
+
action: obj.action === "reply" ? "reply" : "escalate",
|
|
404
|
+
reply: typeof obj.reply === "string" ? obj.reply : void 0,
|
|
405
|
+
reason: typeof obj.reason === "string" ? obj.reason : void 0,
|
|
406
|
+
confidence: typeof obj.confidence === "number" ? obj.confidence : 0
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
return { action: "escalate", reason: "R\xE9ponse IA non interpr\xE9table", confidence: 0 };
|
|
412
|
+
}
|
|
413
|
+
async function runAiAgent(payload, slugs, ticketId, opts) {
|
|
414
|
+
const settings = await readSupportSettings(payload);
|
|
415
|
+
const threshold = opts?.confidenceThreshold ?? 0.7;
|
|
416
|
+
const ticket = await dbFindByID(payload, slugs.tickets, { id: ticketId, depth: 0, overrideAccess: true });
|
|
417
|
+
const messages = await dbFind(payload, slugs.ticketMessages, { where: { ticket: { equals: ticketId } }, sort: "createdAt", limit: 50, depth: 0, overrideAccess: true });
|
|
418
|
+
const kb = await dbFind(payload, slugs.knowledgeBase, { where: { published: { equals: true } }, limit: 20, depth: 0, overrideAccess: true });
|
|
419
|
+
const conversation = messages.docs.map((m) => `${m.authorType}: ${m.body || ""}`).join("\n");
|
|
420
|
+
const kbText = kb.docs.map((a) => `# ${a.title || ""}
|
|
421
|
+
${stripHtml(String(a.content ?? ""))}`).join("\n\n").slice(0, 8e3);
|
|
422
|
+
const prompt = `Tu es un agent de support IA. \xC0 partir du ticket et de la base de connaissances ci-dessous, d\xE9cide :
|
|
423
|
+
- "reply" si tu peux r\xE9pondre avec certitude UNIQUEMENT \xE0 partir de la base de connaissances ;
|
|
424
|
+
- "escalate" sinon (question hors KB, ambigu\xEB, sensible, ou n\xE9cessitant une action humaine).
|
|
425
|
+
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>}.
|
|
426
|
+
|
|
427
|
+
TICKET : ${ticket.subject || ""}
|
|
428
|
+
CONVERSATION :
|
|
429
|
+
${conversation}
|
|
430
|
+
|
|
431
|
+
BASE DE CONNAISSANCES :
|
|
432
|
+
${kbText || "(vide)"}`;
|
|
433
|
+
let decision;
|
|
434
|
+
try {
|
|
435
|
+
const client = getClient2(settings.ai);
|
|
436
|
+
const resp = await client.messages.create({
|
|
437
|
+
model: settings.ai.model || "claude-haiku-4-5-20251001",
|
|
438
|
+
max_tokens: 1024,
|
|
439
|
+
messages: [{ role: "user", content: prompt }]
|
|
440
|
+
});
|
|
441
|
+
const text = resp?.content?.[0]?.text || "";
|
|
442
|
+
decision = parseDecision(text);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.error("[support] AI agent decision failed:", err);
|
|
445
|
+
decision = { action: "escalate", reason: "Erreur du fournisseur IA", confidence: 0 };
|
|
446
|
+
}
|
|
447
|
+
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 };
|
|
448
|
+
return applyAgentDecision(payload, slugs, ticketId, finalDecision);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/endpoints/ai-agent.ts
|
|
452
|
+
function createAiAgentEndpoint(slugs) {
|
|
453
|
+
return {
|
|
454
|
+
path: "/support/ai-agent",
|
|
455
|
+
method: "post",
|
|
456
|
+
handler: async (req) => {
|
|
457
|
+
try {
|
|
458
|
+
requireAdmin(req, slugs);
|
|
459
|
+
let body = {};
|
|
460
|
+
try {
|
|
461
|
+
body = await req.json();
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
if (!body.ticketId) {
|
|
465
|
+
return Response.json({ error: "ticketId requis." }, { status: 400 });
|
|
466
|
+
}
|
|
467
|
+
const result = await runAiAgent(req.payload, slugs, body.ticketId, {
|
|
468
|
+
confidenceThreshold: body.confidenceThreshold
|
|
469
|
+
});
|
|
470
|
+
return Response.json({ ok: true, ...result });
|
|
471
|
+
} catch (error) {
|
|
472
|
+
const authResponse = handleAuthError(error);
|
|
473
|
+
if (authResponse) return authResponse;
|
|
474
|
+
console.error("[ai-agent] Error:", error);
|
|
475
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/endpoints/client-intelligence.ts
|
|
482
|
+
function getClient3(aiSettings) {
|
|
325
483
|
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
326
484
|
if (aiSettings.provider === "ollama") {
|
|
327
485
|
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
@@ -475,7 +633,7 @@ R\xE9ponds en JSON strict (pas de markdown, pas de commentaires) avec cette stru
|
|
|
475
633
|
}
|
|
476
634
|
|
|
477
635
|
Sois factuel. Ne d\xE9passe pas 5 items par tableau. R\xE9ponds UNIQUEMENT avec le JSON.`;
|
|
478
|
-
const anthropic =
|
|
636
|
+
const anthropic = getClient3(aiSettings);
|
|
479
637
|
const model = getModel2(aiSettings);
|
|
480
638
|
const res = await anthropic.messages.create({
|
|
481
639
|
model,
|
|
@@ -539,8 +697,7 @@ function createSearchEndpoint(slugs) {
|
|
|
539
697
|
return Response.json({ tickets: [], clients: [], messages: [], articles: [] });
|
|
540
698
|
}
|
|
541
699
|
const [ticketsRes, clientsRes, messagesRes, articlesRes] = await Promise.all([
|
|
542
|
-
payload.
|
|
543
|
-
collection: slugs.tickets,
|
|
700
|
+
dbFind(payload, slugs.tickets, {
|
|
544
701
|
where: {
|
|
545
702
|
or: [
|
|
546
703
|
{ ticketNumber: { contains: q } },
|
|
@@ -552,8 +709,7 @@ function createSearchEndpoint(slugs) {
|
|
|
552
709
|
depth: 1,
|
|
553
710
|
overrideAccess: true
|
|
554
711
|
}),
|
|
555
|
-
payload.
|
|
556
|
-
collection: slugs.supportClients,
|
|
712
|
+
dbFind(payload, slugs.supportClients, {
|
|
557
713
|
where: {
|
|
558
714
|
or: [
|
|
559
715
|
{ firstName: { contains: q } },
|
|
@@ -566,16 +722,14 @@ function createSearchEndpoint(slugs) {
|
|
|
566
722
|
depth: 0,
|
|
567
723
|
overrideAccess: true
|
|
568
724
|
}),
|
|
569
|
-
payload.
|
|
570
|
-
collection: slugs.ticketMessages,
|
|
725
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
571
726
|
where: { body: { contains: q } },
|
|
572
727
|
sort: "-createdAt",
|
|
573
728
|
limit: 5,
|
|
574
729
|
depth: 1,
|
|
575
730
|
overrideAccess: true
|
|
576
731
|
}),
|
|
577
|
-
payload.
|
|
578
|
-
collection: slugs.knowledgeBase,
|
|
732
|
+
dbFind(payload, slugs.knowledgeBase, {
|
|
579
733
|
where: { title: { contains: q } },
|
|
580
734
|
limit: 5,
|
|
581
735
|
depth: 0,
|
|
@@ -675,8 +829,7 @@ function createKbSearchEndpoint(slugs) {
|
|
|
675
829
|
orClauses.push({ category: { contains: t } });
|
|
676
830
|
}
|
|
677
831
|
orClauses.push({ title: { contains: q } });
|
|
678
|
-
const res = await payload.
|
|
679
|
-
collection: slugs.knowledgeBase,
|
|
832
|
+
const res = await dbFind(payload, slugs.knowledgeBase, {
|
|
680
833
|
where: {
|
|
681
834
|
and: [
|
|
682
835
|
{ published: { equals: true } },
|
|
@@ -735,16 +888,14 @@ function createBulkActionEndpoint(slugs) {
|
|
|
735
888
|
try {
|
|
736
889
|
switch (action) {
|
|
737
890
|
case "close":
|
|
738
|
-
await payload.
|
|
739
|
-
collection: slugs.tickets,
|
|
891
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
740
892
|
id: ticketId,
|
|
741
893
|
data: { status: "resolved", resolvedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
742
894
|
overrideAccess: true
|
|
743
895
|
});
|
|
744
896
|
break;
|
|
745
897
|
case "reopen":
|
|
746
|
-
await payload.
|
|
747
|
-
collection: slugs.tickets,
|
|
898
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
748
899
|
id: ticketId,
|
|
749
900
|
data: { status: "open" },
|
|
750
901
|
overrideAccess: true
|
|
@@ -752,8 +903,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
752
903
|
break;
|
|
753
904
|
case "assign":
|
|
754
905
|
if (value) {
|
|
755
|
-
await payload.
|
|
756
|
-
collection: slugs.tickets,
|
|
906
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
757
907
|
id: ticketId,
|
|
758
908
|
data: { assignedTo: Number(value) },
|
|
759
909
|
overrideAccess: true
|
|
@@ -762,8 +912,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
762
912
|
break;
|
|
763
913
|
case "set_priority":
|
|
764
914
|
if (value) {
|
|
765
|
-
await payload.
|
|
766
|
-
collection: slugs.tickets,
|
|
915
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
767
916
|
id: ticketId,
|
|
768
917
|
data: { priority: String(value) },
|
|
769
918
|
overrideAccess: true
|
|
@@ -772,8 +921,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
772
921
|
break;
|
|
773
922
|
case "set_category":
|
|
774
923
|
if (value) {
|
|
775
|
-
await payload.
|
|
776
|
-
collection: slugs.tickets,
|
|
924
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
777
925
|
id: ticketId,
|
|
778
926
|
data: { category: String(value) },
|
|
779
927
|
overrideAccess: true
|
|
@@ -781,8 +929,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
781
929
|
}
|
|
782
930
|
break;
|
|
783
931
|
case "delete":
|
|
784
|
-
await payload.
|
|
785
|
-
collection: slugs.tickets,
|
|
932
|
+
await dbDelete(payload, slugs.tickets, {
|
|
786
933
|
id: ticketId,
|
|
787
934
|
overrideAccess: true
|
|
788
935
|
});
|
|
@@ -821,8 +968,8 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
821
968
|
return Response.json({ error: "Impossible de fusionner un ticket avec lui-m\xEAme" }, { status: 400 });
|
|
822
969
|
}
|
|
823
970
|
const [source, target] = await Promise.all([
|
|
824
|
-
payload
|
|
825
|
-
payload
|
|
971
|
+
dbFindByID(payload, slugs.tickets, { id: sourceTicketId, depth: 0, overrideAccess: true }),
|
|
972
|
+
dbFindByID(payload, slugs.tickets, { id: targetTicketId, depth: 0, overrideAccess: true })
|
|
826
973
|
]);
|
|
827
974
|
if (!source) return Response.json({ error: "Ticket source introuvable" }, { status: 404 });
|
|
828
975
|
if (!target) return Response.json({ error: "Ticket cible introuvable" }, { status: 404 });
|
|
@@ -831,39 +978,34 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
831
978
|
if (sourceClient !== targetClient) {
|
|
832
979
|
return Response.json({ error: "Les deux tickets doivent appartenir au m\xEAme client" }, { status: 400 });
|
|
833
980
|
}
|
|
834
|
-
const messages = await payload.
|
|
835
|
-
collection: slugs.ticketMessages,
|
|
981
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
836
982
|
where: { ticket: { equals: sourceTicketId } },
|
|
837
983
|
limit: 500,
|
|
838
984
|
depth: 0,
|
|
839
985
|
overrideAccess: true
|
|
840
986
|
});
|
|
841
987
|
for (const msg of messages.docs) {
|
|
842
|
-
await payload.
|
|
843
|
-
collection: slugs.ticketMessages,
|
|
988
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
844
989
|
id: msg.id,
|
|
845
990
|
data: { ticket: targetTicketId },
|
|
846
991
|
overrideAccess: true
|
|
847
992
|
});
|
|
848
993
|
}
|
|
849
|
-
const timeEntries = await payload.
|
|
850
|
-
collection: slugs.timeEntries,
|
|
994
|
+
const timeEntries = await dbFind(payload, slugs.timeEntries, {
|
|
851
995
|
where: { ticket: { equals: sourceTicketId } },
|
|
852
996
|
limit: 500,
|
|
853
997
|
depth: 0,
|
|
854
998
|
overrideAccess: true
|
|
855
999
|
});
|
|
856
1000
|
for (const entry of timeEntries.docs) {
|
|
857
|
-
await payload.
|
|
858
|
-
collection: slugs.timeEntries,
|
|
1001
|
+
await dbUpdate(payload, slugs.timeEntries, {
|
|
859
1002
|
id: entry.id,
|
|
860
1003
|
data: { ticket: targetTicketId },
|
|
861
1004
|
overrideAccess: true
|
|
862
1005
|
});
|
|
863
1006
|
}
|
|
864
1007
|
const sourceNumber = source.ticketNumber || `#${sourceTicketId}`;
|
|
865
|
-
await payload.
|
|
866
|
-
collection: slugs.ticketMessages,
|
|
1008
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
867
1009
|
data: {
|
|
868
1010
|
ticket: targetTicketId,
|
|
869
1011
|
body: `Messages fusionn\xE9s depuis ${sourceNumber} (${messages.totalDocs} messages, ${timeEntries.totalDocs} entr\xE9es de temps)`,
|
|
@@ -873,13 +1015,11 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
873
1015
|
},
|
|
874
1016
|
overrideAccess: true
|
|
875
1017
|
});
|
|
876
|
-
await payload.
|
|
877
|
-
collection: slugs.tickets,
|
|
1018
|
+
await dbDelete(payload, slugs.tickets, {
|
|
878
1019
|
id: sourceTicketId,
|
|
879
1020
|
overrideAccess: true
|
|
880
1021
|
});
|
|
881
|
-
const allTimeEntries = await payload.
|
|
882
|
-
collection: slugs.timeEntries,
|
|
1022
|
+
const allTimeEntries = await dbFind(payload, slugs.timeEntries, {
|
|
883
1023
|
where: { ticket: { equals: targetTicketId } },
|
|
884
1024
|
limit: 500,
|
|
885
1025
|
depth: 0,
|
|
@@ -889,8 +1029,7 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
889
1029
|
(sum, e) => sum + (e.duration || 0),
|
|
890
1030
|
0
|
|
891
1031
|
);
|
|
892
|
-
await payload.
|
|
893
|
-
collection: slugs.tickets,
|
|
1032
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
894
1033
|
id: targetTicketId,
|
|
895
1034
|
data: { totalTimeMinutes: totalMinutes },
|
|
896
1035
|
overrideAccess: true
|
|
@@ -925,8 +1064,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
925
1064
|
if (!messageId) {
|
|
926
1065
|
return Response.json({ error: "messageId required" }, { status: 400 });
|
|
927
1066
|
}
|
|
928
|
-
const message = await payload.
|
|
929
|
-
collection: slugs.ticketMessages,
|
|
1067
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
930
1068
|
id: messageId,
|
|
931
1069
|
depth: 1,
|
|
932
1070
|
overrideAccess: true
|
|
@@ -939,8 +1077,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
939
1077
|
return Response.json({ error: "Cannot resolve source ticket" }, { status: 400 });
|
|
940
1078
|
}
|
|
941
1079
|
const clientId = typeof sourceTicket.client === "object" ? sourceTicket.client.id : sourceTicket.client;
|
|
942
|
-
const newTicket = await payload.
|
|
943
|
-
collection: slugs.tickets,
|
|
1080
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
944
1081
|
data: {
|
|
945
1082
|
subject: subject || `Split: ${sourceTicket.subject}`,
|
|
946
1083
|
client: clientId,
|
|
@@ -955,8 +1092,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
955
1092
|
const attachments = message.attachments?.map((a) => ({
|
|
956
1093
|
file: typeof a.file === "object" ? a.file.id : a.file
|
|
957
1094
|
})) || [];
|
|
958
|
-
await payload.
|
|
959
|
-
collection: slugs.ticketMessages,
|
|
1095
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
960
1096
|
data: {
|
|
961
1097
|
ticket: newTicket.id,
|
|
962
1098
|
body: message.body,
|
|
@@ -969,8 +1105,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
969
1105
|
},
|
|
970
1106
|
overrideAccess: true
|
|
971
1107
|
});
|
|
972
|
-
await payload.
|
|
973
|
-
collection: slugs.ticketMessages,
|
|
1108
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
974
1109
|
data: {
|
|
975
1110
|
ticket: sourceTicket.id,
|
|
976
1111
|
body: `Message extrait vers le ticket ${newTicket.ticketNumber}`,
|
|
@@ -982,15 +1117,13 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
982
1117
|
});
|
|
983
1118
|
const existingRelated = Array.isArray(sourceTicket.relatedTickets) ? sourceTicket.relatedTickets.map((t) => typeof t === "object" ? t.id : t) : [];
|
|
984
1119
|
if (!existingRelated.includes(newTicket.id)) {
|
|
985
|
-
await payload.
|
|
986
|
-
collection: slugs.tickets,
|
|
1120
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
987
1121
|
id: sourceTicket.id,
|
|
988
1122
|
data: { relatedTickets: [...existingRelated, newTicket.id] },
|
|
989
1123
|
overrideAccess: true
|
|
990
1124
|
});
|
|
991
1125
|
}
|
|
992
|
-
await payload.
|
|
993
|
-
collection: slugs.ticketActivityLog,
|
|
1126
|
+
await dbCreate(payload, slugs.ticketActivityLog, {
|
|
994
1127
|
data: {
|
|
995
1128
|
ticket: sourceTicket.id,
|
|
996
1129
|
action: "split",
|
|
@@ -1205,8 +1338,7 @@ function createSettingsGetEndpoint(slugs) {
|
|
|
1205
1338
|
try {
|
|
1206
1339
|
const payload = req.payload;
|
|
1207
1340
|
requireAdmin(req, slugs);
|
|
1208
|
-
const prefs = await payload
|
|
1209
|
-
collection: "payload-preferences",
|
|
1341
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1210
1342
|
where: { key: { equals: PREF_KEY2 } },
|
|
1211
1343
|
limit: 1,
|
|
1212
1344
|
depth: 0,
|
|
@@ -1263,6 +1395,7 @@ function createSettingsPostEndpoint(slugs) {
|
|
|
1263
1395
|
]
|
|
1264
1396
|
}
|
|
1265
1397
|
});
|
|
1398
|
+
invalidateSupportSettingsCache();
|
|
1266
1399
|
return Response.json(merged);
|
|
1267
1400
|
} catch (error) {
|
|
1268
1401
|
const authResponse = handleAuthError(error);
|
|
@@ -1284,8 +1417,7 @@ function createSignatureGetEndpoint(slugs) {
|
|
|
1284
1417
|
try {
|
|
1285
1418
|
const payload = req.payload;
|
|
1286
1419
|
requireAdmin(req, slugs);
|
|
1287
|
-
const prefs = await payload
|
|
1288
|
-
collection: "payload-preferences",
|
|
1420
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1289
1421
|
where: { key: { equals: `${PREF_KEY3}-${req.user.id}` } },
|
|
1290
1422
|
limit: 1,
|
|
1291
1423
|
depth: 0,
|
|
@@ -1312,8 +1444,7 @@ function createSignaturePostEndpoint(slugs) {
|
|
|
1312
1444
|
requireAdmin(req, slugs);
|
|
1313
1445
|
const { signature } = await req.json();
|
|
1314
1446
|
const key = `${PREF_KEY3}-${req.user.id}`;
|
|
1315
|
-
const existing = await payload
|
|
1316
|
-
collection: "payload-preferences",
|
|
1447
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
1317
1448
|
where: { key: { equals: key } },
|
|
1318
1449
|
limit: 1,
|
|
1319
1450
|
depth: 0,
|
|
@@ -1356,8 +1487,7 @@ function createRoundRobinConfigGetEndpoint(slugs) {
|
|
|
1356
1487
|
try {
|
|
1357
1488
|
const payload = req.payload;
|
|
1358
1489
|
requireAdmin(req, slugs);
|
|
1359
|
-
const prefs = await payload
|
|
1360
|
-
collection: "payload-preferences",
|
|
1490
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1361
1491
|
where: { key: { equals: PREF_KEY4 } },
|
|
1362
1492
|
limit: 1,
|
|
1363
1493
|
depth: 0,
|
|
@@ -1383,8 +1513,7 @@ function createRoundRobinConfigPostEndpoint(slugs) {
|
|
|
1383
1513
|
const payload = req.payload;
|
|
1384
1514
|
requireAdmin(req, slugs);
|
|
1385
1515
|
const { enabled } = await req.json();
|
|
1386
|
-
const existing = await payload
|
|
1387
|
-
collection: "payload-preferences",
|
|
1516
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
1388
1517
|
where: { key: { equals: PREF_KEY4 } },
|
|
1389
1518
|
limit: 1,
|
|
1390
1519
|
depth: 0,
|
|
@@ -1428,8 +1557,7 @@ function createSlaCheckEndpoint(slugs) {
|
|
|
1428
1557
|
requireAdmin(req, slugs);
|
|
1429
1558
|
const now = /* @__PURE__ */ new Date();
|
|
1430
1559
|
const nowISO = now.toISOString();
|
|
1431
|
-
const { docs: tickets } = await payload.
|
|
1432
|
-
collection: slugs.tickets,
|
|
1560
|
+
const { docs: tickets } = await dbFind(payload, slugs.tickets, {
|
|
1433
1561
|
where: {
|
|
1434
1562
|
and: [
|
|
1435
1563
|
{ status: { in: ["open", "waiting_client"] } },
|
|
@@ -1523,6 +1651,107 @@ function createSlaCheckEndpoint(slugs) {
|
|
|
1523
1651
|
}
|
|
1524
1652
|
};
|
|
1525
1653
|
}
|
|
1654
|
+
var TRANSPARENT_GIF = Buffer.from(
|
|
1655
|
+
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
1656
|
+
"base64"
|
|
1657
|
+
);
|
|
1658
|
+
function generateTrackingToken(ticketId, messageId, secret) {
|
|
1659
|
+
return crypto3.createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex").substring(0, 16);
|
|
1660
|
+
}
|
|
1661
|
+
function createTrackOpenEndpoint(slugs) {
|
|
1662
|
+
return {
|
|
1663
|
+
path: "/support/track-open",
|
|
1664
|
+
method: "get",
|
|
1665
|
+
handler: async (req) => {
|
|
1666
|
+
const url = new URL(req.url);
|
|
1667
|
+
const ticketId = url.searchParams.get("t");
|
|
1668
|
+
const messageId = url.searchParams.get("m");
|
|
1669
|
+
const sig = url.searchParams.get("sig");
|
|
1670
|
+
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
1671
|
+
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
1672
|
+
const secret = process.env.PAYLOAD_SECRET || "";
|
|
1673
|
+
const validSig = !!secret && !!ticketId && !!messageId && !!sig && sig === generateTrackingToken(ticketId, messageId, secret);
|
|
1674
|
+
if (!validSig) {
|
|
1675
|
+
return new Response(TRANSPARENT_GIF, {
|
|
1676
|
+
status: 200,
|
|
1677
|
+
headers: {
|
|
1678
|
+
"Content-Type": "image/gif",
|
|
1679
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
1680
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
1685
|
+
try {
|
|
1686
|
+
const payload = req.payload;
|
|
1687
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
1688
|
+
id: parsedId,
|
|
1689
|
+
depth: 0,
|
|
1690
|
+
overrideAccess: true,
|
|
1691
|
+
select: { lastClientReadAt: true }
|
|
1692
|
+
});
|
|
1693
|
+
if (ticket) {
|
|
1694
|
+
const lastRead = ticket.lastClientReadAt ? new Date(ticket.lastClientReadAt).getTime() : 0;
|
|
1695
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
|
|
1696
|
+
if (lastRead < fiveMinAgo) {
|
|
1697
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1698
|
+
id: parsedId,
|
|
1699
|
+
data: { lastClientReadAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1700
|
+
overrideAccess: true
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
1705
|
+
const msg = await dbFindByID(payload, slugs.ticketMessages, {
|
|
1706
|
+
id: parsedMsgId,
|
|
1707
|
+
depth: 0,
|
|
1708
|
+
overrideAccess: true,
|
|
1709
|
+
select: { emailOpenedAt: true }
|
|
1710
|
+
});
|
|
1711
|
+
if (msg && !msg.emailOpenedAt) {
|
|
1712
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
1713
|
+
id: parsedMsgId,
|
|
1714
|
+
data: { emailOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1715
|
+
overrideAccess: true
|
|
1716
|
+
});
|
|
1717
|
+
const ticketInfo = await dbFindByID(payload, slugs.tickets, {
|
|
1718
|
+
id: parsedId,
|
|
1719
|
+
depth: 1,
|
|
1720
|
+
overrideAccess: true,
|
|
1721
|
+
select: { ticketNumber: true, subject: true, client: true }
|
|
1722
|
+
});
|
|
1723
|
+
const clientName = typeof ticketInfo?.client === "object" ? ticketInfo.client?.firstName || "Client" : "Client";
|
|
1724
|
+
try {
|
|
1725
|
+
await dbCreate(payload, "admin-notifications", {
|
|
1726
|
+
data: {
|
|
1727
|
+
title: `Email ouvert \u2014 ${ticketInfo?.ticketNumber || "TK-????"}`,
|
|
1728
|
+
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || "ticket"}"`,
|
|
1729
|
+
type: "email_opened",
|
|
1730
|
+
link: `/admin/ticket?id=${parsedId}`
|
|
1731
|
+
},
|
|
1732
|
+
overrideAccess: true
|
|
1733
|
+
});
|
|
1734
|
+
} catch {
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
} catch (err) {
|
|
1739
|
+
console.error("[track-open] Error:", err);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return new Response(TRANSPARENT_GIF, {
|
|
1743
|
+
status: 200,
|
|
1744
|
+
headers: {
|
|
1745
|
+
"Content-Type": "image/gif",
|
|
1746
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
1747
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
1748
|
+
"Pragma": "no-cache",
|
|
1749
|
+
"Expires": "0"
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1526
1755
|
|
|
1527
1756
|
// src/utils/emailTemplate.ts
|
|
1528
1757
|
var COLORS = {
|
|
@@ -1559,7 +1788,10 @@ function escapeHtml(str) {
|
|
|
1559
1788
|
}
|
|
1560
1789
|
function emailTrackingPixel(ticketId, messageId, baseUrl) {
|
|
1561
1790
|
const url = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
1562
|
-
const
|
|
1791
|
+
const secret = process.env.PAYLOAD_SECRET || "";
|
|
1792
|
+
if (!secret || messageId === void 0 || messageId === null) return "";
|
|
1793
|
+
const sig = generateTrackingToken(String(ticketId), String(messageId), secret);
|
|
1794
|
+
const params = `t=${ticketId}&m=${messageId}&sig=${sig}`;
|
|
1563
1795
|
return `<img src="${url}/api/support/track-open?${params}" width="1" height="1" alt="" style="display:block;width:1px;height:1px;border:0;" />`;
|
|
1564
1796
|
}
|
|
1565
1797
|
function emailRichContent(html, config) {
|
|
@@ -1824,8 +2056,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1824
2056
|
const CLOSE_AFTER_REMIND_DAYS = settings.autoClose.reminderDaysBefore;
|
|
1825
2057
|
const results = { reminded: 0, closed: 0, errors: 0 };
|
|
1826
2058
|
const remindCutoff = new Date(now.getTime() - REMIND_AFTER_DAYS * 24 * 60 * 60 * 1e3);
|
|
1827
|
-
const ticketsToRemind = await payload.
|
|
1828
|
-
collection: slugs.tickets,
|
|
2059
|
+
const ticketsToRemind = await dbFind(payload, slugs.tickets, {
|
|
1829
2060
|
where: {
|
|
1830
2061
|
and: [
|
|
1831
2062
|
{ status: { equals: "waiting_client" } },
|
|
@@ -1856,8 +2087,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1856
2087
|
<p><a href="${portalUrl}">R\xE9pondre au ticket</a></p>
|
|
1857
2088
|
</div>`
|
|
1858
2089
|
});
|
|
1859
|
-
await payload.
|
|
1860
|
-
collection: slugs.tickets,
|
|
2090
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1861
2091
|
id: t.id,
|
|
1862
2092
|
data: { autoCloseRemindedAt: now.toISOString() },
|
|
1863
2093
|
overrideAccess: true
|
|
@@ -1869,8 +2099,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1869
2099
|
}
|
|
1870
2100
|
}
|
|
1871
2101
|
const closeCutoff = new Date(now.getTime() - CLOSE_AFTER_REMIND_DAYS * 24 * 60 * 60 * 1e3);
|
|
1872
|
-
const ticketsToClose = await payload.
|
|
1873
|
-
collection: slugs.tickets,
|
|
2102
|
+
const ticketsToClose = await dbFind(payload, slugs.tickets, {
|
|
1874
2103
|
where: {
|
|
1875
2104
|
and: [
|
|
1876
2105
|
{ status: { equals: "waiting_client" } },
|
|
@@ -1905,8 +2134,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1905
2134
|
const closeTotalDays = REMIND_AFTER_DAYS + CLOSE_AFTER_REMIND_DAYS;
|
|
1906
2135
|
const viaSchedule = !!t.autoCloseScheduledAt && new Date(t.autoCloseScheduledAt).getTime() <= now.getTime();
|
|
1907
2136
|
const noteBody = viaSchedule ? "Ticket r\xE9solu automatiquement \u2014 relance manuelle rest\xE9e sans r\xE9ponse du client" : `Ticket r\xE9solu automatiquement \u2014 sans r\xE9ponse client depuis ${closeTotalDays} jours`;
|
|
1908
|
-
await payload.
|
|
1909
|
-
collection: slugs.ticketMessages,
|
|
2137
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
1910
2138
|
data: {
|
|
1911
2139
|
ticket: t.id,
|
|
1912
2140
|
body: noteBody,
|
|
@@ -1916,8 +2144,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1916
2144
|
},
|
|
1917
2145
|
overrideAccess: true
|
|
1918
2146
|
});
|
|
1919
|
-
await payload.
|
|
1920
|
-
collection: slugs.tickets,
|
|
2147
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1921
2148
|
id: t.id,
|
|
1922
2149
|
// Clear the armed deadline so a later manual reopen can't be
|
|
1923
2150
|
// closed instantly by a stale timestamp.
|
|
@@ -2040,8 +2267,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2040
2267
|
}
|
|
2041
2268
|
const rawHours = Number(body.hours);
|
|
2042
2269
|
const hours = Number.isFinite(rawHours) ? Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.round(rawHours))) : 24;
|
|
2043
|
-
const ticket = await payload.
|
|
2044
|
-
collection: slugs.tickets,
|
|
2270
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2045
2271
|
id: ticketId,
|
|
2046
2272
|
depth: 1,
|
|
2047
2273
|
overrideAccess: true
|
|
@@ -2063,8 +2289,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2063
2289
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_REPLY_TO || "";
|
|
2064
2290
|
let waitingSince = ticket.firstResponseAt || ticket.createdAt;
|
|
2065
2291
|
try {
|
|
2066
|
-
const lastAdminMsg = await payload.
|
|
2067
|
-
collection: slugs.ticketMessages,
|
|
2292
|
+
const lastAdminMsg = await dbFind(payload, slugs.ticketMessages, {
|
|
2068
2293
|
where: {
|
|
2069
2294
|
and: [
|
|
2070
2295
|
{ ticket: { equals: ticketId } },
|
|
@@ -2105,8 +2330,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2105
2330
|
preheader: `Sans r\xE9ponse de votre part, votre ticket ${ticketNumber} sera cl\xF4tur\xE9 le ${deadlineLabel}.`
|
|
2106
2331
|
})
|
|
2107
2332
|
});
|
|
2108
|
-
await payload.
|
|
2109
|
-
collection: slugs.ticketMessages,
|
|
2333
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2110
2334
|
data: {
|
|
2111
2335
|
ticket: ticketId,
|
|
2112
2336
|
body: `Relance envoy\xE9e \xE0 ${client.email}. Fermeture automatique programm\xE9e le ${deadlineLabel} sans r\xE9ponse du client.`,
|
|
@@ -2116,8 +2340,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2116
2340
|
},
|
|
2117
2341
|
overrideAccess: true
|
|
2118
2342
|
});
|
|
2119
|
-
await payload.
|
|
2120
|
-
collection: slugs.tickets,
|
|
2343
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2121
2344
|
id: ticketId,
|
|
2122
2345
|
data: {
|
|
2123
2346
|
status: "waiting_client",
|
|
@@ -2152,8 +2375,7 @@ function createStatusesEndpoint(slugs) {
|
|
|
2152
2375
|
if (!req.user) {
|
|
2153
2376
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2154
2377
|
}
|
|
2155
|
-
const { docs } = await payload.
|
|
2156
|
-
collection: slugs.ticketStatuses,
|
|
2378
|
+
const { docs } = await dbFind(payload, slugs.ticketStatuses, {
|
|
2157
2379
|
sort: "sortOrder",
|
|
2158
2380
|
limit: 100,
|
|
2159
2381
|
depth: 0,
|
|
@@ -2192,16 +2414,14 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2192
2414
|
if (!macroId || !ticketId) {
|
|
2193
2415
|
return Response.json({ error: "macroId and ticketId are required" }, { status: 400 });
|
|
2194
2416
|
}
|
|
2195
|
-
const macro = await payload.
|
|
2196
|
-
collection: slugs.macros,
|
|
2417
|
+
const macro = await dbFindByID(payload, slugs.macros, {
|
|
2197
2418
|
id: macroId,
|
|
2198
2419
|
depth: 0,
|
|
2199
2420
|
overrideAccess: true
|
|
2200
2421
|
});
|
|
2201
2422
|
if (!macro) return Response.json({ error: "Macro not found" }, { status: 404 });
|
|
2202
2423
|
if (!macro.isActive) return Response.json({ error: "Macro is disabled" }, { status: 400 });
|
|
2203
|
-
const ticket = await payload.
|
|
2204
|
-
collection: slugs.tickets,
|
|
2424
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2205
2425
|
id: ticketId,
|
|
2206
2426
|
depth: 0,
|
|
2207
2427
|
overrideAccess: true
|
|
@@ -2213,8 +2433,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2213
2433
|
try {
|
|
2214
2434
|
switch (action.type) {
|
|
2215
2435
|
case "set_status":
|
|
2216
|
-
await payload.
|
|
2217
|
-
collection: slugs.tickets,
|
|
2436
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2218
2437
|
id: ticketId,
|
|
2219
2438
|
data: { status: action.value },
|
|
2220
2439
|
overrideAccess: true
|
|
@@ -2222,8 +2441,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2222
2441
|
appliedActions.push({ type: action.type, value: action.value, success: true });
|
|
2223
2442
|
break;
|
|
2224
2443
|
case "set_priority":
|
|
2225
|
-
await payload.
|
|
2226
|
-
collection: slugs.tickets,
|
|
2444
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2227
2445
|
id: ticketId,
|
|
2228
2446
|
data: { priority: action.value },
|
|
2229
2447
|
overrideAccess: true
|
|
@@ -2235,8 +2453,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2235
2453
|
if (!currentTags.includes(action.value)) {
|
|
2236
2454
|
currentTags.push(action.value);
|
|
2237
2455
|
}
|
|
2238
|
-
await payload.
|
|
2239
|
-
collection: slugs.tickets,
|
|
2456
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2240
2457
|
id: ticketId,
|
|
2241
2458
|
data: { tags: currentTags },
|
|
2242
2459
|
overrideAccess: true
|
|
@@ -2245,8 +2462,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2245
2462
|
break;
|
|
2246
2463
|
}
|
|
2247
2464
|
case "send_reply":
|
|
2248
|
-
await payload.
|
|
2249
|
-
collection: slugs.ticketMessages,
|
|
2465
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2250
2466
|
data: {
|
|
2251
2467
|
ticket: ticketId,
|
|
2252
2468
|
body: action.value,
|
|
@@ -2263,8 +2479,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2263
2479
|
appliedActions.push({ type: action.type, value: action.value, success: false, error: "Invalid user ID" });
|
|
2264
2480
|
break;
|
|
2265
2481
|
}
|
|
2266
|
-
await payload.
|
|
2267
|
-
collection: slugs.tickets,
|
|
2482
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2268
2483
|
id: ticketId,
|
|
2269
2484
|
data: { assignedTo: userId },
|
|
2270
2485
|
overrideAccess: true
|
|
@@ -2316,8 +2531,7 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2316
2531
|
return Response.json({ error: "Invalid collection. Use email-logs or auth-logs." }, { status: 400 });
|
|
2317
2532
|
}
|
|
2318
2533
|
const cutoff = days > 0 ? new Date(Date.now() - days * 864e5).toISOString() : null;
|
|
2319
|
-
const result = await payload
|
|
2320
|
-
collection: allowedCollections[collection],
|
|
2534
|
+
const result = await dbDelete(payload, allowedCollections[collection], {
|
|
2321
2535
|
where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
|
|
2322
2536
|
overrideAccess: true
|
|
2323
2537
|
});
|
|
@@ -2360,8 +2574,7 @@ function createChatbotEndpoint(slugs) {
|
|
|
2360
2574
|
return Response.json({ error: "Question too short" }, { status: 400 });
|
|
2361
2575
|
}
|
|
2362
2576
|
const payload = req.payload;
|
|
2363
|
-
const articles = await payload.
|
|
2364
|
-
collection: slugs.knowledgeBase,
|
|
2577
|
+
const articles = await dbFind(payload, slugs.knowledgeBase, {
|
|
2365
2578
|
where: { published: { equals: true } },
|
|
2366
2579
|
limit: 100,
|
|
2367
2580
|
depth: 0,
|
|
@@ -2451,8 +2664,7 @@ function createChatGetEndpoint(slugs) {
|
|
|
2451
2664
|
if (after) {
|
|
2452
2665
|
where.createdAt = { greater_than: after };
|
|
2453
2666
|
}
|
|
2454
|
-
const messages = await payload.
|
|
2455
|
-
collection: slugs.chatMessages,
|
|
2667
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2456
2668
|
where,
|
|
2457
2669
|
sort: "createdAt",
|
|
2458
2670
|
limit: 100,
|
|
@@ -2495,8 +2707,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2495
2707
|
return Response.json({ error: "Trop de sessions cr\xE9\xE9es. R\xE9essayez plus tard." }, { status: 429 });
|
|
2496
2708
|
}
|
|
2497
2709
|
const sessionId = `chat_${crypto3__default.default.randomBytes(16).toString("hex")}`;
|
|
2498
|
-
const systemMsg = await payload.
|
|
2499
|
-
collection: slugs.chatMessages,
|
|
2710
|
+
const systemMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2500
2711
|
data: {
|
|
2501
2712
|
session: sessionId,
|
|
2502
2713
|
client: req.user.id,
|
|
@@ -2516,8 +2727,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2516
2727
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2517
2728
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2518
2729
|
}
|
|
2519
|
-
const existing = await payload.
|
|
2520
|
-
collection: slugs.chatMessages,
|
|
2730
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2521
2731
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2522
2732
|
limit: 1,
|
|
2523
2733
|
overrideAccess: true
|
|
@@ -2525,8 +2735,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2525
2735
|
if (existing.docs.length === 0) {
|
|
2526
2736
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2527
2737
|
}
|
|
2528
|
-
const newMsg = await payload.
|
|
2529
|
-
collection: slugs.chatMessages,
|
|
2738
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2530
2739
|
data: {
|
|
2531
2740
|
session,
|
|
2532
2741
|
client: req.user.id,
|
|
@@ -2539,8 +2748,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2539
2748
|
return Response.json({ message: newMsg });
|
|
2540
2749
|
}
|
|
2541
2750
|
if (action === "close" && session) {
|
|
2542
|
-
const existing = await payload.
|
|
2543
|
-
collection: slugs.chatMessages,
|
|
2751
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2544
2752
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2545
2753
|
limit: 1,
|
|
2546
2754
|
overrideAccess: true
|
|
@@ -2548,8 +2756,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2548
2756
|
if (existing.docs.length === 0) {
|
|
2549
2757
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2550
2758
|
}
|
|
2551
|
-
await payload.
|
|
2552
|
-
collection: slugs.chatMessages,
|
|
2759
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2553
2760
|
data: {
|
|
2554
2761
|
session,
|
|
2555
2762
|
client: req.user.id,
|
|
@@ -2591,8 +2798,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2591
2798
|
if (!sessionId) {
|
|
2592
2799
|
return Response.json({ error: "Missing session parameter" }, { status: 400 });
|
|
2593
2800
|
}
|
|
2594
|
-
const existing = await req.payload.
|
|
2595
|
-
collection: slugs.chatMessages,
|
|
2801
|
+
const existing = await dbFind(req.payload, slugs.chatMessages, {
|
|
2596
2802
|
where: {
|
|
2597
2803
|
session: { equals: sessionId },
|
|
2598
2804
|
client: { equals: userId }
|
|
@@ -2614,8 +2820,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2614
2820
|
);
|
|
2615
2821
|
const interval = setInterval(async () => {
|
|
2616
2822
|
try {
|
|
2617
|
-
const messages = await req.payload.
|
|
2618
|
-
collection: slugs.chatMessages,
|
|
2823
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2619
2824
|
where: {
|
|
2620
2825
|
session: { equals: sessionId },
|
|
2621
2826
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2696,8 +2901,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2696
2901
|
if (session) {
|
|
2697
2902
|
const where = { session: { equals: session } };
|
|
2698
2903
|
if (after) where.createdAt = { greater_than: after };
|
|
2699
|
-
const messages = await payload.
|
|
2700
|
-
collection: slugs.chatMessages,
|
|
2904
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2701
2905
|
where,
|
|
2702
2906
|
sort: "createdAt",
|
|
2703
2907
|
limit: 200,
|
|
@@ -2711,8 +2915,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2711
2915
|
}
|
|
2712
2916
|
});
|
|
2713
2917
|
}
|
|
2714
|
-
const recentMessages = await payload.
|
|
2715
|
-
collection: slugs.chatMessages,
|
|
2918
|
+
const recentMessages = await dbFind(payload, slugs.chatMessages, {
|
|
2716
2919
|
sort: "-createdAt",
|
|
2717
2920
|
limit: 50,
|
|
2718
2921
|
depth: 1,
|
|
@@ -2775,8 +2978,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2775
2978
|
if (!session) {
|
|
2776
2979
|
return Response.json({ error: "Session requise" }, { status: 400 });
|
|
2777
2980
|
}
|
|
2778
|
-
const sessionMsg = await payload.
|
|
2779
|
-
collection: slugs.chatMessages,
|
|
2981
|
+
const sessionMsg = await dbFind(payload, slugs.chatMessages, {
|
|
2780
2982
|
where: { session: { equals: session } },
|
|
2781
2983
|
limit: 1,
|
|
2782
2984
|
depth: 0,
|
|
@@ -2794,8 +2996,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2794
2996
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2795
2997
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2796
2998
|
}
|
|
2797
|
-
const newMsg = await payload.
|
|
2798
|
-
collection: slugs.chatMessages,
|
|
2999
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2799
3000
|
data: {
|
|
2800
3001
|
session,
|
|
2801
3002
|
client: clientId,
|
|
@@ -2807,8 +3008,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2807
3008
|
overrideAccess: true
|
|
2808
3009
|
});
|
|
2809
3010
|
try {
|
|
2810
|
-
const linkedTicket = await payload.
|
|
2811
|
-
collection: slugs.tickets,
|
|
3011
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2812
3012
|
where: { chatSession: { equals: session } },
|
|
2813
3013
|
limit: 1,
|
|
2814
3014
|
depth: 0,
|
|
@@ -2816,8 +3016,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2816
3016
|
});
|
|
2817
3017
|
if (linkedTicket.docs.length > 0) {
|
|
2818
3018
|
const ticketId = linkedTicket.docs[0].id;
|
|
2819
|
-
await payload.
|
|
2820
|
-
collection: slugs.ticketMessages,
|
|
3019
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2821
3020
|
data: {
|
|
2822
3021
|
ticket: ticketId,
|
|
2823
3022
|
body: trimmedMessage,
|
|
@@ -2826,8 +3025,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2826
3025
|
},
|
|
2827
3026
|
overrideAccess: true
|
|
2828
3027
|
});
|
|
2829
|
-
await payload.
|
|
2830
|
-
collection: slugs.chatMessages,
|
|
3028
|
+
await dbUpdate(payload, slugs.chatMessages, {
|
|
2831
3029
|
id: newMsg.id,
|
|
2832
3030
|
data: { ticket: ticketId },
|
|
2833
3031
|
overrideAccess: true
|
|
@@ -2839,8 +3037,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2839
3037
|
return Response.json({ message: newMsg });
|
|
2840
3038
|
}
|
|
2841
3039
|
if (action === "close") {
|
|
2842
|
-
await payload.
|
|
2843
|
-
collection: slugs.chatMessages,
|
|
3040
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2844
3041
|
data: {
|
|
2845
3042
|
session,
|
|
2846
3043
|
client: clientId,
|
|
@@ -2851,16 +3048,14 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2851
3048
|
overrideAccess: true
|
|
2852
3049
|
});
|
|
2853
3050
|
try {
|
|
2854
|
-
const linkedTicket = await payload.
|
|
2855
|
-
collection: slugs.tickets,
|
|
3051
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2856
3052
|
where: { chatSession: { equals: session } },
|
|
2857
3053
|
limit: 1,
|
|
2858
3054
|
depth: 0,
|
|
2859
3055
|
overrideAccess: true
|
|
2860
3056
|
});
|
|
2861
3057
|
if (linkedTicket.docs.length > 0) {
|
|
2862
|
-
await payload.
|
|
2863
|
-
collection: slugs.ticketMessages,
|
|
3058
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2864
3059
|
data: {
|
|
2865
3060
|
ticket: linkedTicket.docs[0].id,
|
|
2866
3061
|
body: "Session de chat termin\xE9e par un agent.",
|
|
@@ -2923,8 +3118,7 @@ function createSessionStream(req, slugs, sessionId) {
|
|
|
2923
3118
|
);
|
|
2924
3119
|
const interval = setInterval(async () => {
|
|
2925
3120
|
try {
|
|
2926
|
-
const messages = await req.payload.
|
|
2927
|
-
collection: slugs.chatMessages,
|
|
3121
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2928
3122
|
where: {
|
|
2929
3123
|
session: { equals: sessionId },
|
|
2930
3124
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2997,8 +3191,7 @@ function createSessionListStream(req, slugs) {
|
|
|
2997
3191
|
);
|
|
2998
3192
|
const interval = setInterval(async () => {
|
|
2999
3193
|
try {
|
|
3000
|
-
const recentMessages = await req.payload.
|
|
3001
|
-
collection: slugs.chatMessages,
|
|
3194
|
+
const recentMessages = await dbFind(req.payload, slugs.chatMessages, {
|
|
3002
3195
|
sort: "-createdAt",
|
|
3003
3196
|
limit: 50,
|
|
3004
3197
|
depth: 1,
|
|
@@ -3085,8 +3278,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3085
3278
|
const statuses = ["open", "waiting_client", "resolved"];
|
|
3086
3279
|
const statusCounts = await Promise.all(
|
|
3087
3280
|
statuses.map(async (status) => {
|
|
3088
|
-
const result = await payload.
|
|
3089
|
-
collection: slugs.tickets,
|
|
3281
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3090
3282
|
where: { status: { equals: status } },
|
|
3091
3283
|
overrideAccess: true
|
|
3092
3284
|
});
|
|
@@ -3098,8 +3290,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3098
3290
|
const priorities = ["low", "normal", "high", "urgent"];
|
|
3099
3291
|
const priorityCounts = await Promise.all(
|
|
3100
3292
|
priorities.map(async (priority) => {
|
|
3101
|
-
const result = await payload.
|
|
3102
|
-
collection: slugs.tickets,
|
|
3293
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3103
3294
|
where: { priority: { equals: priority } },
|
|
3104
3295
|
overrideAccess: true
|
|
3105
3296
|
});
|
|
@@ -3110,8 +3301,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3110
3301
|
const categories = ["bug", "content", "feature", "question", "hosting"];
|
|
3111
3302
|
const categoryCounts = await Promise.all(
|
|
3112
3303
|
categories.map(async (category) => {
|
|
3113
|
-
const result = await payload.
|
|
3114
|
-
collection: slugs.tickets,
|
|
3304
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3115
3305
|
where: { category: { equals: category } },
|
|
3116
3306
|
overrideAccess: true
|
|
3117
3307
|
});
|
|
@@ -3125,17 +3315,34 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3125
3315
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
|
|
3126
3316
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
3127
3317
|
const [created7, created30] = await Promise.all([
|
|
3128
|
-
payload.
|
|
3129
|
-
collection: slugs.tickets,
|
|
3318
|
+
dbCount(payload, slugs.tickets, {
|
|
3130
3319
|
where: { createdAt: { greater_than_equal: sevenDaysAgo.toISOString() } },
|
|
3131
3320
|
overrideAccess: true
|
|
3132
3321
|
}),
|
|
3133
|
-
payload.
|
|
3134
|
-
collection: slugs.tickets,
|
|
3322
|
+
dbCount(payload, slugs.tickets, {
|
|
3135
3323
|
where: { createdAt: { greater_than_equal: thirtyDaysAgo.toISOString() } },
|
|
3136
3324
|
overrideAccess: true
|
|
3137
3325
|
})
|
|
3138
3326
|
]);
|
|
3327
|
+
const volumeByDay = await Promise.all(
|
|
3328
|
+
Array.from({ length: 7 }, (_, i) => i).map(async (offset) => {
|
|
3329
|
+
const dayStart = new Date(now);
|
|
3330
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
3331
|
+
dayStart.setDate(dayStart.getDate() - (6 - offset));
|
|
3332
|
+
const dayEnd = new Date(dayStart);
|
|
3333
|
+
dayEnd.setDate(dayEnd.getDate() + 1);
|
|
3334
|
+
const r = await dbCount(payload, slugs.tickets, {
|
|
3335
|
+
where: {
|
|
3336
|
+
and: [
|
|
3337
|
+
{ createdAt: { greater_than_equal: dayStart.toISOString() } },
|
|
3338
|
+
{ createdAt: { less_than: dayEnd.toISOString() } }
|
|
3339
|
+
]
|
|
3340
|
+
},
|
|
3341
|
+
overrideAccess: true
|
|
3342
|
+
});
|
|
3343
|
+
return { date: dayStart.toISOString(), count: r.totalDocs };
|
|
3344
|
+
})
|
|
3345
|
+
);
|
|
3139
3346
|
const PAGE_SIZE2 = 100;
|
|
3140
3347
|
const MAX_PAGES2 = 50;
|
|
3141
3348
|
let totalResponseTimeMs = 0;
|
|
@@ -3146,8 +3353,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3146
3353
|
let page = 1;
|
|
3147
3354
|
let hasMore = true;
|
|
3148
3355
|
while (hasMore && page <= MAX_PAGES2) {
|
|
3149
|
-
const batch = await payload.
|
|
3150
|
-
collection: slugs.tickets,
|
|
3356
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3151
3357
|
limit: PAGE_SIZE2,
|
|
3152
3358
|
page,
|
|
3153
3359
|
depth: 0,
|
|
@@ -3180,43 +3386,62 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3180
3386
|
hasMore = batch.hasNextPage ?? false;
|
|
3181
3387
|
page++;
|
|
3182
3388
|
}
|
|
3183
|
-
const surveyCount = await payload.
|
|
3184
|
-
collection: slugs.satisfactionSurveys,
|
|
3389
|
+
const surveyCount = await dbCount(payload, slugs.satisfactionSurveys, {
|
|
3185
3390
|
overrideAccess: true
|
|
3186
3391
|
});
|
|
3187
3392
|
let satisfactionAvg = 0;
|
|
3393
|
+
let satisfactionCount = 0;
|
|
3394
|
+
let npsScore = null;
|
|
3395
|
+
let npsCount = 0;
|
|
3188
3396
|
if (surveyCount.totalDocs > 0) {
|
|
3189
|
-
let
|
|
3397
|
+
let ratingSum = 0;
|
|
3398
|
+
let ratingN = 0;
|
|
3399
|
+
let promoters = 0;
|
|
3400
|
+
let detractors = 0;
|
|
3401
|
+
let npsN = 0;
|
|
3190
3402
|
let surveyPage = 1;
|
|
3191
3403
|
let surveyHasMore = true;
|
|
3192
3404
|
while (surveyHasMore && surveyPage <= MAX_PAGES2) {
|
|
3193
|
-
const batch = await payload.
|
|
3194
|
-
collection: slugs.satisfactionSurveys,
|
|
3405
|
+
const batch = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
3195
3406
|
limit: PAGE_SIZE2,
|
|
3196
3407
|
page: surveyPage,
|
|
3197
3408
|
depth: 0,
|
|
3198
3409
|
overrideAccess: true,
|
|
3199
|
-
select: { rating: true }
|
|
3410
|
+
select: { rating: true, nps: true }
|
|
3200
3411
|
});
|
|
3201
3412
|
for (const s of batch.docs) {
|
|
3202
|
-
|
|
3413
|
+
const r = s.rating;
|
|
3414
|
+
if (typeof r === "number" && r > 0) {
|
|
3415
|
+
ratingSum += r;
|
|
3416
|
+
ratingN++;
|
|
3417
|
+
}
|
|
3418
|
+
const n = s.nps;
|
|
3419
|
+
if (typeof n === "number") {
|
|
3420
|
+
npsN++;
|
|
3421
|
+
if (n >= 9) promoters++;
|
|
3422
|
+
else if (n <= 6) detractors++;
|
|
3423
|
+
}
|
|
3203
3424
|
}
|
|
3204
3425
|
surveyHasMore = batch.hasNextPage ?? false;
|
|
3205
3426
|
surveyPage++;
|
|
3206
3427
|
}
|
|
3207
|
-
satisfactionAvg = Math.round(
|
|
3428
|
+
satisfactionAvg = ratingN > 0 ? Math.round(ratingSum / ratingN * 10) / 10 : 0;
|
|
3429
|
+
satisfactionCount = ratingN;
|
|
3430
|
+
if (npsN > 0) {
|
|
3431
|
+
npsScore = Math.round((promoters - detractors) / npsN * 100);
|
|
3432
|
+
npsCount = npsN;
|
|
3433
|
+
}
|
|
3208
3434
|
}
|
|
3209
|
-
const
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
}),
|
|
3214
|
-
payload.count({
|
|
3215
|
-
collection: slugs.pendingEmails,
|
|
3435
|
+
const clientCount = await dbCount(payload, slugs.supportClients, { overrideAccess: true });
|
|
3436
|
+
let pendingEmailsTotal = 0;
|
|
3437
|
+
try {
|
|
3438
|
+
const pe = await dbCount(payload, slugs.pendingEmails, {
|
|
3216
3439
|
where: { status: { equals: "pending" } },
|
|
3217
3440
|
overrideAccess: true
|
|
3218
|
-
})
|
|
3219
|
-
|
|
3441
|
+
});
|
|
3442
|
+
pendingEmailsTotal = pe.totalDocs;
|
|
3443
|
+
} catch {
|
|
3444
|
+
}
|
|
3220
3445
|
return new Response(JSON.stringify({
|
|
3221
3446
|
total,
|
|
3222
3447
|
byStatus,
|
|
@@ -3224,13 +3449,16 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3224
3449
|
byCategory,
|
|
3225
3450
|
createdLast7Days: created7.totalDocs,
|
|
3226
3451
|
createdLast30Days: created30.totalDocs,
|
|
3452
|
+
volumeByDay,
|
|
3227
3453
|
avgResponseTimeHours: responseTimeCount > 0 ? Math.round(totalResponseTimeMs / responseTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3228
3454
|
avgResolutionTimeHours: resolutionTimeCount > 0 ? Math.round(totalResolutionTimeMs / resolutionTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3229
3455
|
totalTimeMinutes,
|
|
3230
3456
|
satisfactionAvg,
|
|
3231
|
-
satisfactionCount
|
|
3457
|
+
satisfactionCount,
|
|
3458
|
+
npsScore,
|
|
3459
|
+
npsCount,
|
|
3232
3460
|
clientCount: clientCount.totalDocs,
|
|
3233
|
-
pendingEmailsCount:
|
|
3461
|
+
pendingEmailsCount: pendingEmailsTotal
|
|
3234
3462
|
}), {
|
|
3235
3463
|
headers: {
|
|
3236
3464
|
"Content-Type": "application/json",
|
|
@@ -3300,13 +3528,26 @@ function createBillingEndpoint(slugs) {
|
|
|
3300
3528
|
let ticketPage = 1;
|
|
3301
3529
|
let ticketHasMore = true;
|
|
3302
3530
|
while (ticketHasMore && ticketPage <= MAX_PAGES) {
|
|
3303
|
-
const batch = await payload.
|
|
3304
|
-
collection: slugs.tickets,
|
|
3531
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3305
3532
|
where: ticketWhere,
|
|
3306
3533
|
limit: PAGE_SIZE,
|
|
3307
3534
|
page: ticketPage,
|
|
3535
|
+
// depth 2 is required: we read ticket.project.client.company below.
|
|
3308
3536
|
depth: 2,
|
|
3309
|
-
overrideAccess: true
|
|
3537
|
+
overrideAccess: true,
|
|
3538
|
+
// Bound the payload to only the fields the report consumes (skips body,
|
|
3539
|
+
// richText, SLA fields, etc.) while keeping project/client populated.
|
|
3540
|
+
select: {
|
|
3541
|
+
ticketNumber: true,
|
|
3542
|
+
subject: true,
|
|
3543
|
+
status: true,
|
|
3544
|
+
billedAmount: true,
|
|
3545
|
+
aiSummary: true,
|
|
3546
|
+
aiSummaryGeneratedAt: true,
|
|
3547
|
+
aiSummaryStatus: true,
|
|
3548
|
+
project: true,
|
|
3549
|
+
client: true
|
|
3550
|
+
}
|
|
3310
3551
|
});
|
|
3311
3552
|
allTickets.push(...batch.docs);
|
|
3312
3553
|
ticketHasMore = batch.hasNextPage ?? false;
|
|
@@ -3316,8 +3557,7 @@ function createBillingEndpoint(slugs) {
|
|
|
3316
3557
|
let entryPage = 1;
|
|
3317
3558
|
let entryHasMore = true;
|
|
3318
3559
|
while (entryHasMore && entryPage <= MAX_PAGES) {
|
|
3319
|
-
const batch = await payload.
|
|
3320
|
-
collection: slugs.timeEntries,
|
|
3560
|
+
const batch = await dbFind(payload, slugs.timeEntries, {
|
|
3321
3561
|
where: {
|
|
3322
3562
|
and: [
|
|
3323
3563
|
{ date: { greater_than_equal: from } },
|
|
@@ -3415,43 +3655,205 @@ function createBillingEndpoint(slugs) {
|
|
|
3415
3655
|
}
|
|
3416
3656
|
};
|
|
3417
3657
|
}
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
function
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3658
|
+
var fmtAmount = (n) => `${n.toFixed(2)} EUR`;
|
|
3659
|
+
var fmtTime = (min) => `${Math.floor(min / 60)}h${String(min % 60).padStart(2, "0")}`;
|
|
3660
|
+
function generateInvoicePdf(data) {
|
|
3661
|
+
return new Promise((resolve, reject) => {
|
|
3662
|
+
try {
|
|
3663
|
+
const doc = new PDFDocument__default.default({ size: "A4", margin: 50 });
|
|
3664
|
+
const chunks = [];
|
|
3665
|
+
doc.on("data", (c) => chunks.push(c));
|
|
3666
|
+
doc.on("end", () => resolve(Buffer.concat(chunks)));
|
|
3667
|
+
doc.on("error", reject);
|
|
3668
|
+
doc.fillColor("#0f172a").fontSize(22).text("Invoice / Pre-billing");
|
|
3669
|
+
doc.moveDown(0.2);
|
|
3670
|
+
doc.fillColor("#64748b").fontSize(10).text(`Period: ${data.from} -> ${data.to}${data.projectId ? ` \xB7 Project #${data.projectId}` : ""}`);
|
|
3671
|
+
doc.moveDown(1);
|
|
3672
|
+
const X = { ticket: 50, subject: 130, client: 320, time: 430, amount: 500 };
|
|
3673
|
+
const drawRow = (r, opts) => {
|
|
3674
|
+
const y = doc.y;
|
|
3675
|
+
doc.fontSize(opts?.header ? 9 : 10).fillColor(opts?.header ? "#475569" : "#0f172a");
|
|
3676
|
+
if (opts?.total) doc.font("Helvetica-Bold");
|
|
3677
|
+
doc.text(r.ticket, X.ticket, y, { width: X.subject - X.ticket - 6 });
|
|
3678
|
+
doc.text(r.subject, X.subject, y, { width: X.client - X.subject - 6 });
|
|
3679
|
+
doc.text(r.client, X.client, y, { width: X.time - X.client - 6 });
|
|
3680
|
+
doc.text(r.time, X.time, y, { width: X.amount - X.time - 6, align: "right" });
|
|
3681
|
+
doc.text(r.amount, X.amount, y, { width: 545 - X.amount, align: "right" });
|
|
3682
|
+
doc.font("Helvetica");
|
|
3683
|
+
doc.moveDown(0.4);
|
|
3684
|
+
};
|
|
3685
|
+
drawRow({ ticket: "Ticket", subject: "Subject", client: "Client", time: "Time", amount: "Amount" }, { header: true });
|
|
3686
|
+
doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor("#e2e8f0").stroke();
|
|
3687
|
+
doc.moveDown(0.3);
|
|
3688
|
+
for (const l of data.lines) {
|
|
3689
|
+
if (doc.y > 760) doc.addPage();
|
|
3690
|
+
drawRow({
|
|
3691
|
+
ticket: l.ticketNumber || "",
|
|
3692
|
+
subject: l.subject || "",
|
|
3693
|
+
client: l.client || "",
|
|
3694
|
+
time: fmtTime(l.minutes),
|
|
3695
|
+
amount: fmtAmount(l.amount)
|
|
3696
|
+
});
|
|
3697
|
+
}
|
|
3698
|
+
doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor("#0f172a").stroke();
|
|
3699
|
+
doc.moveDown(0.3);
|
|
3700
|
+
drawRow({
|
|
3701
|
+
ticket: "",
|
|
3702
|
+
subject: `Total (${data.lines.length} ticket${data.lines.length > 1 ? "s" : ""})`,
|
|
3703
|
+
client: "",
|
|
3704
|
+
time: fmtTime(data.totalMinutes),
|
|
3705
|
+
amount: fmtAmount(data.totalAmount)
|
|
3706
|
+
}, { total: true });
|
|
3707
|
+
doc.end();
|
|
3708
|
+
} catch (err) {
|
|
3709
|
+
reject(err);
|
|
3710
|
+
}
|
|
3442
3711
|
});
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
// src/endpoints/invoice.ts
|
|
3715
|
+
function createInvoiceEndpoint(slugs) {
|
|
3716
|
+
return {
|
|
3717
|
+
path: "/support/billing/invoice",
|
|
3718
|
+
method: "get",
|
|
3719
|
+
handler: async (req) => {
|
|
3720
|
+
try {
|
|
3721
|
+
requireAdmin(req, slugs);
|
|
3722
|
+
const payload = req.payload;
|
|
3723
|
+
const url = new URL(req.url);
|
|
3724
|
+
const from = url.searchParams.get("from");
|
|
3725
|
+
const to = url.searchParams.get("to");
|
|
3726
|
+
const projectId = url.searchParams.get("projectId");
|
|
3727
|
+
const format = url.searchParams.get("format") === "pdf" ? "pdf" : "html";
|
|
3728
|
+
if (!from || !to) {
|
|
3729
|
+
return Response.json({ error: "Param\xE8tres from et to requis." }, { status: 400 });
|
|
3730
|
+
}
|
|
3731
|
+
const toExclusive = new Date(to);
|
|
3732
|
+
toExclusive.setDate(toExclusive.getDate() + 1);
|
|
3733
|
+
const toIso = toExclusive.toISOString();
|
|
3734
|
+
const where = {
|
|
3735
|
+
and: [
|
|
3736
|
+
{ billable: { equals: true } },
|
|
3737
|
+
...projectId ? [{ project: { equals: Number(projectId) } }] : [],
|
|
3738
|
+
{
|
|
3739
|
+
or: [
|
|
3740
|
+
{ and: [{ updatedAt: { greater_than_equal: from } }, { updatedAt: { less_than: toIso } }] },
|
|
3741
|
+
{ and: [{ createdAt: { greater_than_equal: from } }, { createdAt: { less_than: toIso } }] },
|
|
3742
|
+
{ and: [{ resolvedAt: { greater_than_equal: from } }, { resolvedAt: { less_than: toIso } }] }
|
|
3743
|
+
]
|
|
3744
|
+
}
|
|
3745
|
+
]
|
|
3746
|
+
};
|
|
3747
|
+
const tickets = [];
|
|
3748
|
+
let page = 1;
|
|
3749
|
+
let hasMore = true;
|
|
3750
|
+
while (hasMore && page <= 50) {
|
|
3751
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3752
|
+
where,
|
|
3753
|
+
limit: 500,
|
|
3754
|
+
page,
|
|
3755
|
+
depth: 2,
|
|
3756
|
+
overrideAccess: true,
|
|
3757
|
+
select: { ticketNumber: true, subject: true, billedAmount: true, totalTimeMinutes: true, project: true, client: true }
|
|
3758
|
+
});
|
|
3759
|
+
tickets.push(...batch.docs);
|
|
3760
|
+
hasMore = batch.hasNextPage ?? false;
|
|
3761
|
+
page++;
|
|
3762
|
+
}
|
|
3763
|
+
let totalAmount = 0;
|
|
3764
|
+
let totalMinutes = 0;
|
|
3765
|
+
const lines = tickets.map((t) => {
|
|
3766
|
+
const amount = typeof t.billedAmount === "number" ? t.billedAmount : 0;
|
|
3767
|
+
const minutes = typeof t.totalTimeMinutes === "number" ? t.totalTimeMinutes : 0;
|
|
3768
|
+
totalAmount += amount;
|
|
3769
|
+
totalMinutes += minutes;
|
|
3770
|
+
const client = typeof t.client === "object" && t.client ? t.client.company || "" : "";
|
|
3771
|
+
return { ticketNumber: String(t.ticketNumber || ""), subject: String(t.subject || ""), client, minutes, amount };
|
|
3772
|
+
});
|
|
3773
|
+
if (format === "pdf") {
|
|
3774
|
+
const pdf = await generateInvoicePdf({ from, to, projectId, lines, totalMinutes, totalAmount });
|
|
3775
|
+
return new Response(new Uint8Array(pdf), {
|
|
3776
|
+
status: 200,
|
|
3777
|
+
headers: {
|
|
3778
|
+
"Content-Type": "application/pdf",
|
|
3779
|
+
"Content-Disposition": `inline; filename="invoice-${from}_${to}.pdf"`
|
|
3780
|
+
}
|
|
3781
|
+
});
|
|
3782
|
+
}
|
|
3783
|
+
const fmtAmount2 = (n) => `${n.toFixed(2)} \u20AC`;
|
|
3784
|
+
const fmtTime2 = (min) => `${Math.floor(min / 60)}h${String(min % 60).padStart(2, "0")}`;
|
|
3785
|
+
const rows = lines.map((l) => `<tr>
|
|
3786
|
+
<td>${escapeHtml(l.ticketNumber)}</td>
|
|
3787
|
+
<td>${escapeHtml(l.subject)}</td>
|
|
3788
|
+
<td>${escapeHtml(l.client)}</td>
|
|
3789
|
+
<td style="text-align:right;">${fmtTime2(l.minutes)}</td>
|
|
3790
|
+
<td style="text-align:right;">${fmtAmount2(l.amount)}</td>
|
|
3791
|
+
</tr>`).join("");
|
|
3792
|
+
const html = `<!doctype html>
|
|
3793
|
+
<html lang="fr"><head><meta charset="utf-8"><title>Facture ${escapeHtml(from)} \u2192 ${escapeHtml(to)}</title>
|
|
3794
|
+
<style>
|
|
3795
|
+
body { font-family: system-ui, -apple-system, sans-serif; color: #1e293b; max-width: 820px; margin: 32px auto; padding: 0 24px; }
|
|
3796
|
+
h1 { font-size: 22px; margin: 0 0 4px; }
|
|
3797
|
+
.period { color: #64748b; margin-bottom: 24px; }
|
|
3798
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
3799
|
+
th, td { padding: 8px 10px; border-bottom: 1px solid #e2e8f0; text-align: left; }
|
|
3800
|
+
th { background: #f8fafc; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: #475569; }
|
|
3801
|
+
tfoot td { font-weight: 700; border-top: 2px solid #0f172a; border-bottom: none; }
|
|
3802
|
+
@media print { body { margin: 0; } }
|
|
3803
|
+
</style></head>
|
|
3804
|
+
<body>
|
|
3805
|
+
<h1>Facture / Pr\xE9-facture</h1>
|
|
3806
|
+
<div class="period">P\xE9riode : ${escapeHtml(from)} \u2192 ${escapeHtml(to)}${projectId ? ` \xB7 Projet #${escapeHtml(projectId)}` : ""}</div>
|
|
3807
|
+
<table>
|
|
3808
|
+
<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>
|
|
3809
|
+
<tbody>${rows || '<tr><td colspan="5" style="color:#94a3b8;">Aucun ticket facturable sur la p\xE9riode.</td></tr>'}</tbody>
|
|
3810
|
+
<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>
|
|
3811
|
+
</table>
|
|
3812
|
+
</body></html>`;
|
|
3813
|
+
return new Response(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
3814
|
+
} catch (error) {
|
|
3815
|
+
const authResponse = handleAuthError(error);
|
|
3816
|
+
if (authResponse) return authResponse;
|
|
3817
|
+
console.error("[invoice] Error:", error);
|
|
3818
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
// src/utils/generateTicketSynthesis.ts
|
|
3825
|
+
function getClient4(aiSettings) {
|
|
3826
|
+
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
3827
|
+
if (aiSettings.provider === "ollama") {
|
|
3828
|
+
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
3829
|
+
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
3830
|
+
}
|
|
3831
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
3832
|
+
}
|
|
3833
|
+
function getModel3(aiSettings) {
|
|
3834
|
+
return aiSettings.model || "claude-haiku-4-5-20251001";
|
|
3835
|
+
}
|
|
3836
|
+
async function generateTicketSynthesis(args) {
|
|
3837
|
+
const { payload, slugs, ticketId } = args;
|
|
3838
|
+
const settings = await readSupportSettings(payload);
|
|
3839
|
+
if (settings.ai.enableSynthesis === false) {
|
|
3840
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "skipped", reason: "synthesis disabled" };
|
|
3841
|
+
}
|
|
3842
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
3843
|
+
id: ticketId,
|
|
3844
|
+
depth: 1,
|
|
3845
|
+
overrideAccess: true
|
|
3846
|
+
});
|
|
3847
|
+
if (!ticket) {
|
|
3848
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "error", reason: "ticket not found" };
|
|
3849
|
+
}
|
|
3850
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3851
|
+
id: ticketId,
|
|
3852
|
+
data: { aiSummaryStatus: "pending" },
|
|
3450
3853
|
overrideAccess: true
|
|
3451
3854
|
}).catch(() => {
|
|
3452
3855
|
});
|
|
3453
|
-
const messagesResult = await payload.
|
|
3454
|
-
collection: slugs.ticketMessages,
|
|
3856
|
+
const messagesResult = await dbFind(payload, slugs.ticketMessages, {
|
|
3455
3857
|
where: { ticket: { equals: ticketId } },
|
|
3456
3858
|
sort: "createdAt",
|
|
3457
3859
|
limit: 500,
|
|
@@ -3461,8 +3863,7 @@ async function generateTicketSynthesis(args) {
|
|
|
3461
3863
|
const messages = messagesResult.docs;
|
|
3462
3864
|
if (messages.length === 0) {
|
|
3463
3865
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3464
|
-
await payload.
|
|
3465
|
-
collection: slugs.tickets,
|
|
3866
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3466
3867
|
id: ticketId,
|
|
3467
3868
|
data: {
|
|
3468
3869
|
aiSummary: "(Aucun message dans ce ticket)",
|
|
@@ -3507,7 +3908,7 @@ Regles strictes :
|
|
|
3507
3908
|
- Si le ticket n'a pas abouti, decris quand meme le travail d'analyse realise
|
|
3508
3909
|
|
|
3509
3910
|
Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
3510
|
-
const anthropic =
|
|
3911
|
+
const anthropic = getClient4(settings.ai);
|
|
3511
3912
|
const model = getModel3(settings.ai);
|
|
3512
3913
|
try {
|
|
3513
3914
|
const res = await anthropic.messages.create({
|
|
@@ -3517,8 +3918,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3517
3918
|
});
|
|
3518
3919
|
const summary = res.content[0]?.type === "text" ? res.content[0].text.trim() : "";
|
|
3519
3920
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3520
|
-
await payload.
|
|
3521
|
-
collection: slugs.tickets,
|
|
3921
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3522
3922
|
id: ticketId,
|
|
3523
3923
|
data: {
|
|
3524
3924
|
aiSummary: summary,
|
|
@@ -3530,8 +3930,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3530
3930
|
return { summary, generatedAt, status: "done" };
|
|
3531
3931
|
} catch (err) {
|
|
3532
3932
|
const message = err instanceof Error ? err.message : String(err);
|
|
3533
|
-
await payload.
|
|
3534
|
-
collection: slugs.tickets,
|
|
3933
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3535
3934
|
id: ticketId,
|
|
3536
3935
|
data: { aiSummaryStatus: "error" },
|
|
3537
3936
|
overrideAccess: true
|
|
@@ -3561,8 +3960,7 @@ function createTicketSynthesisEndpoint(slugs) {
|
|
|
3561
3960
|
return Response.json({ error: "ticketId must be a number" }, { status: 400 });
|
|
3562
3961
|
}
|
|
3563
3962
|
if (!force) {
|
|
3564
|
-
const existing = await payload.
|
|
3565
|
-
collection: slugs.tickets,
|
|
3963
|
+
const existing = await dbFindByID(payload, slugs.tickets, {
|
|
3566
3964
|
id: ticketId,
|
|
3567
3965
|
depth: 0,
|
|
3568
3966
|
overrideAccess: true
|
|
@@ -3606,8 +4004,7 @@ function createEmailStatsEndpoint(slugs) {
|
|
|
3606
4004
|
let page = 1;
|
|
3607
4005
|
let hasMore = true;
|
|
3608
4006
|
while (hasMore && page <= MAX_PAGES2) {
|
|
3609
|
-
const result = await payload.
|
|
3610
|
-
collection: slugs.emailLogs,
|
|
4007
|
+
const result = await dbFind(payload, slugs.emailLogs, {
|
|
3611
4008
|
where: { createdAt: { greater_than: cutoff } },
|
|
3612
4009
|
sort: "-createdAt",
|
|
3613
4010
|
limit: 500,
|
|
@@ -3681,12 +4078,20 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3681
4078
|
const payload = req.payload;
|
|
3682
4079
|
requireClient(req, slugs);
|
|
3683
4080
|
const body = await req.json();
|
|
3684
|
-
const { ticketId, rating, comment } = body;
|
|
3685
|
-
if (!ticketId
|
|
3686
|
-
return Response.json(
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
4081
|
+
const { ticketId, rating, nps, comment } = body;
|
|
4082
|
+
if (!ticketId) {
|
|
4083
|
+
return Response.json({ error: "ticketId est requis." }, { status: 400 });
|
|
4084
|
+
}
|
|
4085
|
+
const hasRating = rating !== void 0 && rating !== null;
|
|
4086
|
+
const hasNps = nps !== void 0 && nps !== null;
|
|
4087
|
+
if (!hasRating && !hasNps) {
|
|
4088
|
+
return Response.json({ error: "Un rating (1-5) et/ou un nps (0-10) est requis." }, { status: 400 });
|
|
4089
|
+
}
|
|
4090
|
+
if (hasRating && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
4091
|
+
return Response.json({ error: "rating doit \xEAtre un entier 1-5." }, { status: 400 });
|
|
4092
|
+
}
|
|
4093
|
+
if (hasNps && (!Number.isInteger(nps) || nps < 0 || nps > 10)) {
|
|
4094
|
+
return Response.json({ error: "nps doit \xEAtre un entier 0-10." }, { status: 400 });
|
|
3690
4095
|
}
|
|
3691
4096
|
if (comment && typeof comment === "string" && comment.length > 5e3) {
|
|
3692
4097
|
return Response.json(
|
|
@@ -3694,8 +4099,7 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3694
4099
|
{ status: 400 }
|
|
3695
4100
|
);
|
|
3696
4101
|
}
|
|
3697
|
-
const ticket = await payload.
|
|
3698
|
-
collection: slugs.tickets,
|
|
4102
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
3699
4103
|
id: ticketId,
|
|
3700
4104
|
depth: 0,
|
|
3701
4105
|
overrideAccess: false,
|
|
@@ -3710,8 +4114,7 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3710
4114
|
{ status: 400 }
|
|
3711
4115
|
);
|
|
3712
4116
|
}
|
|
3713
|
-
const existing = await payload.
|
|
3714
|
-
collection: slugs.satisfactionSurveys,
|
|
4117
|
+
const existing = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
3715
4118
|
where: { ticket: { equals: ticketId } },
|
|
3716
4119
|
limit: 1,
|
|
3717
4120
|
depth: 0,
|
|
@@ -3723,13 +4126,13 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3723
4126
|
{ status: 409 }
|
|
3724
4127
|
);
|
|
3725
4128
|
}
|
|
3726
|
-
const survey = await payload.
|
|
3727
|
-
collection: slugs.satisfactionSurveys,
|
|
4129
|
+
const survey = await dbCreate(payload, slugs.satisfactionSurveys, {
|
|
3728
4130
|
data: {
|
|
3729
4131
|
source: "ticket",
|
|
3730
4132
|
ticket: ticketId,
|
|
3731
4133
|
client: req.user.id,
|
|
3732
|
-
rating: Math.round(rating),
|
|
4134
|
+
...hasRating ? { rating: Math.round(rating) } : {},
|
|
4135
|
+
...hasNps ? { nps: Math.round(nps) } : {},
|
|
3733
4136
|
...comment ? { comment: comment.trim() } : {}
|
|
3734
4137
|
},
|
|
3735
4138
|
overrideAccess: true
|
|
@@ -3744,124 +4147,6 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3744
4147
|
}
|
|
3745
4148
|
};
|
|
3746
4149
|
}
|
|
3747
|
-
var TRANSPARENT_GIF = Buffer.from(
|
|
3748
|
-
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
3749
|
-
"base64"
|
|
3750
|
-
);
|
|
3751
|
-
function generateTrackingToken(ticketId, messageId, secret) {
|
|
3752
|
-
return crypto3.createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex").substring(0, 16);
|
|
3753
|
-
}
|
|
3754
|
-
function createTrackOpenEndpoint(slugs) {
|
|
3755
|
-
return {
|
|
3756
|
-
path: "/support/track-open",
|
|
3757
|
-
method: "get",
|
|
3758
|
-
handler: async (req) => {
|
|
3759
|
-
const url = new URL(req.url);
|
|
3760
|
-
const ticketId = url.searchParams.get("t");
|
|
3761
|
-
const messageId = url.searchParams.get("m");
|
|
3762
|
-
const sig = url.searchParams.get("sig");
|
|
3763
|
-
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
3764
|
-
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
3765
|
-
const secret = process.env.PAYLOAD_SECRET || "";
|
|
3766
|
-
if (secret && ticketId && messageId && sig) {
|
|
3767
|
-
const expected = generateTrackingToken(ticketId, messageId, secret);
|
|
3768
|
-
if (sig !== expected) {
|
|
3769
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3770
|
-
status: 200,
|
|
3771
|
-
headers: {
|
|
3772
|
-
"Content-Type": "image/gif",
|
|
3773
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3774
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3775
|
-
}
|
|
3776
|
-
});
|
|
3777
|
-
}
|
|
3778
|
-
} else if (secret && (!sig || !ticketId || !messageId)) {
|
|
3779
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3780
|
-
status: 200,
|
|
3781
|
-
headers: {
|
|
3782
|
-
"Content-Type": "image/gif",
|
|
3783
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3784
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3785
|
-
}
|
|
3786
|
-
});
|
|
3787
|
-
}
|
|
3788
|
-
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
3789
|
-
try {
|
|
3790
|
-
const payload = req.payload;
|
|
3791
|
-
const ticket = await payload.findByID({
|
|
3792
|
-
collection: slugs.tickets,
|
|
3793
|
-
id: parsedId,
|
|
3794
|
-
depth: 0,
|
|
3795
|
-
overrideAccess: true,
|
|
3796
|
-
select: { lastClientReadAt: true }
|
|
3797
|
-
});
|
|
3798
|
-
if (ticket) {
|
|
3799
|
-
const lastRead = ticket.lastClientReadAt ? new Date(ticket.lastClientReadAt).getTime() : 0;
|
|
3800
|
-
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
|
|
3801
|
-
if (lastRead < fiveMinAgo) {
|
|
3802
|
-
await payload.update({
|
|
3803
|
-
collection: slugs.tickets,
|
|
3804
|
-
id: parsedId,
|
|
3805
|
-
data: { lastClientReadAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3806
|
-
overrideAccess: true
|
|
3807
|
-
});
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
3811
|
-
const msg = await payload.findByID({
|
|
3812
|
-
collection: slugs.ticketMessages,
|
|
3813
|
-
id: parsedMsgId,
|
|
3814
|
-
depth: 0,
|
|
3815
|
-
overrideAccess: true,
|
|
3816
|
-
select: { emailOpenedAt: true }
|
|
3817
|
-
});
|
|
3818
|
-
if (msg && !msg.emailOpenedAt) {
|
|
3819
|
-
await payload.update({
|
|
3820
|
-
collection: slugs.ticketMessages,
|
|
3821
|
-
id: parsedMsgId,
|
|
3822
|
-
data: { emailOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3823
|
-
overrideAccess: true
|
|
3824
|
-
});
|
|
3825
|
-
const ticketInfo = await payload.findByID({
|
|
3826
|
-
collection: slugs.tickets,
|
|
3827
|
-
id: parsedId,
|
|
3828
|
-
depth: 1,
|
|
3829
|
-
overrideAccess: true,
|
|
3830
|
-
select: { ticketNumber: true, subject: true, client: true }
|
|
3831
|
-
});
|
|
3832
|
-
const clientName = typeof ticketInfo?.client === "object" ? ticketInfo.client?.firstName || "Client" : "Client";
|
|
3833
|
-
try {
|
|
3834
|
-
await payload.create({
|
|
3835
|
-
collection: "admin-notifications",
|
|
3836
|
-
data: {
|
|
3837
|
-
title: `Email ouvert \u2014 ${ticketInfo?.ticketNumber || "TK-????"}`,
|
|
3838
|
-
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || "ticket"}"`,
|
|
3839
|
-
type: "email_opened",
|
|
3840
|
-
link: `/admin/ticket?id=${parsedId}`
|
|
3841
|
-
},
|
|
3842
|
-
overrideAccess: true
|
|
3843
|
-
});
|
|
3844
|
-
} catch {
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
}
|
|
3848
|
-
} catch (err) {
|
|
3849
|
-
console.error("[track-open] Error:", err);
|
|
3850
|
-
}
|
|
3851
|
-
}
|
|
3852
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3853
|
-
status: 200,
|
|
3854
|
-
headers: {
|
|
3855
|
-
"Content-Type": "image/gif",
|
|
3856
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3857
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
3858
|
-
"Pragma": "no-cache",
|
|
3859
|
-
"Expires": "0"
|
|
3860
|
-
}
|
|
3861
|
-
});
|
|
3862
|
-
}
|
|
3863
|
-
};
|
|
3864
|
-
}
|
|
3865
4150
|
|
|
3866
4151
|
// src/endpoints/export-csv.ts
|
|
3867
4152
|
function createExportCsvEndpoint(slugs) {
|
|
@@ -3877,8 +4162,7 @@ function createExportCsvEndpoint(slugs) {
|
|
|
3877
4162
|
let hasMore = true;
|
|
3878
4163
|
const allDocs = [];
|
|
3879
4164
|
while (hasMore) {
|
|
3880
|
-
const batch = await payload.
|
|
3881
|
-
collection: slugs.tickets,
|
|
4165
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3882
4166
|
limit: PAGE_SIZE2,
|
|
3883
4167
|
page,
|
|
3884
4168
|
depth: 1,
|
|
@@ -3954,21 +4238,18 @@ function createExportDataEndpoint(slugs) {
|
|
|
3954
4238
|
const payload = req.payload;
|
|
3955
4239
|
requireClient(req, slugs);
|
|
3956
4240
|
const [clientData, ticketsResult, messagesResult, surveysResult] = await Promise.all([
|
|
3957
|
-
payload.
|
|
3958
|
-
collection: slugs.supportClients,
|
|
4241
|
+
dbFindByID(payload, slugs.supportClients, {
|
|
3959
4242
|
id: req.user.id,
|
|
3960
4243
|
depth: 0,
|
|
3961
4244
|
overrideAccess: true
|
|
3962
4245
|
}),
|
|
3963
|
-
payload.
|
|
3964
|
-
collection: slugs.tickets,
|
|
4246
|
+
dbFind(payload, slugs.tickets, {
|
|
3965
4247
|
where: { client: { equals: req.user.id } },
|
|
3966
4248
|
limit: 1e3,
|
|
3967
4249
|
depth: 0,
|
|
3968
4250
|
overrideAccess: true
|
|
3969
4251
|
}),
|
|
3970
|
-
payload.
|
|
3971
|
-
collection: slugs.ticketMessages,
|
|
4252
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
3972
4253
|
where: {
|
|
3973
4254
|
"ticket.client": { equals: req.user.id },
|
|
3974
4255
|
authorType: { equals: "client" }
|
|
@@ -3977,8 +4258,7 @@ function createExportDataEndpoint(slugs) {
|
|
|
3977
4258
|
depth: 0,
|
|
3978
4259
|
overrideAccess: true
|
|
3979
4260
|
}),
|
|
3980
|
-
payload.
|
|
3981
|
-
collection: slugs.satisfactionSurveys,
|
|
4261
|
+
dbFind(payload, slugs.satisfactionSurveys, {
|
|
3982
4262
|
where: { client: { equals: req.user.id } },
|
|
3983
4263
|
limit: 500,
|
|
3984
4264
|
depth: 0,
|
|
@@ -4052,8 +4332,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4052
4332
|
if (!["create_ticket", "add_to_ticket", "ignore"].includes(action)) {
|
|
4053
4333
|
return Response.json({ error: "Invalid action" }, { status: 400 });
|
|
4054
4334
|
}
|
|
4055
|
-
const pendingEmail = await payload.
|
|
4056
|
-
collection: slugs.pendingEmails,
|
|
4335
|
+
const pendingEmail = await dbFindByID(payload, slugs.pendingEmails, {
|
|
4057
4336
|
id: Number(id),
|
|
4058
4337
|
depth: 1,
|
|
4059
4338
|
overrideAccess: true
|
|
@@ -4067,14 +4346,12 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4067
4346
|
let clientId = overrideClientId || (typeof pendingEmail.client === "object" ? pendingEmail.client?.id : pendingEmail.client);
|
|
4068
4347
|
let clientDoc = typeof pendingEmail.client === "object" ? pendingEmail.client : null;
|
|
4069
4348
|
if (overrideClientId && overrideClientId !== clientDoc?.id) {
|
|
4070
|
-
clientDoc = await payload.
|
|
4071
|
-
collection: slugs.supportClients,
|
|
4349
|
+
clientDoc = await dbFindByID(payload, slugs.supportClients, {
|
|
4072
4350
|
id: overrideClientId,
|
|
4073
4351
|
depth: 0,
|
|
4074
4352
|
overrideAccess: true
|
|
4075
4353
|
});
|
|
4076
|
-
await payload.
|
|
4077
|
-
collection: slugs.pendingEmails,
|
|
4354
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4078
4355
|
id: Number(id),
|
|
4079
4356
|
data: { client: overrideClientId },
|
|
4080
4357
|
overrideAccess: true
|
|
@@ -4091,8 +4368,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4091
4368
|
file: typeof a.file === "object" ? a.file.id : a.file
|
|
4092
4369
|
})) || [];
|
|
4093
4370
|
if (action === "ignore") {
|
|
4094
|
-
await payload.
|
|
4095
|
-
collection: slugs.pendingEmails,
|
|
4371
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4096
4372
|
id: Number(id),
|
|
4097
4373
|
data: {
|
|
4098
4374
|
status: "ignored",
|
|
@@ -4104,8 +4380,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4104
4380
|
return Response.json({ action: "ignored", pendingEmailId: Number(id) });
|
|
4105
4381
|
}
|
|
4106
4382
|
if (action === "create_ticket") {
|
|
4107
|
-
const newTicket = await payload.
|
|
4108
|
-
collection: slugs.tickets,
|
|
4383
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
4109
4384
|
data: {
|
|
4110
4385
|
subject: pendingEmail.subject,
|
|
4111
4386
|
client: clientId,
|
|
@@ -4116,8 +4391,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4116
4391
|
},
|
|
4117
4392
|
overrideAccess: true
|
|
4118
4393
|
});
|
|
4119
|
-
await payload.
|
|
4120
|
-
collection: slugs.ticketMessages,
|
|
4394
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
4121
4395
|
data: {
|
|
4122
4396
|
ticket: newTicket.id,
|
|
4123
4397
|
body: pendingEmail.body,
|
|
@@ -4144,8 +4418,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4144
4418
|
} catch (err) {
|
|
4145
4419
|
console.error("[pending-email-process] Failed to send notification:", err);
|
|
4146
4420
|
}
|
|
4147
|
-
await payload.
|
|
4148
|
-
collection: slugs.pendingEmails,
|
|
4421
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4149
4422
|
id: Number(id),
|
|
4150
4423
|
data: {
|
|
4151
4424
|
status: "processed",
|
|
@@ -4165,8 +4438,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4165
4438
|
if (!ticketId) {
|
|
4166
4439
|
return Response.json({ error: "ticketId is required for add_to_ticket" }, { status: 400 });
|
|
4167
4440
|
}
|
|
4168
|
-
const targetTicket = await payload.
|
|
4169
|
-
collection: slugs.tickets,
|
|
4441
|
+
const targetTicket = await dbFindByID(payload, slugs.tickets, {
|
|
4170
4442
|
id: ticketId,
|
|
4171
4443
|
depth: 0,
|
|
4172
4444
|
overrideAccess: true
|
|
@@ -4174,8 +4446,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4174
4446
|
if (!targetTicket) {
|
|
4175
4447
|
return Response.json({ error: "Target ticket not found" }, { status: 404 });
|
|
4176
4448
|
}
|
|
4177
|
-
await payload.
|
|
4178
|
-
collection: slugs.ticketMessages,
|
|
4449
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
4179
4450
|
data: {
|
|
4180
4451
|
ticket: ticketId,
|
|
4181
4452
|
body: pendingEmail.body,
|
|
@@ -4187,15 +4458,13 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4187
4458
|
overrideAccess: true
|
|
4188
4459
|
});
|
|
4189
4460
|
if (targetTicket.status === "resolved") {
|
|
4190
|
-
await payload.
|
|
4191
|
-
collection: slugs.tickets,
|
|
4461
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
4192
4462
|
id: ticketId,
|
|
4193
4463
|
data: { status: "open" },
|
|
4194
4464
|
overrideAccess: true
|
|
4195
4465
|
});
|
|
4196
4466
|
}
|
|
4197
|
-
await payload.
|
|
4198
|
-
collection: slugs.pendingEmails,
|
|
4467
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4199
4468
|
id: Number(id),
|
|
4200
4469
|
data: {
|
|
4201
4470
|
status: "processed",
|
|
@@ -4248,8 +4517,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4248
4517
|
if (!messageId) {
|
|
4249
4518
|
return Response.json({ error: "messageId requis" }, { status: 400 });
|
|
4250
4519
|
}
|
|
4251
|
-
const message = await payload.
|
|
4252
|
-
collection: slugs.ticketMessages,
|
|
4520
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
4253
4521
|
id: messageId,
|
|
4254
4522
|
depth: 0,
|
|
4255
4523
|
overrideAccess: true
|
|
@@ -4258,8 +4526,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4258
4526
|
return Response.json({ error: "Message introuvable" }, { status: 404 });
|
|
4259
4527
|
}
|
|
4260
4528
|
const ticketId = typeof message.ticket === "object" ? message.ticket.id : message.ticket;
|
|
4261
|
-
const ticket = await payload.
|
|
4262
|
-
collection: slugs.tickets,
|
|
4529
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
4263
4530
|
id: ticketId,
|
|
4264
4531
|
depth: 1,
|
|
4265
4532
|
overrideAccess: true
|
|
@@ -4385,8 +4652,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4385
4652
|
let created = 0;
|
|
4386
4653
|
let skipped = 0;
|
|
4387
4654
|
for (const article of KB_ARTICLES) {
|
|
4388
|
-
const existing = await payload.
|
|
4389
|
-
collection: slugs.knowledgeBase,
|
|
4655
|
+
const existing = await dbFind(payload, slugs.knowledgeBase, {
|
|
4390
4656
|
where: { slug: { equals: article.slug } },
|
|
4391
4657
|
limit: 1,
|
|
4392
4658
|
depth: 0,
|
|
@@ -4413,8 +4679,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4413
4679
|
version: 1
|
|
4414
4680
|
}
|
|
4415
4681
|
};
|
|
4416
|
-
await payload.
|
|
4417
|
-
collection: slugs.knowledgeBase,
|
|
4682
|
+
await dbCreate(payload, slugs.knowledgeBase, {
|
|
4418
4683
|
data: {
|
|
4419
4684
|
title: article.title,
|
|
4420
4685
|
slug: article.slug,
|
|
@@ -4469,8 +4734,7 @@ function createLoginEndpoint(slugs) {
|
|
|
4469
4734
|
collection: slugs.supportClients,
|
|
4470
4735
|
data: { email, password }
|
|
4471
4736
|
});
|
|
4472
|
-
payload.
|
|
4473
|
-
collection: slugs.authLogs,
|
|
4737
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4474
4738
|
data: { email, success: true, action: "login", ipAddress: ip, userAgent },
|
|
4475
4739
|
overrideAccess: true
|
|
4476
4740
|
}).catch(() => {
|
|
@@ -4494,12 +4758,14 @@ function createLoginEndpoint(slugs) {
|
|
|
4494
4758
|
);
|
|
4495
4759
|
} catch (err) {
|
|
4496
4760
|
const errorMessage = err instanceof Error ? err.message : "Erreur inconnue";
|
|
4761
|
+
if (errorMessage.includes("2FA_REQUIRED")) {
|
|
4762
|
+
return Response.json({ requires2FA: true }, { status: 200 });
|
|
4763
|
+
}
|
|
4497
4764
|
let errorReason = "Identifiants incorrects";
|
|
4498
4765
|
if (errorMessage.includes("locked") || errorMessage.includes("verrouill\xE9") || errorMessage.includes("Too many")) {
|
|
4499
4766
|
errorReason = "Compte verrouill\xE9 (trop de tentatives)";
|
|
4500
4767
|
}
|
|
4501
|
-
payload.
|
|
4502
|
-
collection: slugs.authLogs,
|
|
4768
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4503
4769
|
data: { email, success: false, action: "login", errorReason, ipAddress: ip, userAgent },
|
|
4504
4770
|
overrideAccess: true
|
|
4505
4771
|
}).catch(() => {
|
|
@@ -4520,7 +4786,10 @@ function generateSecureCode() {
|
|
|
4520
4786
|
return String(num);
|
|
4521
4787
|
}
|
|
4522
4788
|
function hashCode(code) {
|
|
4523
|
-
const secret = process.env.PAYLOAD_SECRET
|
|
4789
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
4790
|
+
if (!secret) {
|
|
4791
|
+
throw new Error("[support][2fa] PAYLOAD_SECRET is not set \u2014 refusing to operate 2FA with an insecure fallback secret");
|
|
4792
|
+
}
|
|
4524
4793
|
return crypto3.createHmac("sha256", secret).update(code).digest("hex");
|
|
4525
4794
|
}
|
|
4526
4795
|
function createAuth2faEndpoint(slugs) {
|
|
@@ -4545,8 +4814,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4545
4814
|
if (sendLimiter.check(email)) {
|
|
4546
4815
|
return Response.json(genericSendResponse);
|
|
4547
4816
|
}
|
|
4548
|
-
const clients = await payload.
|
|
4549
|
-
collection: slugs.supportClients,
|
|
4817
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4550
4818
|
where: { email: { equals: email } },
|
|
4551
4819
|
limit: 1,
|
|
4552
4820
|
depth: 0,
|
|
@@ -4559,8 +4827,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4559
4827
|
const plainCode = generateSecureCode();
|
|
4560
4828
|
const twoFactorCode = hashCode(plainCode);
|
|
4561
4829
|
const twoFactorExpiry = new Date(Date.now() + 10 * 60 * 1e3).toISOString();
|
|
4562
|
-
await payload.
|
|
4563
|
-
collection: slugs.supportClients,
|
|
4830
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4564
4831
|
id: client.id,
|
|
4565
4832
|
data: { twoFactorCode, twoFactorExpiry },
|
|
4566
4833
|
overrideAccess: true
|
|
@@ -4591,8 +4858,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4591
4858
|
{ status: 429 }
|
|
4592
4859
|
);
|
|
4593
4860
|
}
|
|
4594
|
-
const clients = await payload.
|
|
4595
|
-
collection: slugs.supportClients,
|
|
4861
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4596
4862
|
where: { email: { equals: email } },
|
|
4597
4863
|
limit: 1,
|
|
4598
4864
|
depth: 0,
|
|
@@ -4608,8 +4874,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4608
4874
|
return Response.json({ error: "Aucun code en attente" }, { status: 400 });
|
|
4609
4875
|
}
|
|
4610
4876
|
if (/* @__PURE__ */ new Date() > new Date(storedExpiry)) {
|
|
4611
|
-
await payload.
|
|
4612
|
-
collection: slugs.supportClients,
|
|
4877
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4613
4878
|
id: client.id,
|
|
4614
4879
|
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4615
4880
|
overrideAccess: true
|
|
@@ -4623,10 +4888,9 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4623
4888
|
if (submittedBuffer.length !== storedBuffer.length || !crypto3__default.default.timingSafeEqual(submittedBuffer, storedBuffer)) {
|
|
4624
4889
|
return Response.json({ error: "Code incorrect" }, { status: 400 });
|
|
4625
4890
|
}
|
|
4626
|
-
await payload.
|
|
4627
|
-
collection: slugs.supportClients,
|
|
4891
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4628
4892
|
id: client.id,
|
|
4629
|
-
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4893
|
+
data: { twoFactorCode: "", twoFactorExpiry: "", twoFactorVerifiedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4630
4894
|
overrideAccess: true
|
|
4631
4895
|
});
|
|
4632
4896
|
verifyLimiter.reset(email);
|
|
@@ -4700,15 +4964,33 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4700
4964
|
if (!profile.email) {
|
|
4701
4965
|
return Response.json({ error: "no_email" }, { status: 400 });
|
|
4702
4966
|
}
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4967
|
+
if (profile.verified_email !== true) {
|
|
4968
|
+
return Response.json({ error: "email_not_verified" }, { status: 403 });
|
|
4969
|
+
}
|
|
4970
|
+
const payload$1 = req.payload;
|
|
4971
|
+
const byGoogle = await dbFind(payload$1, slugs.supportClients, {
|
|
4972
|
+
where: { googleId: { equals: profile.id } },
|
|
4707
4973
|
limit: 1,
|
|
4708
4974
|
depth: 0,
|
|
4709
4975
|
overrideAccess: true
|
|
4710
4976
|
});
|
|
4711
|
-
let clientDoc =
|
|
4977
|
+
let clientDoc = byGoogle.docs[0];
|
|
4978
|
+
if (!clientDoc) {
|
|
4979
|
+
const byEmail = await dbFind(payload$1, slugs.supportClients, {
|
|
4980
|
+
where: { email: { equals: profile.email } },
|
|
4981
|
+
limit: 1,
|
|
4982
|
+
depth: 0,
|
|
4983
|
+
overrideAccess: true
|
|
4984
|
+
});
|
|
4985
|
+
clientDoc = byEmail.docs[0];
|
|
4986
|
+
if (clientDoc && profile.id) {
|
|
4987
|
+
await dbUpdate(payload$1, slugs.supportClients, {
|
|
4988
|
+
id: clientDoc.id,
|
|
4989
|
+
data: { googleId: profile.id },
|
|
4990
|
+
overrideAccess: true
|
|
4991
|
+
});
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4712
4994
|
if (!clientDoc) {
|
|
4713
4995
|
const allowedDomains = options?.allowedEmailDomains;
|
|
4714
4996
|
if (allowedDomains && allowedDomains.length > 0) {
|
|
@@ -4726,43 +5008,64 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4726
5008
|
const autoPassword = crypto3__default.default.randomBytes(48).toString("base64url");
|
|
4727
5009
|
const fullName = profile.name || profile.email.split("@")[0];
|
|
4728
5010
|
const nameParts = fullName.split(" ");
|
|
4729
|
-
clientDoc = await payload.
|
|
4730
|
-
collection: slugs.supportClients,
|
|
5011
|
+
clientDoc = await dbCreate(payload$1, slugs.supportClients, {
|
|
4731
5012
|
data: {
|
|
4732
5013
|
email: profile.email,
|
|
4733
5014
|
firstName: nameParts[0] || fullName,
|
|
4734
5015
|
lastName: nameParts.slice(1).join(" ") || "-",
|
|
4735
5016
|
company: fullName,
|
|
5017
|
+
googleId: profile.id,
|
|
4736
5018
|
password: autoPassword
|
|
4737
5019
|
},
|
|
4738
5020
|
overrideAccess: true
|
|
4739
5021
|
});
|
|
4740
5022
|
}
|
|
4741
|
-
const
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
id: clientDoc.id,
|
|
4745
|
-
data: { password: tempPassword },
|
|
4746
|
-
overrideAccess: true
|
|
4747
|
-
});
|
|
4748
|
-
const loginResult = await payload.login({
|
|
4749
|
-
collection: slugs.supportClients,
|
|
4750
|
-
data: { email: profile.email, password: tempPassword }
|
|
4751
|
-
});
|
|
4752
|
-
const postLoginPassword = crypto3__default.default.randomBytes(48).toString("base64url");
|
|
4753
|
-
await payload.update({
|
|
4754
|
-
collection: slugs.supportClients,
|
|
4755
|
-
id: clientDoc.id,
|
|
4756
|
-
data: { password: postLoginPassword },
|
|
4757
|
-
overrideAccess: true
|
|
4758
|
-
});
|
|
4759
|
-
if (!loginResult.token) {
|
|
4760
|
-
return Response.json({ error: "login_failed" }, { status: 400 });
|
|
5023
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
5024
|
+
if (!secret) {
|
|
5025
|
+
return Response.json({ error: "server_misconfigured" }, { status: 500 });
|
|
4761
5026
|
}
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
5027
|
+
const collectionConfig = payload$1.collections[slugs.supportClients].config;
|
|
5028
|
+
const tokenExpiration = collectionConfig.auth?.tokenExpiration ?? 7200;
|
|
5029
|
+
let sid;
|
|
5030
|
+
if (collectionConfig.auth?.useSessions) {
|
|
5031
|
+
sid = crypto3__default.default.randomUUID();
|
|
5032
|
+
const now = /* @__PURE__ */ new Date();
|
|
5033
|
+
const expiresAt = new Date(now.getTime() + tokenExpiration * 1e3);
|
|
5034
|
+
const fresh = await dbFindByID(payload$1, slugs.supportClients, {
|
|
5035
|
+
id: clientDoc.id,
|
|
5036
|
+
depth: 0,
|
|
5037
|
+
overrideAccess: true,
|
|
5038
|
+
showHiddenFields: true
|
|
5039
|
+
});
|
|
5040
|
+
const kept = Array.isArray(fresh?.sessions) ? fresh.sessions.filter((s) => new Date(s.expiresAt) > now) : [];
|
|
5041
|
+
await payload$1.db.updateOne({
|
|
5042
|
+
collection: slugs.supportClients,
|
|
5043
|
+
id: clientDoc.id,
|
|
5044
|
+
data: {
|
|
5045
|
+
sessions: [
|
|
5046
|
+
...kept,
|
|
5047
|
+
{ id: sid, createdAt: now.toISOString(), expiresAt: expiresAt.toISOString() }
|
|
5048
|
+
]
|
|
5049
|
+
},
|
|
5050
|
+
returning: false
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
const fieldsToSign = payload.getFieldsToSign({
|
|
5054
|
+
collectionConfig,
|
|
5055
|
+
email: clientDoc.email,
|
|
5056
|
+
sid,
|
|
5057
|
+
user: { ...clientDoc, collection: slugs.supportClients }
|
|
5058
|
+
});
|
|
5059
|
+
const { token, exp } = await payload.jwtSign({ fieldsToSign, secret, tokenExpiration });
|
|
5060
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
5061
|
+
const cookieSecure = process.env.NODE_ENV === "production";
|
|
5062
|
+
headers.append(
|
|
5063
|
+
"Set-Cookie",
|
|
5064
|
+
`payload-token=${token}; HttpOnly; ${cookieSecure ? "Secure; " : ""}SameSite=Lax; Path=/; Max-Age=${tokenExpiration}`
|
|
5065
|
+
);
|
|
5066
|
+
return new Response(JSON.stringify({ token, user: clientDoc, exp }), {
|
|
5067
|
+
status: 200,
|
|
5068
|
+
headers
|
|
4766
5069
|
});
|
|
4767
5070
|
}
|
|
4768
5071
|
return Response.json({ error: "Action invalide" }, { status: 400 });
|
|
@@ -4803,8 +5106,7 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4803
5106
|
);
|
|
4804
5107
|
}
|
|
4805
5108
|
const clientId = req.user.id;
|
|
4806
|
-
const tickets = await payload.
|
|
4807
|
-
collection: slugs.tickets,
|
|
5109
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4808
5110
|
where: { client: { equals: clientId } },
|
|
4809
5111
|
limit: 1e4,
|
|
4810
5112
|
depth: 0,
|
|
@@ -4813,39 +5115,32 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4813
5115
|
});
|
|
4814
5116
|
const ticketIds = tickets.docs.map((t) => t.id);
|
|
4815
5117
|
if (ticketIds.length > 0) {
|
|
4816
|
-
await payload.
|
|
4817
|
-
collection: slugs.ticketMessages,
|
|
5118
|
+
await dbDelete(payload, slugs.ticketMessages, {
|
|
4818
5119
|
where: { ticket: { in: ticketIds } },
|
|
4819
5120
|
overrideAccess: true
|
|
4820
5121
|
});
|
|
4821
|
-
await payload.
|
|
4822
|
-
collection: slugs.ticketActivityLog,
|
|
5122
|
+
await dbDelete(payload, slugs.ticketActivityLog, {
|
|
4823
5123
|
where: { ticket: { in: ticketIds } },
|
|
4824
5124
|
overrideAccess: true
|
|
4825
5125
|
});
|
|
4826
|
-
await payload.
|
|
4827
|
-
collection: slugs.timeEntries,
|
|
5126
|
+
await dbDelete(payload, slugs.timeEntries, {
|
|
4828
5127
|
where: { ticket: { in: ticketIds } },
|
|
4829
5128
|
overrideAccess: true
|
|
4830
5129
|
});
|
|
4831
|
-
await payload.
|
|
4832
|
-
collection: slugs.tickets,
|
|
5130
|
+
await dbDelete(payload, slugs.tickets, {
|
|
4833
5131
|
where: { client: { equals: clientId } },
|
|
4834
5132
|
overrideAccess: true
|
|
4835
5133
|
});
|
|
4836
5134
|
}
|
|
4837
|
-
await payload.
|
|
4838
|
-
collection: slugs.satisfactionSurveys,
|
|
5135
|
+
await dbDelete(payload, slugs.satisfactionSurveys, {
|
|
4839
5136
|
where: { client: { equals: clientId } },
|
|
4840
5137
|
overrideAccess: true
|
|
4841
5138
|
});
|
|
4842
|
-
await payload.
|
|
4843
|
-
collection: slugs.chatMessages,
|
|
5139
|
+
await dbDelete(payload, slugs.chatMessages, {
|
|
4844
5140
|
where: { client: { equals: clientId } },
|
|
4845
5141
|
overrideAccess: true
|
|
4846
5142
|
});
|
|
4847
|
-
await payload.
|
|
4848
|
-
collection: slugs.supportClients,
|
|
5143
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4849
5144
|
id: clientId,
|
|
4850
5145
|
overrideAccess: true
|
|
4851
5146
|
});
|
|
@@ -4886,8 +5181,8 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4886
5181
|
return Response.json({ error: "sourceId and targetId are required and must be different" }, { status: 400 });
|
|
4887
5182
|
}
|
|
4888
5183
|
const [source, target] = await Promise.all([
|
|
4889
|
-
payload
|
|
4890
|
-
payload
|
|
5184
|
+
dbFindByID(payload, slugs.supportClients, { id: sourceId, depth: 0, overrideAccess: true }),
|
|
5185
|
+
dbFindByID(payload, slugs.supportClients, { id: targetId, depth: 0, overrideAccess: true })
|
|
4891
5186
|
]);
|
|
4892
5187
|
if (!source || !target) {
|
|
4893
5188
|
return Response.json({ error: "Source or target client not found" }, { status: 404 });
|
|
@@ -4899,63 +5194,57 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4899
5194
|
pendingEmails: 0,
|
|
4900
5195
|
satisfactionSurveys: 0
|
|
4901
5196
|
};
|
|
4902
|
-
const tickets = await payload.
|
|
4903
|
-
collection: slugs.tickets,
|
|
5197
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4904
5198
|
where: { client: { equals: sourceId } },
|
|
4905
5199
|
limit: 500,
|
|
4906
5200
|
depth: 0,
|
|
4907
5201
|
overrideAccess: true
|
|
4908
5202
|
});
|
|
4909
5203
|
for (const ticket of tickets.docs) {
|
|
4910
|
-
await payload
|
|
5204
|
+
await dbUpdate(payload, slugs.tickets, { id: ticket.id, data: { client: targetId }, overrideAccess: true });
|
|
4911
5205
|
results.tickets++;
|
|
4912
5206
|
}
|
|
4913
|
-
const messages = await payload.
|
|
4914
|
-
collection: slugs.ticketMessages,
|
|
5207
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
4915
5208
|
where: { authorClient: { equals: sourceId } },
|
|
4916
5209
|
limit: 1e3,
|
|
4917
5210
|
depth: 0,
|
|
4918
5211
|
overrideAccess: true
|
|
4919
5212
|
});
|
|
4920
5213
|
for (const msg of messages.docs) {
|
|
4921
|
-
await payload
|
|
5214
|
+
await dbUpdate(payload, slugs.ticketMessages, { id: msg.id, data: { authorClient: targetId }, overrideAccess: true });
|
|
4922
5215
|
results.ticketMessages++;
|
|
4923
5216
|
}
|
|
4924
|
-
const chats = await payload.
|
|
4925
|
-
collection: slugs.chatMessages,
|
|
5217
|
+
const chats = await dbFind(payload, slugs.chatMessages, {
|
|
4926
5218
|
where: { client: { equals: sourceId } },
|
|
4927
5219
|
limit: 1e3,
|
|
4928
5220
|
depth: 0,
|
|
4929
5221
|
overrideAccess: true
|
|
4930
5222
|
});
|
|
4931
5223
|
for (const chat of chats.docs) {
|
|
4932
|
-
await payload
|
|
5224
|
+
await dbUpdate(payload, slugs.chatMessages, { id: chat.id, data: { client: targetId }, overrideAccess: true });
|
|
4933
5225
|
results.chatMessages++;
|
|
4934
5226
|
}
|
|
4935
|
-
const pendingEmails = await payload.
|
|
4936
|
-
collection: slugs.pendingEmails,
|
|
5227
|
+
const pendingEmails = await dbFind(payload, slugs.pendingEmails, {
|
|
4937
5228
|
where: { client: { equals: sourceId } },
|
|
4938
5229
|
limit: 500,
|
|
4939
5230
|
depth: 0,
|
|
4940
5231
|
overrideAccess: true
|
|
4941
5232
|
});
|
|
4942
5233
|
for (const pe of pendingEmails.docs) {
|
|
4943
|
-
await payload
|
|
5234
|
+
await dbUpdate(payload, slugs.pendingEmails, { id: pe.id, data: { client: targetId }, overrideAccess: true });
|
|
4944
5235
|
results.pendingEmails++;
|
|
4945
5236
|
}
|
|
4946
|
-
const surveys = await payload.
|
|
4947
|
-
collection: slugs.satisfactionSurveys,
|
|
5237
|
+
const surveys = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
4948
5238
|
where: { client: { equals: sourceId } },
|
|
4949
5239
|
limit: 500,
|
|
4950
5240
|
depth: 0,
|
|
4951
5241
|
overrideAccess: true
|
|
4952
5242
|
});
|
|
4953
5243
|
for (const survey of surveys.docs) {
|
|
4954
|
-
await payload
|
|
5244
|
+
await dbUpdate(payload, slugs.satisfactionSurveys, { id: survey.id, data: { client: targetId }, overrideAccess: true });
|
|
4955
5245
|
results.satisfactionSurveys++;
|
|
4956
5246
|
}
|
|
4957
|
-
await payload.
|
|
4958
|
-
collection: slugs.supportClients,
|
|
5247
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4959
5248
|
id: sourceId,
|
|
4960
5249
|
overrideAccess: true
|
|
4961
5250
|
});
|
|
@@ -5149,8 +5438,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5149
5438
|
if (blockedEmails.includes(conversation.client.email)) {
|
|
5150
5439
|
return Response.json({ error: "Cannot create ticket from system email address" }, { status: 400 });
|
|
5151
5440
|
}
|
|
5152
|
-
const clientResult = await payload.
|
|
5153
|
-
collection: slugs.supportClients,
|
|
5441
|
+
const clientResult = await dbFind(payload, slugs.supportClients, {
|
|
5154
5442
|
where: { email: { equals: conversation.client.email } },
|
|
5155
5443
|
limit: 1,
|
|
5156
5444
|
depth: 0,
|
|
@@ -5161,8 +5449,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5161
5449
|
if (!client) {
|
|
5162
5450
|
const nameParts = conversation.client.name.split(" ");
|
|
5163
5451
|
const randomPassword = crypto3__default.default.randomBytes(48).toString("base64url");
|
|
5164
|
-
client = await payload.
|
|
5165
|
-
collection: slugs.supportClients,
|
|
5452
|
+
client = await dbCreate(payload, slugs.supportClients, {
|
|
5166
5453
|
data: {
|
|
5167
5454
|
email: conversation.client.email,
|
|
5168
5455
|
password: randomPassword,
|
|
@@ -5174,15 +5461,13 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5174
5461
|
});
|
|
5175
5462
|
isNewClient = true;
|
|
5176
5463
|
}
|
|
5177
|
-
const adminUsers = await payload.
|
|
5178
|
-
collection: slugs.users,
|
|
5464
|
+
const adminUsers = await dbFind(payload, slugs.users, {
|
|
5179
5465
|
limit: 1,
|
|
5180
5466
|
depth: 0,
|
|
5181
5467
|
overrideAccess: true
|
|
5182
5468
|
});
|
|
5183
5469
|
const adminUserId = adminUsers.docs[0]?.id;
|
|
5184
|
-
const ticket = await payload.
|
|
5185
|
-
collection: slugs.tickets,
|
|
5470
|
+
const ticket = await dbCreate(payload, slugs.tickets, {
|
|
5186
5471
|
data: {
|
|
5187
5472
|
subject: conversation.subject,
|
|
5188
5473
|
client: client.id,
|
|
@@ -5195,8 +5480,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5195
5480
|
let importedCount = 0;
|
|
5196
5481
|
for (const msg of conversation.messages) {
|
|
5197
5482
|
const isAdmin = msg.from === "admin";
|
|
5198
|
-
await payload.
|
|
5199
|
-
collection: slugs.ticketMessages,
|
|
5483
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
5200
5484
|
data: {
|
|
5201
5485
|
ticket: ticket.id,
|
|
5202
5486
|
body: msg.content,
|
|
@@ -5232,8 +5516,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5232
5516
|
// src/utils/fireWebhooks.ts
|
|
5233
5517
|
async function fireWebhooks(payload, slugs, event, data) {
|
|
5234
5518
|
try {
|
|
5235
|
-
const endpoints = await payload.
|
|
5236
|
-
collection: slugs.webhookEndpoints,
|
|
5519
|
+
const endpoints = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5237
5520
|
where: {
|
|
5238
5521
|
active: { equals: true },
|
|
5239
5522
|
events: { contains: event }
|
|
@@ -5257,8 +5540,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5257
5540
|
signal: AbortSignal.timeout(1e4)
|
|
5258
5541
|
});
|
|
5259
5542
|
try {
|
|
5260
|
-
await payload.
|
|
5261
|
-
collection: slugs.webhookEndpoints,
|
|
5543
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5262
5544
|
id: endpoint.id,
|
|
5263
5545
|
data: {
|
|
5264
5546
|
lastTriggeredAt: timestamp,
|
|
@@ -5271,8 +5553,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5271
5553
|
} catch (error) {
|
|
5272
5554
|
console.warn(`[support] Webhook delivery failed: ${endpoint.url}`, error);
|
|
5273
5555
|
try {
|
|
5274
|
-
await payload.
|
|
5275
|
-
collection: slugs.webhookEndpoints,
|
|
5556
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5276
5557
|
id: endpoint.id,
|
|
5277
5558
|
data: {
|
|
5278
5559
|
lastTriggeredAt: timestamp,
|
|
@@ -5307,8 +5588,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5307
5588
|
const settings = await readSupportSettings(payload);
|
|
5308
5589
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
|
|
5309
5590
|
const results = { processed: 0, errors: 0 };
|
|
5310
|
-
const scheduled = await payload.
|
|
5311
|
-
collection: slugs.ticketMessages,
|
|
5591
|
+
const scheduled = await dbFind(payload, slugs.ticketMessages, {
|
|
5312
5592
|
where: {
|
|
5313
5593
|
and: [
|
|
5314
5594
|
{ scheduledAt: { less_than_equal: now.toISOString() } },
|
|
@@ -5328,8 +5608,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5328
5608
|
try {
|
|
5329
5609
|
const msg = message;
|
|
5330
5610
|
const ticketId = typeof msg.ticket === "object" ? msg.ticket.id : msg.ticket;
|
|
5331
|
-
const ticket = await payload.
|
|
5332
|
-
collection: slugs.tickets,
|
|
5611
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5333
5612
|
id: ticketId,
|
|
5334
5613
|
depth: 1,
|
|
5335
5614
|
overrideAccess: true
|
|
@@ -5352,8 +5631,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5352
5631
|
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
5353
5632
|
html: `<p>Bonjour ${escapeHtml(client.firstName || "")},</p><p>Notre \xE9quipe a r\xE9pondu \xE0 votre ticket <strong>${escapeHtml(ticketNumber)}</strong>.</p><blockquote style="border-left:4px solid #2563eb;padding:12px;margin:16px 0;background:#f8fafc;">${escapeHtml(preview)}</blockquote><p><a href="${baseUrl}/support/tickets/${ticketId}">Consulter le ticket</a></p>`
|
|
5354
5633
|
});
|
|
5355
|
-
await payload.
|
|
5356
|
-
collection: slugs.ticketMessages,
|
|
5634
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5357
5635
|
id: msg.id,
|
|
5358
5636
|
data: {
|
|
5359
5637
|
scheduledSent: true,
|
|
@@ -5363,16 +5641,14 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5363
5641
|
overrideAccess: true
|
|
5364
5642
|
});
|
|
5365
5643
|
} else {
|
|
5366
|
-
await payload.
|
|
5367
|
-
collection: slugs.ticketMessages,
|
|
5644
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5368
5645
|
id: msg.id,
|
|
5369
5646
|
data: { scheduledSent: true },
|
|
5370
5647
|
overrideAccess: true
|
|
5371
5648
|
});
|
|
5372
5649
|
}
|
|
5373
5650
|
if (msg.authorType === "admin" && !msg.isInternal) {
|
|
5374
|
-
await payload.
|
|
5375
|
-
collection: slugs.tickets,
|
|
5651
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
5376
5652
|
id: ticketId,
|
|
5377
5653
|
data: { status: "waiting_client" },
|
|
5378
5654
|
overrideAccess: true
|
|
@@ -5404,6 +5680,208 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5404
5680
|
};
|
|
5405
5681
|
}
|
|
5406
5682
|
|
|
5683
|
+
// src/endpoints/process-snooze.ts
|
|
5684
|
+
function createProcessSnoozeEndpoint(slugs) {
|
|
5685
|
+
return {
|
|
5686
|
+
path: "/support/process-snooze",
|
|
5687
|
+
method: "post",
|
|
5688
|
+
handler: async (req) => {
|
|
5689
|
+
const secret = req.headers.get("x-cron-secret");
|
|
5690
|
+
const expectedSecret = process.env.CRON_SECRET;
|
|
5691
|
+
if (!expectedSecret || secret !== expectedSecret) {
|
|
5692
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5693
|
+
}
|
|
5694
|
+
try {
|
|
5695
|
+
const payload = req.payload;
|
|
5696
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
5697
|
+
const results = { processed: 0, errors: 0 };
|
|
5698
|
+
const due = await dbFind(payload, slugs.tickets, {
|
|
5699
|
+
where: {
|
|
5700
|
+
and: [
|
|
5701
|
+
{ snoozeUntil: { exists: true } },
|
|
5702
|
+
{ snoozeUntil: { less_than_equal: nowIso } }
|
|
5703
|
+
]
|
|
5704
|
+
},
|
|
5705
|
+
limit: 500,
|
|
5706
|
+
depth: 0,
|
|
5707
|
+
overrideAccess: true,
|
|
5708
|
+
select: { id: true, ticketNumber: true }
|
|
5709
|
+
});
|
|
5710
|
+
for (const ticket of due.docs) {
|
|
5711
|
+
try {
|
|
5712
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
5713
|
+
id: ticket.id,
|
|
5714
|
+
data: { snoozeUntil: null },
|
|
5715
|
+
overrideAccess: true
|
|
5716
|
+
});
|
|
5717
|
+
try {
|
|
5718
|
+
await dbCreate(payload, slugs.ticketActivityLog, {
|
|
5719
|
+
data: {
|
|
5720
|
+
ticket: ticket.id,
|
|
5721
|
+
action: "snooze_expired",
|
|
5722
|
+
detail: "Snooze \xE9chu \u2014 ticket r\xE9activ\xE9",
|
|
5723
|
+
actorType: "system"
|
|
5724
|
+
},
|
|
5725
|
+
overrideAccess: true
|
|
5726
|
+
});
|
|
5727
|
+
} catch {
|
|
5728
|
+
}
|
|
5729
|
+
results.processed++;
|
|
5730
|
+
} catch (err) {
|
|
5731
|
+
console.error("[support] Failed to wake snoozed ticket:", err);
|
|
5732
|
+
results.errors++;
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
return Response.json({ ok: true, ...results });
|
|
5736
|
+
} catch (err) {
|
|
5737
|
+
console.error("[process-snooze] Error:", err);
|
|
5738
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
};
|
|
5742
|
+
}
|
|
5743
|
+
|
|
5744
|
+
// src/endpoints/process-digests.ts
|
|
5745
|
+
function createProcessDigestsEndpoint(slugs) {
|
|
5746
|
+
return {
|
|
5747
|
+
path: "/support/process-digests",
|
|
5748
|
+
method: "post",
|
|
5749
|
+
handler: async (req) => {
|
|
5750
|
+
const secret = req.headers.get("x-cron-secret");
|
|
5751
|
+
if (!process.env.CRON_SECRET || secret !== process.env.CRON_SECRET) {
|
|
5752
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5753
|
+
}
|
|
5754
|
+
try {
|
|
5755
|
+
const payload = req.payload;
|
|
5756
|
+
let body = {};
|
|
5757
|
+
try {
|
|
5758
|
+
body = await req.json();
|
|
5759
|
+
} catch {
|
|
5760
|
+
}
|
|
5761
|
+
const frequency = body.frequency === "weekly" ? "weekly" : "daily";
|
|
5762
|
+
const settings = await readSupportSettings(payload);
|
|
5763
|
+
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
|
|
5764
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
5765
|
+
const itemsByClient = /* @__PURE__ */ new Map();
|
|
5766
|
+
let page = 1;
|
|
5767
|
+
let hasMore = true;
|
|
5768
|
+
while (hasMore && page <= 50) {
|
|
5769
|
+
const batch = await dbFind(payload, slugs.notificationQueue, { limit: 200, page, depth: 0, overrideAccess: true, sort: "createdAt" });
|
|
5770
|
+
for (const it of batch.docs) {
|
|
5771
|
+
const raw = it;
|
|
5772
|
+
const clientId = typeof raw.client === "object" ? raw.client?.id : raw.client;
|
|
5773
|
+
if (clientId === void 0 || clientId === null) continue;
|
|
5774
|
+
const key = String(clientId);
|
|
5775
|
+
if (!itemsByClient.has(key)) itemsByClient.set(key, []);
|
|
5776
|
+
itemsByClient.get(key).push({ id: raw.id, title: raw.title, message: raw.message });
|
|
5777
|
+
}
|
|
5778
|
+
hasMore = batch.hasNextPage ?? false;
|
|
5779
|
+
page++;
|
|
5780
|
+
}
|
|
5781
|
+
const results = { clients: 0, items: 0, errors: 0 };
|
|
5782
|
+
for (const [clientId, items] of itemsByClient) {
|
|
5783
|
+
try {
|
|
5784
|
+
const client = await dbFindByID(payload, slugs.supportClients, { id: clientId, depth: 0, overrideAccess: true });
|
|
5785
|
+
if (!client || client.notificationFrequency !== frequency) continue;
|
|
5786
|
+
if (client.email) {
|
|
5787
|
+
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("");
|
|
5788
|
+
const html = emailWrapper("Recapitulatif de vos tickets", [
|
|
5789
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
|
|
5790
|
+
emailParagraph(`Voici le recapitulatif de l'activite sur vos tickets (${items.length} notification${items.length > 1 ? "s" : ""}) :`),
|
|
5791
|
+
`<table cellpadding="0" cellspacing="0" border="0" width="100%">${rows}</table>`,
|
|
5792
|
+
...baseUrl ? [emailParagraph(`<a href="${baseUrl}/support/dashboard">Acceder a votre espace support</a>`)] : []
|
|
5793
|
+
].join(""), { kind: "system" });
|
|
5794
|
+
await payload.sendEmail({ to: client.email, ...replyTo ? { replyTo } : {}, subject: `Recapitulatif support (${items.length})`, html });
|
|
5795
|
+
}
|
|
5796
|
+
for (const i of items) {
|
|
5797
|
+
try {
|
|
5798
|
+
await dbDelete(payload, slugs.notificationQueue, { id: i.id, overrideAccess: true });
|
|
5799
|
+
} catch {
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
results.clients++;
|
|
5803
|
+
results.items += items.length;
|
|
5804
|
+
} catch (err) {
|
|
5805
|
+
console.error("[process-digests] client error:", err);
|
|
5806
|
+
results.errors++;
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
return Response.json({ ok: true, frequency, ...results });
|
|
5810
|
+
} catch (err) {
|
|
5811
|
+
console.error("[process-digests] Error:", err);
|
|
5812
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
};
|
|
5816
|
+
}
|
|
5817
|
+
var PROVIDERS = ["whatsapp", "messenger"];
|
|
5818
|
+
function createChannelsWebhookEndpoint(slugs) {
|
|
5819
|
+
return {
|
|
5820
|
+
path: "/support/channels/webhook",
|
|
5821
|
+
method: "post",
|
|
5822
|
+
handler: async (req) => {
|
|
5823
|
+
const secret = req.headers.get("x-channel-secret");
|
|
5824
|
+
if (!process.env.CHANNELS_WEBHOOK_SECRET || secret !== process.env.CHANNELS_WEBHOOK_SECRET) {
|
|
5825
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5826
|
+
}
|
|
5827
|
+
let body;
|
|
5828
|
+
try {
|
|
5829
|
+
body = await req.json();
|
|
5830
|
+
} catch {
|
|
5831
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5832
|
+
}
|
|
5833
|
+
const provider = body.provider;
|
|
5834
|
+
const from = (body.from || "").trim();
|
|
5835
|
+
const text = (body.text || "").trim();
|
|
5836
|
+
if (!PROVIDERS.includes(provider) || !from || !text) {
|
|
5837
|
+
return Response.json({ error: "provider, from et text requis." }, { status: 400 });
|
|
5838
|
+
}
|
|
5839
|
+
try {
|
|
5840
|
+
const payload = req.payload;
|
|
5841
|
+
const email = `${from}@${provider}.channel`;
|
|
5842
|
+
const existing = await dbFind(payload, slugs.supportClients, { where: { email: { equals: email } }, limit: 1, depth: 0, overrideAccess: true });
|
|
5843
|
+
let clientId;
|
|
5844
|
+
if (existing.docs.length > 0) {
|
|
5845
|
+
clientId = existing.docs[0].id;
|
|
5846
|
+
} else {
|
|
5847
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
5848
|
+
data: { email, firstName: body.name || provider, lastName: from.slice(0, 40), company: provider, password: crypto3.randomBytes(16).toString("hex") },
|
|
5849
|
+
overrideAccess: true
|
|
5850
|
+
});
|
|
5851
|
+
clientId = created.id;
|
|
5852
|
+
}
|
|
5853
|
+
const open = await dbFind(payload, slugs.tickets, {
|
|
5854
|
+
where: { and: [{ client: { equals: clientId } }, { source: { equals: provider } }, { status: { in: ["open", "waiting_client"] } }] },
|
|
5855
|
+
sort: "-createdAt",
|
|
5856
|
+
limit: 1,
|
|
5857
|
+
depth: 0,
|
|
5858
|
+
overrideAccess: true
|
|
5859
|
+
});
|
|
5860
|
+
let ticketId;
|
|
5861
|
+
let createdTicket = false;
|
|
5862
|
+
if (open.docs.length > 0) {
|
|
5863
|
+
ticketId = open.docs[0].id;
|
|
5864
|
+
} else {
|
|
5865
|
+
const t = await dbCreate(payload, slugs.tickets, {
|
|
5866
|
+
data: { subject: text.slice(0, 80), client: clientId, status: "open", priority: "normal", source: provider },
|
|
5867
|
+
overrideAccess: true
|
|
5868
|
+
});
|
|
5869
|
+
ticketId = t.id;
|
|
5870
|
+
createdTicket = true;
|
|
5871
|
+
}
|
|
5872
|
+
const msg = await dbCreate(payload, slugs.ticketMessages, {
|
|
5873
|
+
data: { ticket: ticketId, body: text, authorType: "client", authorClient: clientId },
|
|
5874
|
+
overrideAccess: true
|
|
5875
|
+
});
|
|
5876
|
+
return Response.json({ ok: true, ticketId, createdTicket, messageId: msg.id });
|
|
5877
|
+
} catch (err) {
|
|
5878
|
+
console.error("[channels] Error:", err);
|
|
5879
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
};
|
|
5883
|
+
}
|
|
5884
|
+
|
|
5407
5885
|
// src/endpoints/user-prefs.ts
|
|
5408
5886
|
var PREF_KEY_PREFIX = "support-user-prefs";
|
|
5409
5887
|
var DEFAULT_USER_PREFS2 = {
|
|
@@ -5419,8 +5897,7 @@ function createUserPrefsGetEndpoint(slugs) {
|
|
|
5419
5897
|
const payload = req.payload;
|
|
5420
5898
|
requireAdmin(req, slugs);
|
|
5421
5899
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5422
|
-
const prefs = await payload
|
|
5423
|
-
collection: "payload-preferences",
|
|
5900
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
5424
5901
|
where: { key: { equals: key } },
|
|
5425
5902
|
limit: 1,
|
|
5426
5903
|
depth: 0,
|
|
@@ -5454,8 +5931,7 @@ function createUserPrefsPostEndpoint(slugs) {
|
|
|
5454
5931
|
requireAdmin(req, slugs);
|
|
5455
5932
|
const body = await req.json();
|
|
5456
5933
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5457
|
-
const existing = await payload
|
|
5458
|
-
collection: "payload-preferences",
|
|
5934
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
5459
5935
|
where: { key: { equals: key } },
|
|
5460
5936
|
limit: 1,
|
|
5461
5937
|
depth: 0,
|
|
@@ -5516,8 +5992,7 @@ function createEscalateEndpoint(slugs) {
|
|
|
5516
5992
|
if (Number.isNaN(ticketId)) {
|
|
5517
5993
|
return Response.json({ error: "invalid-id" }, { status: 400 });
|
|
5518
5994
|
}
|
|
5519
|
-
const ticket = await req.payload.
|
|
5520
|
-
collection: slugs.tickets,
|
|
5995
|
+
const ticket = await dbUpdate(req.payload, slugs.tickets, {
|
|
5521
5996
|
id: ticketId,
|
|
5522
5997
|
data: { status: "escalated" },
|
|
5523
5998
|
overrideAccess: true
|
|
@@ -5565,8 +6040,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5565
6040
|
{ status: 400 }
|
|
5566
6041
|
);
|
|
5567
6042
|
}
|
|
5568
|
-
const ticket = await payload.
|
|
5569
|
-
collection: slugs.tickets,
|
|
6043
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5570
6044
|
id: ticketId,
|
|
5571
6045
|
depth: 0,
|
|
5572
6046
|
overrideAccess: true
|
|
@@ -5578,8 +6052,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5578
6052
|
if (ticketClientId !== req.user.id) {
|
|
5579
6053
|
return Response.json({ error: "forbidden" }, { status: 403 });
|
|
5580
6054
|
}
|
|
5581
|
-
const existing = await payload
|
|
5582
|
-
collection: "ticket-feedback",
|
|
6055
|
+
const existing = await dbFind(payload, "ticket-feedback", {
|
|
5583
6056
|
where: { ticket: { equals: ticketId } },
|
|
5584
6057
|
limit: 1,
|
|
5585
6058
|
depth: 0,
|
|
@@ -5591,8 +6064,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5591
6064
|
{ status: 409 }
|
|
5592
6065
|
);
|
|
5593
6066
|
}
|
|
5594
|
-
const feedback = await payload
|
|
5595
|
-
collection: "ticket-feedback",
|
|
6067
|
+
const feedback = await dbCreate(payload, "ticket-feedback", {
|
|
5596
6068
|
data: {
|
|
5597
6069
|
ticket: ticketId,
|
|
5598
6070
|
client: req.user.id,
|
|
@@ -5616,6 +6088,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5616
6088
|
// src/endpoints/transfer-ticket.ts
|
|
5617
6089
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5618
6090
|
var MAX_TRANSFERS_PER_DAY = 5;
|
|
6091
|
+
var transferLimiter = new RateLimiter(24 * 60 * 60 * 1e3, MAX_TRANSFERS_PER_DAY);
|
|
5619
6092
|
function createTransferTicketEndpoint(slugs) {
|
|
5620
6093
|
return {
|
|
5621
6094
|
path: "/support/tickets/:id/transfer",
|
|
@@ -5640,8 +6113,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5640
6113
|
const includeAttachments = body.includeAttachments !== false;
|
|
5641
6114
|
const customMessage = typeof body.message === "string" ? body.message.slice(0, 1e3) : "";
|
|
5642
6115
|
const payload = req.payload;
|
|
5643
|
-
const ticket = await payload.
|
|
5644
|
-
collection: slugs.tickets,
|
|
6116
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5645
6117
|
id: ticketId,
|
|
5646
6118
|
depth: 1,
|
|
5647
6119
|
overrideAccess: true
|
|
@@ -5652,10 +6124,15 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5652
6124
|
if (!isAdmin && !isOwner) {
|
|
5653
6125
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5654
6126
|
}
|
|
6127
|
+
if (transferLimiter.check(`${req.user.id}:${ticketId}`)) {
|
|
6128
|
+
return Response.json(
|
|
6129
|
+
{ error: `Limite atteinte (${MAX_TRANSFERS_PER_DAY} transferts par 24h sur ce ticket)` },
|
|
6130
|
+
{ status: 429 }
|
|
6131
|
+
);
|
|
6132
|
+
}
|
|
5655
6133
|
try {
|
|
5656
6134
|
const since = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
5657
|
-
const existing = await payload.
|
|
5658
|
-
collection: slugs.emailLogs,
|
|
6135
|
+
const existing = await dbCount(payload, slugs.emailLogs, {
|
|
5659
6136
|
where: {
|
|
5660
6137
|
and: [
|
|
5661
6138
|
{ action: { equals: "transfer" } },
|
|
@@ -5673,8 +6150,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5673
6150
|
}
|
|
5674
6151
|
} catch {
|
|
5675
6152
|
}
|
|
5676
|
-
const messages = await payload.
|
|
5677
|
-
collection: slugs.ticketMessages,
|
|
6153
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
5678
6154
|
where: {
|
|
5679
6155
|
and: [
|
|
5680
6156
|
{ ticket: { equals: ticketId } },
|
|
@@ -5754,8 +6230,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5754
6230
|
html
|
|
5755
6231
|
});
|
|
5756
6232
|
try {
|
|
5757
|
-
await payload.
|
|
5758
|
-
collection: slugs.emailLogs,
|
|
6233
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5759
6234
|
data: {
|
|
5760
6235
|
status: "success",
|
|
5761
6236
|
action: "transfer",
|
|
@@ -5795,6 +6270,8 @@ function statusToLabel(status) {
|
|
|
5795
6270
|
}
|
|
5796
6271
|
}
|
|
5797
6272
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6273
|
+
var MAX_COLLABORATORS_PER_TICKET = 20;
|
|
6274
|
+
var inviteLimiter = new RateLimiter(60 * 60 * 1e3, 10);
|
|
5798
6275
|
function createInviteCollaboratorEndpoint(slugs) {
|
|
5799
6276
|
return {
|
|
5800
6277
|
path: "/support/tickets/:id/invite",
|
|
@@ -5818,8 +6295,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5818
6295
|
}
|
|
5819
6296
|
const role = body.role === "collaborator" ? "collaborator" : "viewer";
|
|
5820
6297
|
const payload = req.payload;
|
|
5821
|
-
const ticket = await payload.
|
|
5822
|
-
collection: slugs.tickets,
|
|
6298
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5823
6299
|
id: ticketId,
|
|
5824
6300
|
depth: 1,
|
|
5825
6301
|
overrideAccess: true
|
|
@@ -5831,13 +6307,22 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5831
6307
|
if (!isAdmin && !isOwner) {
|
|
5832
6308
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5833
6309
|
}
|
|
6310
|
+
if (inviteLimiter.check(String(req.user.id))) {
|
|
6311
|
+
return Response.json({ error: "Trop d'invitations. R\xE9essayez plus tard." }, { status: 429 });
|
|
6312
|
+
}
|
|
6313
|
+
const collabCount = await dbCount(payload, "ticket-collaborators", { where: { ticket: { equals: ticketId } }, overrideAccess: true }).catch(() => ({ totalDocs: 0 }));
|
|
6314
|
+
if (collabCount.totalDocs >= MAX_COLLABORATORS_PER_TICKET) {
|
|
6315
|
+
return Response.json(
|
|
6316
|
+
{ error: `Limite de ${MAX_COLLABORATORS_PER_TICKET} collaborateurs atteinte sur ce ticket` },
|
|
6317
|
+
{ status: 429 }
|
|
6318
|
+
);
|
|
6319
|
+
}
|
|
5834
6320
|
const ownerEmail = typeof ticket.client === "object" ? ticket.client?.email : void 0;
|
|
5835
6321
|
if (ownerEmail && ownerEmail.toLowerCase() === email) {
|
|
5836
6322
|
return Response.json({ error: "Ce client est d\xE9j\xE0 propri\xE9taire du ticket" }, { status: 400 });
|
|
5837
6323
|
}
|
|
5838
6324
|
let inviteeId = null;
|
|
5839
|
-
const existing = await payload.
|
|
5840
|
-
collection: slugs.supportClients,
|
|
6325
|
+
const existing = await dbFind(payload, slugs.supportClients, {
|
|
5841
6326
|
where: { email: { equals: email } },
|
|
5842
6327
|
limit: 1,
|
|
5843
6328
|
depth: 0,
|
|
@@ -5847,8 +6332,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5847
6332
|
inviteeId = existing.docs[0].id;
|
|
5848
6333
|
} else {
|
|
5849
6334
|
const tempPassword = crypto3.randomBytes(16).toString("hex");
|
|
5850
|
-
const created = await payload.
|
|
5851
|
-
collection: slugs.supportClients,
|
|
6335
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
5852
6336
|
data: {
|
|
5853
6337
|
email,
|
|
5854
6338
|
firstName: "Invit\xE9",
|
|
@@ -5865,8 +6349,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5865
6349
|
if (!inviteeId) {
|
|
5866
6350
|
return Response.json({ error: "Impossible de cr\xE9er le client invit\xE9" }, { status: 500 });
|
|
5867
6351
|
}
|
|
5868
|
-
const dupe = await payload
|
|
5869
|
-
collection: "ticket-collaborators",
|
|
6352
|
+
const dupe = await dbFind(payload, "ticket-collaborators", {
|
|
5870
6353
|
where: {
|
|
5871
6354
|
and: [
|
|
5872
6355
|
{ ticket: { equals: ticketId } },
|
|
@@ -5879,15 +6362,13 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5879
6362
|
});
|
|
5880
6363
|
const invitationToken = crypto3.randomBytes(24).toString("hex");
|
|
5881
6364
|
if (dupe.docs.length > 0) {
|
|
5882
|
-
await payload
|
|
5883
|
-
collection: "ticket-collaborators",
|
|
6365
|
+
await dbUpdate(payload, "ticket-collaborators", {
|
|
5884
6366
|
id: dupe.docs[0].id,
|
|
5885
6367
|
data: { role, invitationToken },
|
|
5886
6368
|
overrideAccess: true
|
|
5887
6369
|
});
|
|
5888
6370
|
} else {
|
|
5889
|
-
await payload
|
|
5890
|
-
collection: "ticket-collaborators",
|
|
6371
|
+
await dbCreate(payload, "ticket-collaborators", {
|
|
5891
6372
|
data: {
|
|
5892
6373
|
ticket: ticketId,
|
|
5893
6374
|
client: inviteeId,
|
|
@@ -5930,8 +6411,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5930
6411
|
)
|
|
5931
6412
|
});
|
|
5932
6413
|
try {
|
|
5933
|
-
await payload.
|
|
5934
|
-
collection: slugs.emailLogs,
|
|
6414
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5935
6415
|
data: {
|
|
5936
6416
|
status: "success",
|
|
5937
6417
|
action: "invite",
|
|
@@ -5984,6 +6464,7 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5984
6464
|
];
|
|
5985
6465
|
if (!f || f.ai !== false) {
|
|
5986
6466
|
endpoints.push(createAiEndpoint(slugs));
|
|
6467
|
+
endpoints.push(createAiAgentEndpoint(slugs));
|
|
5987
6468
|
endpoints.push(...createClientIntelligenceEndpoint(slugs));
|
|
5988
6469
|
endpoints.push(createTicketSynthesisEndpoint(slugs));
|
|
5989
6470
|
}
|
|
@@ -6014,21 +6495,26 @@ function createSupportEndpoints(slugs, options) {
|
|
|
6014
6495
|
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs));
|
|
6015
6496
|
endpoints.push(createAdminChatStreamEndpoint(slugs));
|
|
6016
6497
|
}
|
|
6017
|
-
if (!f || f.timeTracking !== false)
|
|
6498
|
+
if (!f || f.timeTracking !== false) {
|
|
6499
|
+
endpoints.push(createBillingEndpoint(slugs));
|
|
6500
|
+
endpoints.push(createInvoiceEndpoint(slugs));
|
|
6501
|
+
}
|
|
6018
6502
|
if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs));
|
|
6019
6503
|
if (!f || f.emailTracking !== false) {
|
|
6020
6504
|
endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs));
|
|
6021
6505
|
}
|
|
6022
6506
|
if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs));
|
|
6023
6507
|
if (!f || f.scheduledReplies !== false) endpoints.push(createProcessScheduledEndpoint(slugs));
|
|
6508
|
+
if (!f || f.snooze !== false) endpoints.push(createProcessSnoozeEndpoint(slugs));
|
|
6509
|
+
endpoints.push(createProcessDigestsEndpoint(slugs));
|
|
6510
|
+
endpoints.push(createChannelsWebhookEndpoint(slugs));
|
|
6024
6511
|
return endpoints;
|
|
6025
6512
|
}
|
|
6026
6513
|
|
|
6027
6514
|
// src/utils/adminNotification.ts
|
|
6028
6515
|
async function createAdminNotification(payload, data, collectionSlug = "admin-notifications") {
|
|
6029
6516
|
try {
|
|
6030
|
-
await payload
|
|
6031
|
-
collection: collectionSlug,
|
|
6517
|
+
await dbCreate(payload, collectionSlug, {
|
|
6032
6518
|
data: {
|
|
6033
6519
|
title: data.title,
|
|
6034
6520
|
message: data.message,
|
|
@@ -6048,8 +6534,7 @@ function dispatchWebhook(data, event, payload, slugs) {
|
|
|
6048
6534
|
}
|
|
6049
6535
|
async function _dispatch(data, event, payload, slugs) {
|
|
6050
6536
|
try {
|
|
6051
|
-
const { docs: endpoints } = await payload.
|
|
6052
|
-
collection: slugs.webhookEndpoints,
|
|
6537
|
+
const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
|
|
6053
6538
|
where: {
|
|
6054
6539
|
and: [
|
|
6055
6540
|
{ active: { equals: true } },
|
|
@@ -6086,8 +6571,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
6086
6571
|
signal: AbortSignal.timeout(1e4)
|
|
6087
6572
|
// 10s timeout
|
|
6088
6573
|
});
|
|
6089
|
-
await payload.
|
|
6090
|
-
collection: slugs.webhookEndpoints,
|
|
6574
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6091
6575
|
id: endpoint.id,
|
|
6092
6576
|
data: {
|
|
6093
6577
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6101,8 +6585,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
6101
6585
|
} catch (err) {
|
|
6102
6586
|
console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
|
|
6103
6587
|
try {
|
|
6104
|
-
await payload.
|
|
6105
|
-
collection: slugs.webhookEndpoints,
|
|
6588
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6106
6589
|
id: endpoint.id,
|
|
6107
6590
|
data: {
|
|
6108
6591
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6336,8 +6819,7 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6336
6819
|
if (typeof policyRef === "object" && policyRef.firstResponseTime) {
|
|
6337
6820
|
return policyRef;
|
|
6338
6821
|
}
|
|
6339
|
-
return await payload.
|
|
6340
|
-
collection: slugs.slaPolicies,
|
|
6822
|
+
return await dbFindByID(payload, slugs.slaPolicies, {
|
|
6341
6823
|
id: policyId,
|
|
6342
6824
|
depth: 0,
|
|
6343
6825
|
overrideAccess: true
|
|
@@ -6348,8 +6830,7 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6348
6830
|
}
|
|
6349
6831
|
const priority = field(ticket, "priority") || "normal";
|
|
6350
6832
|
try {
|
|
6351
|
-
const defaults = await payload.
|
|
6352
|
-
collection: slugs.slaPolicies,
|
|
6833
|
+
const defaults = await dbFind(payload, slugs.slaPolicies, {
|
|
6353
6834
|
where: {
|
|
6354
6835
|
and: [
|
|
6355
6836
|
{ isDefault: { equals: true } },
|
|
@@ -6386,8 +6867,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6386
6867
|
if (typeof policyRef === "object" && policyRef.escalateOnBreach !== void 0) {
|
|
6387
6868
|
policy = policyRef;
|
|
6388
6869
|
} else {
|
|
6389
|
-
policy = await payload.
|
|
6390
|
-
collection: slugs.slaPolicies,
|
|
6870
|
+
policy = await dbFindByID(payload, slugs.slaPolicies, {
|
|
6391
6871
|
id: policyId,
|
|
6392
6872
|
depth: 0,
|
|
6393
6873
|
overrideAccess: true
|
|
@@ -6400,8 +6880,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6400
6880
|
try {
|
|
6401
6881
|
const escalateToId = typeof policy.escalateTo === "object" ? policy.escalateTo.id : policy.escalateTo;
|
|
6402
6882
|
if (!escalateToId) return;
|
|
6403
|
-
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await payload.
|
|
6404
|
-
collection: slugs.users,
|
|
6883
|
+
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await dbFindByID(payload, slugs.users, {
|
|
6405
6884
|
id: escalateToId,
|
|
6406
6885
|
depth: 0,
|
|
6407
6886
|
overrideAccess: true
|
|
@@ -6449,8 +6928,7 @@ function createAssignSlaDeadlines(slugs, notificationSlug = "admin-notifications
|
|
|
6449
6928
|
if (!field(doc, "slaPolicy") && policy.id) {
|
|
6450
6929
|
updateData.slaPolicy = policy.id;
|
|
6451
6930
|
}
|
|
6452
|
-
await payload.
|
|
6453
|
-
collection: slugs.tickets,
|
|
6931
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6454
6932
|
id: doc.id,
|
|
6455
6933
|
data: updateData,
|
|
6456
6934
|
overrideAccess: true
|
|
@@ -6473,8 +6951,7 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6473
6951
|
const now = /* @__PURE__ */ new Date();
|
|
6474
6952
|
const deadline = new Date(doc.slaResolutionDue);
|
|
6475
6953
|
const breached = now > deadline;
|
|
6476
|
-
await payload.
|
|
6477
|
-
collection: slugs.tickets,
|
|
6954
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6478
6955
|
id: doc.id,
|
|
6479
6956
|
data: { slaResolutionBreached: breached },
|
|
6480
6957
|
overrideAccess: true
|
|
@@ -6488,6 +6965,35 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6488
6965
|
return doc;
|
|
6489
6966
|
};
|
|
6490
6967
|
}
|
|
6968
|
+
function createPauseSlaOnHold(slugs) {
|
|
6969
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
6970
|
+
if (operation !== "update" || !previousDoc) return doc;
|
|
6971
|
+
if (previousDoc.status === doc.status) return doc;
|
|
6972
|
+
if (!doc.slaResolutionDue) return doc;
|
|
6973
|
+
const wasWaiting = previousDoc.status === "waiting_client";
|
|
6974
|
+
const isWaiting = doc.status === "waiting_client";
|
|
6975
|
+
try {
|
|
6976
|
+
if (!wasWaiting && isWaiting && !doc.slaPausedAt && doc.status !== "resolved") {
|
|
6977
|
+
await req.payload.update({
|
|
6978
|
+
collection: slugs.tickets,
|
|
6979
|
+
id: doc.id,
|
|
6980
|
+
data: { slaPausedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
6981
|
+
overrideAccess: true
|
|
6982
|
+
});
|
|
6983
|
+
} else if (wasWaiting && !isWaiting && doc.slaPausedAt) {
|
|
6984
|
+
const pausedMs = Date.now() - new Date(doc.slaPausedAt).getTime();
|
|
6985
|
+
const data = { slaPausedAt: null };
|
|
6986
|
+
if (pausedMs > 0) {
|
|
6987
|
+
data.slaResolutionDue = new Date(new Date(doc.slaResolutionDue).getTime() + pausedMs).toISOString();
|
|
6988
|
+
}
|
|
6989
|
+
await req.payload.update({ collection: slugs.tickets, id: doc.id, data, overrideAccess: true });
|
|
6990
|
+
}
|
|
6991
|
+
} catch (err) {
|
|
6992
|
+
console.error("[sla] Failed to pause/resume SLA on hold:", err);
|
|
6993
|
+
}
|
|
6994
|
+
return doc;
|
|
6995
|
+
};
|
|
6996
|
+
}
|
|
6491
6997
|
function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications") {
|
|
6492
6998
|
return async ({ doc, operation, req }) => {
|
|
6493
6999
|
if (operation !== "create") return doc;
|
|
@@ -6495,8 +7001,7 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6495
7001
|
try {
|
|
6496
7002
|
const { payload } = req;
|
|
6497
7003
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
6498
|
-
const ticket = await payload.
|
|
6499
|
-
collection: slugs.tickets,
|
|
7004
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
6500
7005
|
id: ticketId,
|
|
6501
7006
|
depth: 1,
|
|
6502
7007
|
overrideAccess: true
|
|
@@ -6507,8 +7012,7 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6507
7012
|
const now = /* @__PURE__ */ new Date();
|
|
6508
7013
|
const deadline = new Date(ticket.slaFirstResponseDue);
|
|
6509
7014
|
const breached = now > deadline;
|
|
6510
|
-
await payload.
|
|
6511
|
-
collection: slugs.tickets,
|
|
7015
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6512
7016
|
id: ticketId,
|
|
6513
7017
|
data: { slaFirstResponseBreached: breached },
|
|
6514
7018
|
overrideAccess: true
|
|
@@ -6523,6 +7027,108 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6523
7027
|
};
|
|
6524
7028
|
}
|
|
6525
7029
|
|
|
7030
|
+
// src/hooks/automationRules.ts
|
|
7031
|
+
function evaluateConditions(doc, rule) {
|
|
7032
|
+
const conds = Array.isArray(rule.conditions) ? rule.conditions : [];
|
|
7033
|
+
if (conds.length === 0) return true;
|
|
7034
|
+
const results = conds.map((c) => {
|
|
7035
|
+
const fieldVal = String(doc[c.field] ?? "");
|
|
7036
|
+
const v = String(c.value ?? "");
|
|
7037
|
+
switch (c.operator) {
|
|
7038
|
+
case "equals":
|
|
7039
|
+
return fieldVal === v;
|
|
7040
|
+
case "not_equals":
|
|
7041
|
+
return fieldVal !== v;
|
|
7042
|
+
case "contains":
|
|
7043
|
+
return fieldVal.toLowerCase().includes(v.toLowerCase());
|
|
7044
|
+
default:
|
|
7045
|
+
return false;
|
|
7046
|
+
}
|
|
7047
|
+
});
|
|
7048
|
+
return rule.matchType === "any" ? results.some(Boolean) : results.every(Boolean);
|
|
7049
|
+
}
|
|
7050
|
+
function createApplyAutomationRules(slugs) {
|
|
7051
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
7052
|
+
if (req.context?.skipAutomation) return doc;
|
|
7053
|
+
const events = [];
|
|
7054
|
+
if (operation === "create") events.push("ticket_created");
|
|
7055
|
+
if (operation === "update") {
|
|
7056
|
+
events.push("ticket_updated");
|
|
7057
|
+
if (previousDoc && previousDoc.status !== doc.status) events.push("ticket_status_changed");
|
|
7058
|
+
}
|
|
7059
|
+
if (events.length === 0) return doc;
|
|
7060
|
+
try {
|
|
7061
|
+
const rules = await dbFind(req.payload, slugs.automationRules, {
|
|
7062
|
+
where: { and: [{ enabled: { equals: true } }, { event: { in: events } }] },
|
|
7063
|
+
sort: "order",
|
|
7064
|
+
limit: 200,
|
|
7065
|
+
depth: 0,
|
|
7066
|
+
overrideAccess: true
|
|
7067
|
+
});
|
|
7068
|
+
const changes = {};
|
|
7069
|
+
const tags = Array.isArray(doc.tags) ? [...doc.tags] : [];
|
|
7070
|
+
let tagsTouched = false;
|
|
7071
|
+
for (const rule of rules.docs) {
|
|
7072
|
+
if (!evaluateConditions(doc, rule)) continue;
|
|
7073
|
+
for (const a of Array.isArray(rule.actions) ? rule.actions : []) {
|
|
7074
|
+
if (!a.value) continue;
|
|
7075
|
+
switch (a.type) {
|
|
7076
|
+
case "set_status":
|
|
7077
|
+
changes.status = a.value;
|
|
7078
|
+
break;
|
|
7079
|
+
case "set_priority":
|
|
7080
|
+
changes.priority = a.value;
|
|
7081
|
+
break;
|
|
7082
|
+
case "set_category":
|
|
7083
|
+
changes.category = a.value;
|
|
7084
|
+
break;
|
|
7085
|
+
case "assign":
|
|
7086
|
+
changes.assignedTo = a.value;
|
|
7087
|
+
break;
|
|
7088
|
+
case "add_tag":
|
|
7089
|
+
if (!tags.includes(a.value)) {
|
|
7090
|
+
tags.push(a.value);
|
|
7091
|
+
tagsTouched = true;
|
|
7092
|
+
}
|
|
7093
|
+
break;
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
7096
|
+
}
|
|
7097
|
+
if (tagsTouched) changes.tags = tags;
|
|
7098
|
+
if (Object.keys(changes).length > 0) {
|
|
7099
|
+
await req.payload.update({
|
|
7100
|
+
collection: slugs.tickets,
|
|
7101
|
+
id: doc.id,
|
|
7102
|
+
data: changes,
|
|
7103
|
+
overrideAccess: true,
|
|
7104
|
+
context: { skipAutomation: true }
|
|
7105
|
+
});
|
|
7106
|
+
}
|
|
7107
|
+
} catch (err) {
|
|
7108
|
+
console.error("[support] Failed to apply automation rules:", err);
|
|
7109
|
+
}
|
|
7110
|
+
return doc;
|
|
7111
|
+
};
|
|
7112
|
+
}
|
|
7113
|
+
|
|
7114
|
+
// src/utils/teamAccess.ts
|
|
7115
|
+
function isTeamScopingEnabled() {
|
|
7116
|
+
return process.env.SUPPORT_TEAM_SCOPING === "1" || process.env.SUPPORT_TEAM_SCOPING === "true";
|
|
7117
|
+
}
|
|
7118
|
+
async function resolveAgentTeamIds(payload, slugs, userId) {
|
|
7119
|
+
try {
|
|
7120
|
+
const teams = await dbFind(payload, slugs.supportTeams, {
|
|
7121
|
+
where: { members: { in: [userId] } },
|
|
7122
|
+
limit: 200,
|
|
7123
|
+
depth: 0,
|
|
7124
|
+
overrideAccess: true
|
|
7125
|
+
});
|
|
7126
|
+
return teams.docs.map((t) => t.id);
|
|
7127
|
+
} catch {
|
|
7128
|
+
return [];
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
|
|
6526
7132
|
// src/collections/Tickets.ts
|
|
6527
7133
|
function createAssignTicketNumber(slugs) {
|
|
6528
7134
|
return async ({ data, operation, req }) => {
|
|
@@ -6785,56 +7391,6 @@ function createNotifyClientOnResolve(slugs) {
|
|
|
6785
7391
|
return doc;
|
|
6786
7392
|
};
|
|
6787
7393
|
}
|
|
6788
|
-
function createAutoCalculateSLA(slugs) {
|
|
6789
|
-
return async ({ doc, operation, req }) => {
|
|
6790
|
-
if (operation !== "create") return doc;
|
|
6791
|
-
try {
|
|
6792
|
-
const { payload } = req;
|
|
6793
|
-
const ticketPriority = doc.priority || "normal";
|
|
6794
|
-
let policy = null;
|
|
6795
|
-
const byPriority = await payload.find({
|
|
6796
|
-
collection: slugs.slaPolicies,
|
|
6797
|
-
where: { priority: { equals: ticketPriority } },
|
|
6798
|
-
limit: 1,
|
|
6799
|
-
depth: 0,
|
|
6800
|
-
overrideAccess: true
|
|
6801
|
-
});
|
|
6802
|
-
if (byPriority.docs.length > 0) {
|
|
6803
|
-
policy = byPriority.docs[0];
|
|
6804
|
-
} else {
|
|
6805
|
-
const defaults = await payload.find({
|
|
6806
|
-
collection: slugs.slaPolicies,
|
|
6807
|
-
where: { isDefault: { equals: true } },
|
|
6808
|
-
limit: 1,
|
|
6809
|
-
depth: 0,
|
|
6810
|
-
overrideAccess: true
|
|
6811
|
-
});
|
|
6812
|
-
if (defaults.docs.length > 0) {
|
|
6813
|
-
policy = defaults.docs[0];
|
|
6814
|
-
}
|
|
6815
|
-
}
|
|
6816
|
-
if (!policy) return doc;
|
|
6817
|
-
const now = /* @__PURE__ */ new Date();
|
|
6818
|
-
const firstResponseMinutes = policy.firstResponseTime || 240;
|
|
6819
|
-
const resolutionMinutes = policy.resolutionTime || 1440;
|
|
6820
|
-
const firstResponseDue = new Date(now.getTime() + firstResponseMinutes * 6e4);
|
|
6821
|
-
const resolutionDue = new Date(now.getTime() + resolutionMinutes * 6e4);
|
|
6822
|
-
await payload.update({
|
|
6823
|
-
collection: slugs.tickets,
|
|
6824
|
-
id: doc.id,
|
|
6825
|
-
data: {
|
|
6826
|
-
slaPolicy: policy.id,
|
|
6827
|
-
slaFirstResponseDue: firstResponseDue.toISOString(),
|
|
6828
|
-
slaResolutionDue: resolutionDue.toISOString()
|
|
6829
|
-
},
|
|
6830
|
-
overrideAccess: true
|
|
6831
|
-
});
|
|
6832
|
-
} catch (err) {
|
|
6833
|
-
console.error("[support] Failed to auto-calculate SLA:", err);
|
|
6834
|
-
}
|
|
6835
|
-
return doc;
|
|
6836
|
-
};
|
|
6837
|
-
}
|
|
6838
7394
|
function createFireTicketWebhooks(slugs) {
|
|
6839
7395
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
6840
7396
|
const { payload } = req;
|
|
@@ -6926,8 +7482,7 @@ function createCascadeDelete(slugs) {
|
|
|
6926
7482
|
return async ({ id, req }) => {
|
|
6927
7483
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys];
|
|
6928
7484
|
for (const slug of collections) {
|
|
6929
|
-
await req.payload
|
|
6930
|
-
collection: slug,
|
|
7485
|
+
await dbDelete(req.payload, slug, {
|
|
6931
7486
|
where: { ticket: { equals: id } },
|
|
6932
7487
|
overrideAccess: true
|
|
6933
7488
|
});
|
|
@@ -7052,6 +7607,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7052
7607
|
{
|
|
7053
7608
|
name: "status",
|
|
7054
7609
|
type: "select",
|
|
7610
|
+
index: true,
|
|
7055
7611
|
defaultValue: "open",
|
|
7056
7612
|
label: "Statut",
|
|
7057
7613
|
options: [
|
|
@@ -7066,6 +7622,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7066
7622
|
{
|
|
7067
7623
|
name: "priority",
|
|
7068
7624
|
type: "select",
|
|
7625
|
+
index: true,
|
|
7069
7626
|
defaultValue: "normal",
|
|
7070
7627
|
label: "Priorite",
|
|
7071
7628
|
options: [
|
|
@@ -7081,7 +7638,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7081
7638
|
{
|
|
7082
7639
|
type: "row",
|
|
7083
7640
|
fields: [
|
|
7084
|
-
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, label: "Client", admin: { width: "50%" } }
|
|
7641
|
+
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, index: true, label: "Client", admin: { width: "50%" } }
|
|
7085
7642
|
]
|
|
7086
7643
|
},
|
|
7087
7644
|
// Billing collapsible
|
|
@@ -7150,15 +7707,16 @@ function createTicketsCollection(slugs, options) {
|
|
|
7150
7707
|
{
|
|
7151
7708
|
type: "row",
|
|
7152
7709
|
fields: [
|
|
7153
|
-
{ name: "slaFirstResponseDue", type: "date", label: "Echeance 1ere reponse (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7154
|
-
{ name: "slaResolutionDue", type: "date", label: "Echeance resolution (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
7710
|
+
{ name: "slaFirstResponseDue", type: "date", index: true, label: "Echeance 1ere reponse (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7711
|
+
{ name: "slaResolutionDue", type: "date", index: true, label: "Echeance resolution (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
7155
7712
|
]
|
|
7156
7713
|
},
|
|
7157
7714
|
{
|
|
7158
7715
|
type: "row",
|
|
7159
7716
|
fields: [
|
|
7160
7717
|
{ name: "slaFirstResponseBreached", type: "checkbox", label: "1ere reponse depassee", admin: { readOnly: true, width: "50%" } },
|
|
7161
|
-
{ name: "slaResolutionBreached", type: "checkbox", label: "Resolution depassee", admin: { readOnly: true, width: "50%" } }
|
|
7718
|
+
{ name: "slaResolutionBreached", type: "checkbox", label: "Resolution depassee", admin: { readOnly: true, width: "50%" } },
|
|
7719
|
+
{ name: "slaPausedAt", type: "date", label: "SLA en pause depuis", admin: { hidden: true } }
|
|
7162
7720
|
]
|
|
7163
7721
|
}
|
|
7164
7722
|
]
|
|
@@ -7180,7 +7738,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7180
7738
|
},
|
|
7181
7739
|
{ name: "mergedInto", type: "relationship", relationTo: slugs.tickets, label: "Fusionne dans", admin: { readOnly: true } },
|
|
7182
7740
|
{ name: "autoCloseRemindedAt", type: "date", label: "Rappel auto-close envoye", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7183
|
-
{ name: "autoCloseScheduledAt", type: "date", label: "Fermeture auto programmee", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" }, description: "Echeance ferme posee par une relance manuelle. Le ticket se ferme apres cette date sans reponse client." } }
|
|
7741
|
+
{ name: "autoCloseScheduledAt", type: "date", index: true, label: "Fermeture auto programmee", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" }, description: "Echeance ferme posee par une relance manuelle. Le ticket se ferme apres cette date sans reponse client." } }
|
|
7184
7742
|
]
|
|
7185
7743
|
},
|
|
7186
7744
|
// Sidebar
|
|
@@ -7189,6 +7747,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7189
7747
|
{
|
|
7190
7748
|
name: "category",
|
|
7191
7749
|
type: "select",
|
|
7750
|
+
index: true,
|
|
7192
7751
|
label: "Categorie",
|
|
7193
7752
|
options: [
|
|
7194
7753
|
{ label: "Bug / Dysfonctionnement", value: "bug" },
|
|
@@ -7199,7 +7758,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7199
7758
|
],
|
|
7200
7759
|
admin: { position: "sidebar" }
|
|
7201
7760
|
},
|
|
7202
|
-
{ name: "assignedTo", type: "relationship", relationTo: slugs.users, label: "Assigne a", admin: { position: "sidebar" } },
|
|
7761
|
+
{ name: "assignedTo", type: "relationship", relationTo: slugs.users, index: true, label: "Assigne a", admin: { position: "sidebar" } },
|
|
7762
|
+
{ name: "team", type: "relationship", relationTo: slugs.supportTeams, index: true, label: "Equipe", admin: { position: "sidebar" } },
|
|
7203
7763
|
{
|
|
7204
7764
|
name: "source",
|
|
7205
7765
|
type: "select",
|
|
@@ -7209,7 +7769,9 @@ function createTicketsCollection(slugs, options) {
|
|
|
7209
7769
|
{ label: "Portail client", value: "portal" },
|
|
7210
7770
|
{ label: "Chat en direct", value: "live-chat" },
|
|
7211
7771
|
{ label: "Email", value: "email" },
|
|
7212
|
-
{ label: "Admin", value: "admin" }
|
|
7772
|
+
{ label: "Admin", value: "admin" },
|
|
7773
|
+
{ label: "WhatsApp", value: "whatsapp" },
|
|
7774
|
+
{ label: "Messenger", value: "messenger" }
|
|
7213
7775
|
],
|
|
7214
7776
|
admin: { position: "sidebar" }
|
|
7215
7777
|
},
|
|
@@ -7260,8 +7822,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7260
7822
|
afterChange: [
|
|
7261
7823
|
createTrackSLA(slugs),
|
|
7262
7824
|
createTrackAiSummaryOnResolve(slugs),
|
|
7263
|
-
createAutoCalculateSLA(slugs),
|
|
7264
7825
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
7826
|
+
createPauseSlaOnHold(slugs),
|
|
7265
7827
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
7266
7828
|
createLogTicketActivity(slugs),
|
|
7267
7829
|
createNotifyOnAssignment(slugs),
|
|
@@ -7269,7 +7831,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7269
7831
|
createTicketStatusEmail(slugs),
|
|
7270
7832
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
7271
7833
|
createFireTicketWebhooks(slugs),
|
|
7272
|
-
createDispatchWebhookOnTicket(slugs)
|
|
7834
|
+
createDispatchWebhookOnTicket(slugs),
|
|
7835
|
+
createApplyAutomationRules(slugs)
|
|
7273
7836
|
],
|
|
7274
7837
|
beforeDelete: [createCascadeDelete(slugs)]
|
|
7275
7838
|
},
|
|
@@ -7280,12 +7843,20 @@ function createTicketsCollection(slugs, options) {
|
|
|
7280
7843
|
return false;
|
|
7281
7844
|
},
|
|
7282
7845
|
read: async ({ req }) => {
|
|
7283
|
-
if (req.user?.collection === slugs.users)
|
|
7846
|
+
if (req.user?.collection === slugs.users) {
|
|
7847
|
+
if (!isTeamScopingEnabled()) return true;
|
|
7848
|
+
const teamIds = await resolveAgentTeamIds(req.payload, slugs, req.user.id);
|
|
7849
|
+
return {
|
|
7850
|
+
or: [
|
|
7851
|
+
{ team: { in: teamIds.length > 0 ? teamIds : [-1] } },
|
|
7852
|
+
{ team: { exists: false } }
|
|
7853
|
+
]
|
|
7854
|
+
};
|
|
7855
|
+
}
|
|
7284
7856
|
if (req.user?.collection === slugs.supportClients) {
|
|
7285
7857
|
let collabTicketIds = [];
|
|
7286
7858
|
try {
|
|
7287
|
-
const rows = await req.payload
|
|
7288
|
-
collection: "ticket-collaborators",
|
|
7859
|
+
const rows = await dbFind(req.payload, "ticket-collaborators", {
|
|
7289
7860
|
where: { client: { equals: req.user.id } },
|
|
7290
7861
|
limit: 1e3,
|
|
7291
7862
|
depth: 0,
|
|
@@ -7320,6 +7891,125 @@ function createTicketsCollection(slugs, options) {
|
|
|
7320
7891
|
};
|
|
7321
7892
|
}
|
|
7322
7893
|
|
|
7894
|
+
// src/utils/ticketAccess.ts
|
|
7895
|
+
async function resolveAccessibleTicketIds(payload, slugs, clientId) {
|
|
7896
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7897
|
+
try {
|
|
7898
|
+
const owned = await dbFind(payload, slugs.tickets, {
|
|
7899
|
+
where: { client: { equals: clientId } },
|
|
7900
|
+
limit: 5e3,
|
|
7901
|
+
depth: 0,
|
|
7902
|
+
overrideAccess: true
|
|
7903
|
+
});
|
|
7904
|
+
for (const t of owned.docs) ids.add(t.id);
|
|
7905
|
+
} catch {
|
|
7906
|
+
}
|
|
7907
|
+
try {
|
|
7908
|
+
const collab = await dbFind(payload, "ticket-collaborators", {
|
|
7909
|
+
where: { client: { equals: clientId } },
|
|
7910
|
+
limit: 1e3,
|
|
7911
|
+
depth: 0,
|
|
7912
|
+
overrideAccess: true
|
|
7913
|
+
});
|
|
7914
|
+
for (const r of collab.docs) {
|
|
7915
|
+
const row = r;
|
|
7916
|
+
const tid = typeof row.ticket === "object" ? row.ticket?.id : row.ticket;
|
|
7917
|
+
if (tid !== void 0 && tid !== null) ids.add(tid);
|
|
7918
|
+
}
|
|
7919
|
+
} catch {
|
|
7920
|
+
}
|
|
7921
|
+
return Array.from(ids);
|
|
7922
|
+
}
|
|
7923
|
+
var OPTIONS = {
|
|
7924
|
+
allowedTags: [
|
|
7925
|
+
"p",
|
|
7926
|
+
"br",
|
|
7927
|
+
"b",
|
|
7928
|
+
"strong",
|
|
7929
|
+
"i",
|
|
7930
|
+
"em",
|
|
7931
|
+
"u",
|
|
7932
|
+
"s",
|
|
7933
|
+
"strike",
|
|
7934
|
+
"del",
|
|
7935
|
+
"ins",
|
|
7936
|
+
"mark",
|
|
7937
|
+
"a",
|
|
7938
|
+
"ul",
|
|
7939
|
+
"ol",
|
|
7940
|
+
"li",
|
|
7941
|
+
"blockquote",
|
|
7942
|
+
"code",
|
|
7943
|
+
"pre",
|
|
7944
|
+
"kbd",
|
|
7945
|
+
"span",
|
|
7946
|
+
"div",
|
|
7947
|
+
"h1",
|
|
7948
|
+
"h2",
|
|
7949
|
+
"h3",
|
|
7950
|
+
"h4",
|
|
7951
|
+
"h5",
|
|
7952
|
+
"h6",
|
|
7953
|
+
"hr",
|
|
7954
|
+
"img",
|
|
7955
|
+
"table",
|
|
7956
|
+
"thead",
|
|
7957
|
+
"tbody",
|
|
7958
|
+
"tr",
|
|
7959
|
+
"th",
|
|
7960
|
+
"td"
|
|
7961
|
+
],
|
|
7962
|
+
allowedAttributes: {
|
|
7963
|
+
a: ["href", "title", "target", "rel"],
|
|
7964
|
+
img: ["src", "alt", "title", "width", "height"],
|
|
7965
|
+
code: ["class"],
|
|
7966
|
+
pre: ["class"],
|
|
7967
|
+
span: ["class"],
|
|
7968
|
+
"*": ["style"]
|
|
7969
|
+
},
|
|
7970
|
+
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
7971
|
+
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
|
7972
|
+
allowProtocolRelative: false,
|
|
7973
|
+
disallowedTagsMode: "discard",
|
|
7974
|
+
// Force safe rel on links opening a new tab (defense against tabnabbing).
|
|
7975
|
+
transformTags: {
|
|
7976
|
+
a: sanitizeHtml__default.default.simpleTransform("a", { rel: "noopener noreferrer nofollow" }, true)
|
|
7977
|
+
},
|
|
7978
|
+
// Only allow CSS that cannot carry a script payload.
|
|
7979
|
+
allowedStyles: {
|
|
7980
|
+
"*": {
|
|
7981
|
+
color: [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}\s*\)$/i, /^[a-z-]+$/i],
|
|
7982
|
+
"background-color": [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}\s*\)$/i, /^[a-z-]+$/i],
|
|
7983
|
+
"text-align": [/^(left|right|center|justify)$/],
|
|
7984
|
+
"font-weight": [/^(normal|bold|[1-9]00)$/],
|
|
7985
|
+
"font-style": [/^(normal|italic)$/],
|
|
7986
|
+
"text-decoration": [/^(underline|line-through|none)$/]
|
|
7987
|
+
}
|
|
7988
|
+
}
|
|
7989
|
+
};
|
|
7990
|
+
function sanitizeMessageHtml(html) {
|
|
7991
|
+
if (!html) return html;
|
|
7992
|
+
return sanitizeHtml__default.default(html, OPTIONS);
|
|
7993
|
+
}
|
|
7994
|
+
|
|
7995
|
+
// src/utils/notificationQueue.ts
|
|
7996
|
+
async function queueClientNotification(payload, slugs, item) {
|
|
7997
|
+
try {
|
|
7998
|
+
await dbCreate(payload, slugs.notificationQueue, {
|
|
7999
|
+
data: {
|
|
8000
|
+
client: item.client,
|
|
8001
|
+
type: item.type,
|
|
8002
|
+
title: item.title,
|
|
8003
|
+
message: item.message,
|
|
8004
|
+
...item.ticket ? { ticket: item.ticket } : {}
|
|
8005
|
+
},
|
|
8006
|
+
overrideAccess: true
|
|
8007
|
+
});
|
|
8008
|
+
} catch (err) {
|
|
8009
|
+
console.error("[support] Failed to queue client notification:", err);
|
|
8010
|
+
}
|
|
8011
|
+
}
|
|
8012
|
+
|
|
7323
8013
|
// src/collections/TicketMessages.ts
|
|
7324
8014
|
function createAssignAuthor(slugs) {
|
|
7325
8015
|
return async ({ data, operation, req }) => {
|
|
@@ -7382,29 +8072,46 @@ function createNotifyClient(slugs) {
|
|
|
7382
8072
|
const internalBcc = process.env.SUPPORT_CLIENT_BCC || void 0;
|
|
7383
8073
|
const rawContent = doc.bodyHtml ? emailRichContent(doc.bodyHtml) : emailQuote(doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body);
|
|
7384
8074
|
const replyPreview = (doc.body || "").replace(/\s+/g, " ").trim().slice(0, 90);
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
8075
|
+
const freq = client.notificationFrequency;
|
|
8076
|
+
if (freq === "daily" || freq === "weekly") {
|
|
8077
|
+
await queueClientNotification(req.payload, slugs, {
|
|
8078
|
+
client: client.id,
|
|
8079
|
+
type: "reply",
|
|
8080
|
+
title: `Nouvelle reponse \u2014 ${ticketNumber}`,
|
|
8081
|
+
message: replyPreview || `Reponse a votre ticket ${ticketNumber}`,
|
|
8082
|
+
ticket: ticketId
|
|
8083
|
+
});
|
|
8084
|
+
return doc;
|
|
8085
|
+
}
|
|
8086
|
+
void (async () => {
|
|
8087
|
+
try {
|
|
8088
|
+
await req.payload.sendEmail({
|
|
8089
|
+
to: client.email,
|
|
8090
|
+
...supportEmail ? { replyTo: supportEmail } : {},
|
|
8091
|
+
...internalBcc ? { bcc: internalBcc } : {},
|
|
8092
|
+
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
8093
|
+
html: emailWrapper(`Nouvelle reponse \u2014 ${ticketNumber}`, [
|
|
8094
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
|
|
8095
|
+
emailParagraph(`Notre equipe a apporte une reponse a votre ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(subject)}</em>.`),
|
|
8096
|
+
rawContent,
|
|
8097
|
+
emailButton("Consulter le ticket", portalUrl),
|
|
8098
|
+
emailParagraph('<span style="font-size: 13px; color: #6b7280;">Vous pouvez egalement repondre directement a cet email. Votre message sera automatiquement ajoute au ticket.</span>'),
|
|
8099
|
+
emailTrackingPixel(ticketId, doc.id)
|
|
8100
|
+
].join(""), {
|
|
8101
|
+
kind: "reply",
|
|
8102
|
+
preheader: replyPreview ? `${replyPreview}\u2026` : `Nouvelle reponse a votre ticket ${ticketNumber}`
|
|
8103
|
+
})
|
|
8104
|
+
});
|
|
8105
|
+
await req.payload.update({
|
|
8106
|
+
collection: slugs.ticketMessages,
|
|
8107
|
+
id: doc.id,
|
|
8108
|
+
data: { emailSentAt: (/* @__PURE__ */ new Date()).toISOString(), emailSentTo: client.email },
|
|
8109
|
+
overrideAccess: true
|
|
8110
|
+
});
|
|
8111
|
+
} catch (err) {
|
|
8112
|
+
console.error("[support] Failed to notify client (async):", err);
|
|
8113
|
+
}
|
|
8114
|
+
})();
|
|
7408
8115
|
} catch (err) {
|
|
7409
8116
|
console.error("[support] Failed to notify client:", err);
|
|
7410
8117
|
}
|
|
@@ -7442,8 +8149,7 @@ function createSyncTicketReplyToChat(slugs) {
|
|
|
7442
8149
|
if (!ticket?.chatSession) return doc;
|
|
7443
8150
|
if (doc.skipNotification) return doc;
|
|
7444
8151
|
const clientId = typeof ticket.client === "object" ? ticket.client.id : ticket.client;
|
|
7445
|
-
await payload.
|
|
7446
|
-
collection: slugs.chatMessages,
|
|
8152
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
7447
8153
|
data: {
|
|
7448
8154
|
session: ticket.chatSession,
|
|
7449
8155
|
client: clientId,
|
|
@@ -7560,6 +8266,57 @@ function createDispatchWebhookOnReply(slugs) {
|
|
|
7560
8266
|
return doc;
|
|
7561
8267
|
};
|
|
7562
8268
|
}
|
|
8269
|
+
function createSanitizeMessageHtml() {
|
|
8270
|
+
return ({ data }) => {
|
|
8271
|
+
if (data && typeof data.bodyHtml === "string" && data.bodyHtml) {
|
|
8272
|
+
data.bodyHtml = sanitizeMessageHtml(data.bodyHtml);
|
|
8273
|
+
}
|
|
8274
|
+
return data;
|
|
8275
|
+
};
|
|
8276
|
+
}
|
|
8277
|
+
var MENTION_RE = /@([^\s@]+@[^\s@]+\.[^\s@]+)/g;
|
|
8278
|
+
function createResolveMentions(slugs) {
|
|
8279
|
+
return async ({ data, operation, req }) => {
|
|
8280
|
+
if (operation !== "create") return data;
|
|
8281
|
+
const body = typeof data?.body === "string" ? data.body : "";
|
|
8282
|
+
const emails = Array.from(new Set([...body.matchAll(MENTION_RE)].map((m) => m[1].toLowerCase())));
|
|
8283
|
+
if (emails.length === 0) return data;
|
|
8284
|
+
try {
|
|
8285
|
+
const users = await dbFind(req.payload, slugs.users, { where: { email: { in: emails } }, limit: 50, depth: 0, overrideAccess: true });
|
|
8286
|
+
const ids = users.docs.map((u) => u.id);
|
|
8287
|
+
if (ids.length > 0) data.mentions = ids;
|
|
8288
|
+
} catch {
|
|
8289
|
+
}
|
|
8290
|
+
return data;
|
|
8291
|
+
};
|
|
8292
|
+
}
|
|
8293
|
+
function createNotifyMentions(slugs) {
|
|
8294
|
+
return async ({ doc, operation, req }) => {
|
|
8295
|
+
if (operation !== "create") return doc;
|
|
8296
|
+
const mentions = Array.isArray(doc.mentions) ? doc.mentions : [];
|
|
8297
|
+
if (mentions.length === 0) return doc;
|
|
8298
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8299
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
8300
|
+
for (const m of mentions) {
|
|
8301
|
+
const userId = typeof m === "object" ? m.id : m;
|
|
8302
|
+
try {
|
|
8303
|
+
const user = await dbFindByID(req.payload, slugs.users, { id: userId, depth: 0, overrideAccess: true });
|
|
8304
|
+
if (!user?.email) continue;
|
|
8305
|
+
await req.payload.sendEmail({
|
|
8306
|
+
to: user.email,
|
|
8307
|
+
subject: `Vous avez ete mentionne \u2014 ticket #${ticketId}`,
|
|
8308
|
+
html: emailWrapper("Mention", [
|
|
8309
|
+
emailParagraph(`Vous avez ete mentionne dans un message du ticket <strong>#${escapeHtml(String(ticketId))}</strong>.`),
|
|
8310
|
+
...baseUrl ? [emailButton("Ouvrir le ticket", `${baseUrl}/admin/collections/${slugs.tickets}/${ticketId}`, "dark")] : []
|
|
8311
|
+
].join(""), { kind: "system" })
|
|
8312
|
+
});
|
|
8313
|
+
} catch (err) {
|
|
8314
|
+
console.error("[support] Failed to notify mention:", err);
|
|
8315
|
+
}
|
|
8316
|
+
}
|
|
8317
|
+
return doc;
|
|
8318
|
+
};
|
|
8319
|
+
}
|
|
7563
8320
|
function createTicketMessagesCollection(slugs, options) {
|
|
7564
8321
|
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
7565
8322
|
return {
|
|
@@ -7567,7 +8324,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7567
8324
|
labels: { singular: "Message", plural: "Messages" },
|
|
7568
8325
|
admin: { hidden: true, group: "Support", defaultColumns: ["ticket", "authorType", "createdAt"] },
|
|
7569
8326
|
fields: [
|
|
7570
|
-
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, required: true, label: "Ticket" },
|
|
8327
|
+
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, required: true, index: true, label: "Ticket" },
|
|
7571
8328
|
{ name: "body", type: "textarea", required: true, label: "Message" },
|
|
7572
8329
|
{ name: "bodyHtml", type: "textarea", label: "Message HTML", admin: { hidden: true } },
|
|
7573
8330
|
{
|
|
@@ -7576,6 +8333,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7576
8333
|
{
|
|
7577
8334
|
name: "authorType",
|
|
7578
8335
|
type: "select",
|
|
8336
|
+
index: true,
|
|
7579
8337
|
label: "Type d'auteur",
|
|
7580
8338
|
defaultValue: "admin",
|
|
7581
8339
|
options: [
|
|
@@ -7611,8 +8369,9 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7611
8369
|
},
|
|
7612
8370
|
{ name: "isInternal", type: "checkbox", defaultValue: false, label: "Note interne", admin: { position: "sidebar" } },
|
|
7613
8371
|
{ name: "isSolution", type: "checkbox", defaultValue: false, label: "Reponse solution", admin: { position: "sidebar" } },
|
|
8372
|
+
{ name: "mentions", type: "relationship", relationTo: slugs.users, hasMany: true, label: "Agents mentionnes", admin: { hidden: true } },
|
|
7614
8373
|
{ name: "skipNotification", type: "checkbox", defaultValue: false, label: "Sans notification", admin: { position: "sidebar", condition: (data) => data?.skipNotification === true } },
|
|
7615
|
-
{ name: "scheduledAt", type: "date", label: "Programme pour", admin: { date: { pickerAppearance: "dayAndTime" }, position: "sidebar", condition: (data) => !!data?.scheduledAt } },
|
|
8374
|
+
{ name: "scheduledAt", type: "date", index: true, label: "Programme pour", admin: { date: { pickerAppearance: "dayAndTime" }, position: "sidebar", condition: (data) => !!data?.scheduledAt } },
|
|
7616
8375
|
{ name: "scheduledSent", type: "checkbox", defaultValue: false, admin: { hidden: true } },
|
|
7617
8376
|
{ name: "editedAt", type: "date", label: "Modifie le", admin: { hidden: true } },
|
|
7618
8377
|
{ name: "deletedAt", type: "date", label: "Supprime le", admin: { hidden: true } },
|
|
@@ -7621,7 +8380,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7621
8380
|
{ name: "emailOpenedAt", type: "date", label: "Email ouvert le", admin: { hidden: true } }
|
|
7622
8381
|
],
|
|
7623
8382
|
hooks: {
|
|
7624
|
-
beforeChange: [createAssignAuthor(slugs)],
|
|
8383
|
+
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
7625
8384
|
afterChange: [
|
|
7626
8385
|
createAutoUpdateStatus(slugs),
|
|
7627
8386
|
createNotifyClient(slugs),
|
|
@@ -7630,17 +8389,19 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7630
8389
|
createSyncTicketReplyToChat(slugs),
|
|
7631
8390
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
7632
8391
|
createFireMessageWebhooks(slugs),
|
|
7633
|
-
createDispatchWebhookOnReply(slugs)
|
|
8392
|
+
createDispatchWebhookOnReply(slugs),
|
|
8393
|
+
createNotifyMentions(slugs)
|
|
7634
8394
|
]
|
|
7635
8395
|
},
|
|
7636
8396
|
access: {
|
|
7637
8397
|
create: ({ req }) => req.user?.collection === slugs.users || req.user?.collection === slugs.supportClients,
|
|
7638
|
-
read: ({ req }) => {
|
|
8398
|
+
read: async ({ req }) => {
|
|
7639
8399
|
if (req.user?.collection === slugs.users) return true;
|
|
7640
8400
|
if (req.user?.collection === slugs.supportClients) {
|
|
8401
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
7641
8402
|
return {
|
|
7642
8403
|
and: [
|
|
7643
|
-
{
|
|
8404
|
+
{ ticket: { in: ids.length > 0 ? ids : [-1] } },
|
|
7644
8405
|
{ isInternal: { equals: false } },
|
|
7645
8406
|
{ or: [{ scheduledAt: { exists: false } }, { scheduledSent: { equals: true } }] }
|
|
7646
8407
|
]
|
|
@@ -7660,8 +8421,6 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7660
8421
|
timestamps: true
|
|
7661
8422
|
};
|
|
7662
8423
|
}
|
|
7663
|
-
|
|
7664
|
-
// src/collections/SupportClients.ts
|
|
7665
8424
|
function createSendInvitationOnCreate(slugs) {
|
|
7666
8425
|
return async ({ doc, operation, req }) => {
|
|
7667
8426
|
if (operation !== "create") return doc;
|
|
@@ -7701,6 +8460,26 @@ function createSendInvitationOnCreate(slugs) {
|
|
|
7701
8460
|
return doc;
|
|
7702
8461
|
};
|
|
7703
8462
|
}
|
|
8463
|
+
var TWO_FA_WINDOW_MS = 5 * 60 * 1e3;
|
|
8464
|
+
function createEnforce2FA(slugs) {
|
|
8465
|
+
return async ({ req, user }) => {
|
|
8466
|
+
if (!user?.twoFactorEnabled) return user;
|
|
8467
|
+
const raw = user.twoFactorVerifiedAt;
|
|
8468
|
+
const verifiedAt = raw ? new Date(raw).getTime() : 0;
|
|
8469
|
+
if (!(verifiedAt > Date.now() - TWO_FA_WINDOW_MS)) {
|
|
8470
|
+
throw new payload.APIError("2FA_REQUIRED", 401);
|
|
8471
|
+
}
|
|
8472
|
+
await req.payload.update({
|
|
8473
|
+
collection: slugs.supportClients,
|
|
8474
|
+
id: user.id,
|
|
8475
|
+
data: { twoFactorVerifiedAt: null },
|
|
8476
|
+
req,
|
|
8477
|
+
overrideAccess: true
|
|
8478
|
+
});
|
|
8479
|
+
user.twoFactorVerifiedAt = null;
|
|
8480
|
+
return user;
|
|
8481
|
+
};
|
|
8482
|
+
}
|
|
7704
8483
|
function createSupportClientsCollection(slugs) {
|
|
7705
8484
|
return {
|
|
7706
8485
|
slug: slugs.supportClients,
|
|
@@ -7829,6 +8608,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7829
8608
|
name: "opportunities",
|
|
7830
8609
|
type: "textarea",
|
|
7831
8610
|
label: "Opportunites commerciales",
|
|
8611
|
+
access: {
|
|
8612
|
+
// Admin-only: never serialize commercial notes onto the client's own doc.
|
|
8613
|
+
read: ({ req }) => req.user?.collection === slugs.users
|
|
8614
|
+
},
|
|
7832
8615
|
admin: {
|
|
7833
8616
|
description: "Notes commerciales (visible uniquement par les admins)"
|
|
7834
8617
|
}
|
|
@@ -7872,6 +8655,19 @@ function createSupportClientsCollection(slugs) {
|
|
|
7872
8655
|
type: "date",
|
|
7873
8656
|
admin: { hidden: true }
|
|
7874
8657
|
},
|
|
8658
|
+
{
|
|
8659
|
+
// Set by /support/2fa "verify"; gates login via the beforeLogin hook.
|
|
8660
|
+
name: "twoFactorVerifiedAt",
|
|
8661
|
+
type: "date",
|
|
8662
|
+
admin: { hidden: true }
|
|
8663
|
+
},
|
|
8664
|
+
{
|
|
8665
|
+
// Google OAuth subject id — accounts are linked by this, not email alone.
|
|
8666
|
+
name: "googleId",
|
|
8667
|
+
type: "text",
|
|
8668
|
+
index: true,
|
|
8669
|
+
admin: { hidden: true }
|
|
8670
|
+
},
|
|
7875
8671
|
{
|
|
7876
8672
|
name: "notifyOnReply",
|
|
7877
8673
|
type: "checkbox",
|
|
@@ -7938,6 +8734,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7938
8734
|
name: "notes",
|
|
7939
8735
|
type: "textarea",
|
|
7940
8736
|
label: "Notes internes",
|
|
8737
|
+
access: {
|
|
8738
|
+
// Admin-only: never serialize internal notes onto the client's own doc.
|
|
8739
|
+
read: ({ req }) => req.user?.collection === slugs.users
|
|
8740
|
+
},
|
|
7941
8741
|
admin: {
|
|
7942
8742
|
description: "Visible uniquement par les admins",
|
|
7943
8743
|
position: "sidebar"
|
|
@@ -7945,6 +8745,7 @@ function createSupportClientsCollection(slugs) {
|
|
|
7945
8745
|
}
|
|
7946
8746
|
],
|
|
7947
8747
|
hooks: {
|
|
8748
|
+
beforeLogin: [createEnforce2FA(slugs)],
|
|
7948
8749
|
afterChange: [createSendInvitationOnCreate(slugs)]
|
|
7949
8750
|
},
|
|
7950
8751
|
access: {
|
|
@@ -8158,6 +8959,7 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
8158
8959
|
type: "relationship",
|
|
8159
8960
|
relationTo: slugs.tickets,
|
|
8160
8961
|
required: true,
|
|
8962
|
+
index: true,
|
|
8161
8963
|
label: "Ticket"
|
|
8162
8964
|
},
|
|
8163
8965
|
{
|
|
@@ -8203,10 +9005,11 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
8203
9005
|
access: {
|
|
8204
9006
|
create: () => false,
|
|
8205
9007
|
// Created only by hooks via overrideAccess
|
|
8206
|
-
read: ({ req }) => {
|
|
9008
|
+
read: async ({ req }) => {
|
|
8207
9009
|
if (req.user?.collection === slugs.users) return true;
|
|
8208
9010
|
if (req.user?.collection === slugs.supportClients) {
|
|
8209
|
-
|
|
9011
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
9012
|
+
return { ticket: { in: ids.length > 0 ? ids : [-1] } };
|
|
8210
9013
|
}
|
|
8211
9014
|
return false;
|
|
8212
9015
|
},
|
|
@@ -8275,12 +9078,21 @@ function createSatisfactionSurveysCollection(slugs) {
|
|
|
8275
9078
|
{
|
|
8276
9079
|
name: "rating",
|
|
8277
9080
|
type: "number",
|
|
8278
|
-
required: true,
|
|
8279
9081
|
min: 1,
|
|
8280
9082
|
max: 5,
|
|
8281
9083
|
label: "Note (1-5)",
|
|
8282
9084
|
admin: {
|
|
8283
|
-
description: "1 = Tr\xE8s insatisfait, 5 = Tr\xE8s satisfait"
|
|
9085
|
+
description: "1 = Tr\xE8s insatisfait, 5 = Tr\xE8s satisfait (CSAT)"
|
|
9086
|
+
}
|
|
9087
|
+
},
|
|
9088
|
+
{
|
|
9089
|
+
name: "nps",
|
|
9090
|
+
type: "number",
|
|
9091
|
+
min: 0,
|
|
9092
|
+
max: 10,
|
|
9093
|
+
label: "NPS (0-10)",
|
|
9094
|
+
admin: {
|
|
9095
|
+
description: "\xAB Recommanderiez-vous ? \xBB \u2014 promoteur 9-10, passif 7-8, d\xE9tracteur 0-6"
|
|
8284
9096
|
}
|
|
8285
9097
|
},
|
|
8286
9098
|
{
|
|
@@ -9526,6 +10338,124 @@ function createTicketCollaboratorsCollection(slugs) {
|
|
|
9526
10338
|
};
|
|
9527
10339
|
}
|
|
9528
10340
|
|
|
10341
|
+
// src/collections/NotificationQueue.ts
|
|
10342
|
+
function createNotificationQueueCollection(slugs) {
|
|
10343
|
+
return {
|
|
10344
|
+
slug: slugs.notificationQueue,
|
|
10345
|
+
labels: { singular: "Notification en file", plural: "Notifications en file" },
|
|
10346
|
+
admin: { hidden: true, group: "Support", defaultColumns: ["client", "type", "title", "createdAt"] },
|
|
10347
|
+
access: {
|
|
10348
|
+
// Internal: written/drained only via overrideAccess (hooks + cron).
|
|
10349
|
+
create: () => false,
|
|
10350
|
+
update: () => false,
|
|
10351
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10352
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10353
|
+
},
|
|
10354
|
+
fields: [
|
|
10355
|
+
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, index: true, label: "Client" },
|
|
10356
|
+
{ name: "type", type: "text", label: "Type" },
|
|
10357
|
+
{ name: "title", type: "text", label: "Titre" },
|
|
10358
|
+
{ name: "message", type: "textarea", label: "Message" },
|
|
10359
|
+
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, label: "Ticket" }
|
|
10360
|
+
],
|
|
10361
|
+
timestamps: true
|
|
10362
|
+
};
|
|
10363
|
+
}
|
|
10364
|
+
|
|
10365
|
+
// src/collections/AutomationRules.ts
|
|
10366
|
+
var CONDITION_FIELDS = ["status", "priority", "category", "source", "subject"];
|
|
10367
|
+
var ACTION_TYPES = [
|
|
10368
|
+
{ label: "D\xE9finir le statut", value: "set_status" },
|
|
10369
|
+
{ label: "D\xE9finir la priorit\xE9", value: "set_priority" },
|
|
10370
|
+
{ label: "D\xE9finir la cat\xE9gorie", value: "set_category" },
|
|
10371
|
+
{ label: "Assigner \xE0 (id agent)", value: "assign" },
|
|
10372
|
+
{ label: "Ajouter un tag", value: "add_tag" }
|
|
10373
|
+
];
|
|
10374
|
+
function createAutomationRulesCollection(slugs) {
|
|
10375
|
+
return {
|
|
10376
|
+
slug: slugs.automationRules,
|
|
10377
|
+
labels: { singular: "R\xE8gle d'automatisation", plural: "R\xE8gles d'automatisation" },
|
|
10378
|
+
admin: { useAsTitle: "name", group: "Support", defaultColumns: ["name", "event", "enabled", "order"] },
|
|
10379
|
+
access: {
|
|
10380
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10381
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
10382
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
10383
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10384
|
+
},
|
|
10385
|
+
fields: [
|
|
10386
|
+
{ name: "name", type: "text", required: true, label: "Nom" },
|
|
10387
|
+
{ name: "enabled", type: "checkbox", defaultValue: true, label: "Active", index: true },
|
|
10388
|
+
{
|
|
10389
|
+
name: "event",
|
|
10390
|
+
type: "select",
|
|
10391
|
+
defaultValue: "ticket_created",
|
|
10392
|
+
index: true,
|
|
10393
|
+
label: "D\xE9clencheur",
|
|
10394
|
+
options: [
|
|
10395
|
+
{ label: "Ticket cr\xE9\xE9", value: "ticket_created" },
|
|
10396
|
+
{ label: "Ticket mis \xE0 jour", value: "ticket_updated" },
|
|
10397
|
+
{ label: "Changement de statut", value: "ticket_status_changed" }
|
|
10398
|
+
]
|
|
10399
|
+
},
|
|
10400
|
+
{
|
|
10401
|
+
name: "matchType",
|
|
10402
|
+
type: "select",
|
|
10403
|
+
defaultValue: "all",
|
|
10404
|
+
label: "Correspondance",
|
|
10405
|
+
options: [{ label: "Toutes les conditions (ET)", value: "all" }, { label: "Au moins une (OU)", value: "any" }]
|
|
10406
|
+
},
|
|
10407
|
+
{ name: "order", type: "number", defaultValue: 0, label: "Ordre d'\xE9valuation" },
|
|
10408
|
+
{
|
|
10409
|
+
name: "conditions",
|
|
10410
|
+
type: "array",
|
|
10411
|
+
label: "Conditions",
|
|
10412
|
+
fields: [
|
|
10413
|
+
{ name: "field", type: "select", required: true, options: CONDITION_FIELDS.map((v) => ({ label: v, value: v })) },
|
|
10414
|
+
{
|
|
10415
|
+
name: "operator",
|
|
10416
|
+
type: "select",
|
|
10417
|
+
defaultValue: "equals",
|
|
10418
|
+
required: true,
|
|
10419
|
+
options: [{ label: "= \xE9gal", value: "equals" }, { label: "\u2260 diff\xE9rent", value: "not_equals" }, { label: "contient", value: "contains" }]
|
|
10420
|
+
},
|
|
10421
|
+
{ name: "value", type: "text", required: true }
|
|
10422
|
+
]
|
|
10423
|
+
},
|
|
10424
|
+
{
|
|
10425
|
+
name: "actions",
|
|
10426
|
+
type: "array",
|
|
10427
|
+
label: "Actions",
|
|
10428
|
+
fields: [
|
|
10429
|
+
{ name: "type", type: "select", required: true, options: ACTION_TYPES },
|
|
10430
|
+
{ name: "value", type: "text", required: true }
|
|
10431
|
+
]
|
|
10432
|
+
}
|
|
10433
|
+
],
|
|
10434
|
+
timestamps: true
|
|
10435
|
+
};
|
|
10436
|
+
}
|
|
10437
|
+
|
|
10438
|
+
// src/collections/SupportTeam.ts
|
|
10439
|
+
function createSupportTeamCollection(slugs) {
|
|
10440
|
+
return {
|
|
10441
|
+
slug: slugs.supportTeams,
|
|
10442
|
+
labels: { singular: "\xC9quipe support", plural: "\xC9quipes support" },
|
|
10443
|
+
admin: { useAsTitle: "name", group: "Support", defaultColumns: ["name", "members"] },
|
|
10444
|
+
access: {
|
|
10445
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10446
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
10447
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
10448
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10449
|
+
},
|
|
10450
|
+
fields: [
|
|
10451
|
+
{ name: "name", type: "text", required: true, label: "Nom" },
|
|
10452
|
+
{ name: "description", type: "textarea", label: "Description" },
|
|
10453
|
+
{ name: "members", type: "relationship", relationTo: slugs.users, hasMany: true, index: true, label: "Membres (agents)" }
|
|
10454
|
+
],
|
|
10455
|
+
timestamps: true
|
|
10456
|
+
};
|
|
10457
|
+
}
|
|
10458
|
+
|
|
9529
10459
|
// src/plugin.ts
|
|
9530
10460
|
function viewConfig(component, path) {
|
|
9531
10461
|
return { Component: component, path };
|
|
@@ -9561,7 +10491,10 @@ function supportPlugin(config) {
|
|
|
9561
10491
|
createSatisfactionSurveysCollection(slugs),
|
|
9562
10492
|
createKnowledgeBaseCollection(slugs),
|
|
9563
10493
|
createTicketFeedbackCollection(slugs, { notificationSlug: config?.notificationSlug }),
|
|
9564
|
-
createTicketCollaboratorsCollection(slugs)
|
|
10494
|
+
createTicketCollaboratorsCollection(slugs),
|
|
10495
|
+
createNotificationQueueCollection(slugs),
|
|
10496
|
+
createAutomationRulesCollection(slugs),
|
|
10497
|
+
createSupportTeamCollection(slugs)
|
|
9565
10498
|
];
|
|
9566
10499
|
if (features.authLogs !== false) supportCollections.push(createAuthLogsCollection(slugs));
|
|
9567
10500
|
if (features.timeTracking !== false) supportCollections.push(createTimeEntriesCollection(slugs));
|