@consilioweb/payload-support 0.15.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/components/ActionPanels.cjs +72 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +73 -2
- package/dist/components/TicketConversation/components/QuickActions.cjs +22 -1
- package/dist/components/TicketConversation/components/QuickActions.js +22 -1
- 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/hooks/useTicketActions.cjs +59 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.js +59 -0
- package/dist/components/TicketConversation/index.cjs +42 -149
- package/dist/components/TicketConversation/index.js +37 -144
- package/dist/index.cjs +1835 -741
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1833 -741
- 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 +137 -31
- package/src/collections/Tickets.ts +31 -72
- 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/components/ActionPanels.tsx +82 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +25 -0
- package/src/components/TicketConversation/constants.ts +29 -0
- package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +51 -0
- package/src/components/TicketConversation/index.tsx +29 -157
- 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 +38 -18
- 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 +24 -2
- 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 +186 -0
- 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,16 +2099,24 @@ 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" } },
|
|
1877
|
-
{ autoCloseRemindedAt: { less_than: closeCutoff.toISOString() } },
|
|
1878
2106
|
{
|
|
1879
2107
|
or: [
|
|
1880
|
-
{
|
|
1881
|
-
|
|
2108
|
+
{
|
|
2109
|
+
and: [
|
|
2110
|
+
{ autoCloseRemindedAt: { less_than: closeCutoff.toISOString() } },
|
|
2111
|
+
{
|
|
2112
|
+
or: [
|
|
2113
|
+
{ lastClientMessageAt: { exists: false } },
|
|
2114
|
+
{ lastClientMessageAt: { less_than_equal: closeCutoff.toISOString() } }
|
|
2115
|
+
]
|
|
2116
|
+
}
|
|
2117
|
+
]
|
|
2118
|
+
},
|
|
2119
|
+
{ autoCloseScheduledAt: { less_than_equal: now.toISOString() } }
|
|
1882
2120
|
]
|
|
1883
2121
|
}
|
|
1884
2122
|
]
|
|
@@ -1894,21 +2132,23 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1894
2132
|
const ticketNumber = t.ticketNumber || "TK-????";
|
|
1895
2133
|
const subject = t.subject || "Support";
|
|
1896
2134
|
const closeTotalDays = REMIND_AFTER_DAYS + CLOSE_AFTER_REMIND_DAYS;
|
|
1897
|
-
|
|
1898
|
-
|
|
2135
|
+
const viaSchedule = !!t.autoCloseScheduledAt && new Date(t.autoCloseScheduledAt).getTime() <= now.getTime();
|
|
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`;
|
|
2137
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
1899
2138
|
data: {
|
|
1900
2139
|
ticket: t.id,
|
|
1901
|
-
body:
|
|
2140
|
+
body: noteBody,
|
|
1902
2141
|
authorType: "admin",
|
|
1903
2142
|
isInternal: true,
|
|
1904
2143
|
skipNotification: true
|
|
1905
2144
|
},
|
|
1906
2145
|
overrideAccess: true
|
|
1907
2146
|
});
|
|
1908
|
-
await payload.
|
|
1909
|
-
collection: slugs.tickets,
|
|
2147
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1910
2148
|
id: t.id,
|
|
1911
|
-
|
|
2149
|
+
// Clear the armed deadline so a later manual reopen can't be
|
|
2150
|
+
// closed instantly by a stale timestamp.
|
|
2151
|
+
data: { status: "resolved", autoCloseScheduledAt: null },
|
|
1912
2152
|
overrideAccess: true
|
|
1913
2153
|
});
|
|
1914
2154
|
if (client?.email) {
|
|
@@ -1919,7 +2159,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1919
2159
|
subject: `[${ticketNumber}] Ticket r\xE9solu \u2014 ${subject}`,
|
|
1920
2160
|
html: `<div style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
1921
2161
|
<p>Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,</p>
|
|
1922
|
-
<p>Votre ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(subject)}</em> \u2014 a \xE9t\xE9 r\xE9solu automatiquement apr\xE8s ${closeTotalDays} jours sans r\xE9ponse.</p>
|
|
2162
|
+
<p>Votre ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(subject)}</em> \u2014 a \xE9t\xE9 r\xE9solu automatiquement ${viaSchedule ? "faute de r\xE9ponse de votre part" : `apr\xE8s ${closeTotalDays} jours sans r\xE9ponse`}.</p>
|
|
1923
2163
|
<p>Si vous avez encore besoin d'aide, n'h\xE9sitez pas \xE0 rouvrir ce ticket ou \xE0 en cr\xE9er un nouveau.</p>
|
|
1924
2164
|
<p><a href="${portalUrl}">Consulter le ticket</a></p>
|
|
1925
2165
|
</div>`
|
|
@@ -1944,27 +2184,206 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1944
2184
|
};
|
|
1945
2185
|
}
|
|
1946
2186
|
|
|
1947
|
-
// src/
|
|
1948
|
-
|
|
2187
|
+
// src/utils/rateLimiter.ts
|
|
2188
|
+
var RateLimiter = class {
|
|
2189
|
+
constructor(windowMs, maxRequests) {
|
|
2190
|
+
this.windowMs = windowMs;
|
|
2191
|
+
this.maxRequests = maxRequests;
|
|
2192
|
+
const timer = setInterval(() => this.cleanup(), windowMs);
|
|
2193
|
+
timer.unref();
|
|
2194
|
+
}
|
|
2195
|
+
windowMs;
|
|
2196
|
+
maxRequests;
|
|
2197
|
+
store = /* @__PURE__ */ new Map();
|
|
2198
|
+
/**
|
|
2199
|
+
* Check if a key has exceeded the rate limit.
|
|
2200
|
+
* Returns true if the request should be blocked.
|
|
2201
|
+
*/
|
|
2202
|
+
check(key) {
|
|
2203
|
+
const now = Date.now();
|
|
2204
|
+
const entry = this.store.get(key);
|
|
2205
|
+
if (!entry || now > entry.resetAt) {
|
|
2206
|
+
this.store.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
2207
|
+
return false;
|
|
2208
|
+
}
|
|
2209
|
+
entry.count++;
|
|
2210
|
+
return entry.count > this.maxRequests;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Reset the counter for a specific key.
|
|
2214
|
+
*/
|
|
2215
|
+
reset(key) {
|
|
2216
|
+
this.store.delete(key);
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* Remove expired entries from the store.
|
|
2220
|
+
*/
|
|
2221
|
+
cleanup() {
|
|
2222
|
+
const now = Date.now();
|
|
2223
|
+
for (const [key, entry] of this.store) {
|
|
2224
|
+
if (now > entry.resetAt) {
|
|
2225
|
+
this.store.delete(key);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
// src/endpoints/send-reminder.ts
|
|
2232
|
+
var reminderLimiter = new RateLimiter(60 * 60 * 1e3, 30);
|
|
2233
|
+
var MIN_HOURS = 1;
|
|
2234
|
+
var MAX_HOURS = 24 * 30;
|
|
2235
|
+
function formatFr(date, withTime) {
|
|
2236
|
+
return date.toLocaleString("fr-FR", {
|
|
2237
|
+
day: "numeric",
|
|
2238
|
+
month: "long",
|
|
2239
|
+
year: "numeric",
|
|
2240
|
+
...withTime ? { hour: "2-digit", minute: "2-digit" } : {},
|
|
2241
|
+
timeZone: "Europe/Paris"
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
function createSendReminderEndpoint(slugs) {
|
|
1949
2245
|
return {
|
|
1950
|
-
path: "/support/
|
|
1951
|
-
method: "
|
|
2246
|
+
path: "/support/send-reminder",
|
|
2247
|
+
method: "post",
|
|
1952
2248
|
handler: async (req) => {
|
|
1953
2249
|
try {
|
|
1954
2250
|
const payload = req.payload;
|
|
1955
|
-
|
|
1956
|
-
|
|
2251
|
+
requireAdmin(req, slugs);
|
|
2252
|
+
if (reminderLimiter.check(String(req.user.id))) {
|
|
2253
|
+
return Response.json(
|
|
2254
|
+
{ error: "Trop de relances. R\xE9essayez dans une heure." },
|
|
2255
|
+
{ status: 429 }
|
|
2256
|
+
);
|
|
1957
2257
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2258
|
+
let body;
|
|
2259
|
+
try {
|
|
2260
|
+
body = await req.json();
|
|
2261
|
+
} catch {
|
|
2262
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
2263
|
+
}
|
|
2264
|
+
const { ticketId } = body;
|
|
2265
|
+
if (!ticketId) {
|
|
2266
|
+
return Response.json({ error: "ticketId requis" }, { status: 400 });
|
|
2267
|
+
}
|
|
2268
|
+
const rawHours = Number(body.hours);
|
|
2269
|
+
const hours = Number.isFinite(rawHours) ? Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.round(rawHours))) : 24;
|
|
2270
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2271
|
+
id: ticketId,
|
|
2272
|
+
depth: 1,
|
|
1963
2273
|
overrideAccess: true
|
|
1964
2274
|
});
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2275
|
+
if (!ticket) {
|
|
2276
|
+
return Response.json({ error: "Ticket introuvable" }, { status: 404 });
|
|
2277
|
+
}
|
|
2278
|
+
const client = typeof ticket.client === "object" ? ticket.client : null;
|
|
2279
|
+
if (!client?.email) {
|
|
2280
|
+
return Response.json({ error: "Client sans email" }, { status: 400 });
|
|
2281
|
+
}
|
|
2282
|
+
const now = /* @__PURE__ */ new Date();
|
|
2283
|
+
const deadline = new Date(now.getTime() + hours * 60 * 60 * 1e3);
|
|
2284
|
+
const settings = await readSupportSettings(payload);
|
|
2285
|
+
const ticketNumber = ticket.ticketNumber || "TK-????";
|
|
2286
|
+
const subject = ticket.subject || "Support";
|
|
2287
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
2288
|
+
const portalUrl = `${baseUrl}/support/tickets/${ticketId}`;
|
|
2289
|
+
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_REPLY_TO || "";
|
|
2290
|
+
let waitingSince = ticket.firstResponseAt || ticket.createdAt;
|
|
2291
|
+
try {
|
|
2292
|
+
const lastAdminMsg = await dbFind(payload, slugs.ticketMessages, {
|
|
2293
|
+
where: {
|
|
2294
|
+
and: [
|
|
2295
|
+
{ ticket: { equals: ticketId } },
|
|
2296
|
+
{ authorType: { equals: "admin" } },
|
|
2297
|
+
{ isInternal: { equals: false } }
|
|
2298
|
+
]
|
|
2299
|
+
},
|
|
2300
|
+
sort: "-createdAt",
|
|
2301
|
+
limit: 1,
|
|
2302
|
+
depth: 0,
|
|
2303
|
+
overrideAccess: true
|
|
2304
|
+
});
|
|
2305
|
+
if (lastAdminMsg.docs.length > 0 && lastAdminMsg.docs[0].createdAt) {
|
|
2306
|
+
waitingSince = lastAdminMsg.docs[0].createdAt;
|
|
2307
|
+
}
|
|
2308
|
+
} catch {
|
|
2309
|
+
}
|
|
2310
|
+
const sinceLabel = waitingSince ? formatFr(new Date(waitingSince), false) : null;
|
|
2311
|
+
const deadlineLabel = formatFr(deadline, true);
|
|
2312
|
+
await payload.sendEmail({
|
|
2313
|
+
to: client.email,
|
|
2314
|
+
...replyTo ? { replyTo } : {},
|
|
2315
|
+
subject: `Rappel : [${ticketNumber}] ${subject} \u2014 votre r\xE9ponse est attendue`,
|
|
2316
|
+
html: emailWrapper(`Votre ticket attend votre r\xE9ponse`, [
|
|
2317
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
|
|
2318
|
+
emailParagraph(
|
|
2319
|
+
`Votre ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(String(subject))}</em> \u2014 est en attente de votre r\xE9ponse${sinceLabel ? ` depuis le ${escapeHtml(sinceLabel)}` : ""}.`
|
|
2320
|
+
),
|
|
2321
|
+
emailParagraph(
|
|
2322
|
+
`<strong>Sans retour de votre part, ce ticket sera automatiquement cl\xF4tur\xE9 le ${escapeHtml(deadlineLabel)}.</strong>`
|
|
2323
|
+
),
|
|
2324
|
+
emailParagraph(
|
|
2325
|
+
`Si vous avez encore besoin d'assistance, il vous suffit de r\xE9pondre \xE0 ce message \u2014 votre r\xE9ponse maintiendra le ticket ouvert.`
|
|
2326
|
+
),
|
|
2327
|
+
emailButton("R\xE9pondre au ticket", portalUrl, "primary")
|
|
2328
|
+
].join(""), {
|
|
2329
|
+
kind: "alert",
|
|
2330
|
+
preheader: `Sans r\xE9ponse de votre part, votre ticket ${ticketNumber} sera cl\xF4tur\xE9 le ${deadlineLabel}.`
|
|
2331
|
+
})
|
|
2332
|
+
});
|
|
2333
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2334
|
+
data: {
|
|
2335
|
+
ticket: ticketId,
|
|
2336
|
+
body: `Relance envoy\xE9e \xE0 ${client.email}. Fermeture automatique programm\xE9e le ${deadlineLabel} sans r\xE9ponse du client.`,
|
|
2337
|
+
authorType: "admin",
|
|
2338
|
+
isInternal: true,
|
|
2339
|
+
skipNotification: true
|
|
2340
|
+
},
|
|
2341
|
+
overrideAccess: true
|
|
2342
|
+
});
|
|
2343
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2344
|
+
id: ticketId,
|
|
2345
|
+
data: {
|
|
2346
|
+
status: "waiting_client",
|
|
2347
|
+
autoCloseRemindedAt: now.toISOString(),
|
|
2348
|
+
autoCloseScheduledAt: deadline.toISOString()
|
|
2349
|
+
},
|
|
2350
|
+
overrideAccess: true
|
|
2351
|
+
});
|
|
2352
|
+
return Response.json({
|
|
2353
|
+
success: true,
|
|
2354
|
+
sentTo: client.email,
|
|
2355
|
+
scheduledCloseAt: deadline.toISOString()
|
|
2356
|
+
});
|
|
2357
|
+
} catch (error) {
|
|
2358
|
+
const authResponse = handleAuthError(error);
|
|
2359
|
+
if (authResponse) return authResponse;
|
|
2360
|
+
console.error("[send-reminder] Error:", error);
|
|
2361
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// src/endpoints/statuses.ts
|
|
2368
|
+
function createStatusesEndpoint(slugs) {
|
|
2369
|
+
return {
|
|
2370
|
+
path: "/support/statuses",
|
|
2371
|
+
method: "get",
|
|
2372
|
+
handler: async (req) => {
|
|
2373
|
+
try {
|
|
2374
|
+
const payload = req.payload;
|
|
2375
|
+
if (!req.user) {
|
|
2376
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2377
|
+
}
|
|
2378
|
+
const { docs } = await dbFind(payload, slugs.ticketStatuses, {
|
|
2379
|
+
sort: "sortOrder",
|
|
2380
|
+
limit: 100,
|
|
2381
|
+
depth: 0,
|
|
2382
|
+
overrideAccess: true
|
|
2383
|
+
});
|
|
2384
|
+
return Response.json({
|
|
2385
|
+
statuses: docs.map((s) => ({
|
|
2386
|
+
id: s.id,
|
|
1968
2387
|
name: s.name,
|
|
1969
2388
|
slug: s.slug,
|
|
1970
2389
|
color: s.color,
|
|
@@ -1995,16 +2414,14 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
1995
2414
|
if (!macroId || !ticketId) {
|
|
1996
2415
|
return Response.json({ error: "macroId and ticketId are required" }, { status: 400 });
|
|
1997
2416
|
}
|
|
1998
|
-
const macro = await payload.
|
|
1999
|
-
collection: slugs.macros,
|
|
2417
|
+
const macro = await dbFindByID(payload, slugs.macros, {
|
|
2000
2418
|
id: macroId,
|
|
2001
2419
|
depth: 0,
|
|
2002
2420
|
overrideAccess: true
|
|
2003
2421
|
});
|
|
2004
2422
|
if (!macro) return Response.json({ error: "Macro not found" }, { status: 404 });
|
|
2005
2423
|
if (!macro.isActive) return Response.json({ error: "Macro is disabled" }, { status: 400 });
|
|
2006
|
-
const ticket = await payload.
|
|
2007
|
-
collection: slugs.tickets,
|
|
2424
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2008
2425
|
id: ticketId,
|
|
2009
2426
|
depth: 0,
|
|
2010
2427
|
overrideAccess: true
|
|
@@ -2016,8 +2433,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2016
2433
|
try {
|
|
2017
2434
|
switch (action.type) {
|
|
2018
2435
|
case "set_status":
|
|
2019
|
-
await payload.
|
|
2020
|
-
collection: slugs.tickets,
|
|
2436
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2021
2437
|
id: ticketId,
|
|
2022
2438
|
data: { status: action.value },
|
|
2023
2439
|
overrideAccess: true
|
|
@@ -2025,8 +2441,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2025
2441
|
appliedActions.push({ type: action.type, value: action.value, success: true });
|
|
2026
2442
|
break;
|
|
2027
2443
|
case "set_priority":
|
|
2028
|
-
await payload.
|
|
2029
|
-
collection: slugs.tickets,
|
|
2444
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2030
2445
|
id: ticketId,
|
|
2031
2446
|
data: { priority: action.value },
|
|
2032
2447
|
overrideAccess: true
|
|
@@ -2038,8 +2453,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2038
2453
|
if (!currentTags.includes(action.value)) {
|
|
2039
2454
|
currentTags.push(action.value);
|
|
2040
2455
|
}
|
|
2041
|
-
await payload.
|
|
2042
|
-
collection: slugs.tickets,
|
|
2456
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2043
2457
|
id: ticketId,
|
|
2044
2458
|
data: { tags: currentTags },
|
|
2045
2459
|
overrideAccess: true
|
|
@@ -2048,8 +2462,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2048
2462
|
break;
|
|
2049
2463
|
}
|
|
2050
2464
|
case "send_reply":
|
|
2051
|
-
await payload.
|
|
2052
|
-
collection: slugs.ticketMessages,
|
|
2465
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2053
2466
|
data: {
|
|
2054
2467
|
ticket: ticketId,
|
|
2055
2468
|
body: action.value,
|
|
@@ -2066,8 +2479,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2066
2479
|
appliedActions.push({ type: action.type, value: action.value, success: false, error: "Invalid user ID" });
|
|
2067
2480
|
break;
|
|
2068
2481
|
}
|
|
2069
|
-
await payload.
|
|
2070
|
-
collection: slugs.tickets,
|
|
2482
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2071
2483
|
id: ticketId,
|
|
2072
2484
|
data: { assignedTo: userId },
|
|
2073
2485
|
overrideAccess: true
|
|
@@ -2119,8 +2531,7 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2119
2531
|
return Response.json({ error: "Invalid collection. Use email-logs or auth-logs." }, { status: 400 });
|
|
2120
2532
|
}
|
|
2121
2533
|
const cutoff = days > 0 ? new Date(Date.now() - days * 864e5).toISOString() : null;
|
|
2122
|
-
const result = await payload
|
|
2123
|
-
collection: allowedCollections[collection],
|
|
2534
|
+
const result = await dbDelete(payload, allowedCollections[collection], {
|
|
2124
2535
|
where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
|
|
2125
2536
|
overrideAccess: true
|
|
2126
2537
|
});
|
|
@@ -2140,50 +2551,6 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2140
2551
|
};
|
|
2141
2552
|
}
|
|
2142
2553
|
|
|
2143
|
-
// src/utils/rateLimiter.ts
|
|
2144
|
-
var RateLimiter = class {
|
|
2145
|
-
constructor(windowMs, maxRequests) {
|
|
2146
|
-
this.windowMs = windowMs;
|
|
2147
|
-
this.maxRequests = maxRequests;
|
|
2148
|
-
const timer = setInterval(() => this.cleanup(), windowMs);
|
|
2149
|
-
timer.unref();
|
|
2150
|
-
}
|
|
2151
|
-
windowMs;
|
|
2152
|
-
maxRequests;
|
|
2153
|
-
store = /* @__PURE__ */ new Map();
|
|
2154
|
-
/**
|
|
2155
|
-
* Check if a key has exceeded the rate limit.
|
|
2156
|
-
* Returns true if the request should be blocked.
|
|
2157
|
-
*/
|
|
2158
|
-
check(key) {
|
|
2159
|
-
const now = Date.now();
|
|
2160
|
-
const entry = this.store.get(key);
|
|
2161
|
-
if (!entry || now > entry.resetAt) {
|
|
2162
|
-
this.store.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
2163
|
-
return false;
|
|
2164
|
-
}
|
|
2165
|
-
entry.count++;
|
|
2166
|
-
return entry.count > this.maxRequests;
|
|
2167
|
-
}
|
|
2168
|
-
/**
|
|
2169
|
-
* Reset the counter for a specific key.
|
|
2170
|
-
*/
|
|
2171
|
-
reset(key) {
|
|
2172
|
-
this.store.delete(key);
|
|
2173
|
-
}
|
|
2174
|
-
/**
|
|
2175
|
-
* Remove expired entries from the store.
|
|
2176
|
-
*/
|
|
2177
|
-
cleanup() {
|
|
2178
|
-
const now = Date.now();
|
|
2179
|
-
for (const [key, entry] of this.store) {
|
|
2180
|
-
if (now > entry.resetAt) {
|
|
2181
|
-
this.store.delete(key);
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
};
|
|
2186
|
-
|
|
2187
2554
|
// src/endpoints/chatbot.ts
|
|
2188
2555
|
var chatbotLimiter = new RateLimiter(6e4, 10);
|
|
2189
2556
|
function createChatbotEndpoint(slugs) {
|
|
@@ -2207,8 +2574,7 @@ function createChatbotEndpoint(slugs) {
|
|
|
2207
2574
|
return Response.json({ error: "Question too short" }, { status: 400 });
|
|
2208
2575
|
}
|
|
2209
2576
|
const payload = req.payload;
|
|
2210
|
-
const articles = await payload.
|
|
2211
|
-
collection: slugs.knowledgeBase,
|
|
2577
|
+
const articles = await dbFind(payload, slugs.knowledgeBase, {
|
|
2212
2578
|
where: { published: { equals: true } },
|
|
2213
2579
|
limit: 100,
|
|
2214
2580
|
depth: 0,
|
|
@@ -2298,8 +2664,7 @@ function createChatGetEndpoint(slugs) {
|
|
|
2298
2664
|
if (after) {
|
|
2299
2665
|
where.createdAt = { greater_than: after };
|
|
2300
2666
|
}
|
|
2301
|
-
const messages = await payload.
|
|
2302
|
-
collection: slugs.chatMessages,
|
|
2667
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2303
2668
|
where,
|
|
2304
2669
|
sort: "createdAt",
|
|
2305
2670
|
limit: 100,
|
|
@@ -2342,8 +2707,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2342
2707
|
return Response.json({ error: "Trop de sessions cr\xE9\xE9es. R\xE9essayez plus tard." }, { status: 429 });
|
|
2343
2708
|
}
|
|
2344
2709
|
const sessionId = `chat_${crypto3__default.default.randomBytes(16).toString("hex")}`;
|
|
2345
|
-
const systemMsg = await payload.
|
|
2346
|
-
collection: slugs.chatMessages,
|
|
2710
|
+
const systemMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2347
2711
|
data: {
|
|
2348
2712
|
session: sessionId,
|
|
2349
2713
|
client: req.user.id,
|
|
@@ -2363,8 +2727,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2363
2727
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2364
2728
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2365
2729
|
}
|
|
2366
|
-
const existing = await payload.
|
|
2367
|
-
collection: slugs.chatMessages,
|
|
2730
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2368
2731
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2369
2732
|
limit: 1,
|
|
2370
2733
|
overrideAccess: true
|
|
@@ -2372,8 +2735,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2372
2735
|
if (existing.docs.length === 0) {
|
|
2373
2736
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2374
2737
|
}
|
|
2375
|
-
const newMsg = await payload.
|
|
2376
|
-
collection: slugs.chatMessages,
|
|
2738
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2377
2739
|
data: {
|
|
2378
2740
|
session,
|
|
2379
2741
|
client: req.user.id,
|
|
@@ -2386,8 +2748,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2386
2748
|
return Response.json({ message: newMsg });
|
|
2387
2749
|
}
|
|
2388
2750
|
if (action === "close" && session) {
|
|
2389
|
-
const existing = await payload.
|
|
2390
|
-
collection: slugs.chatMessages,
|
|
2751
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2391
2752
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2392
2753
|
limit: 1,
|
|
2393
2754
|
overrideAccess: true
|
|
@@ -2395,8 +2756,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2395
2756
|
if (existing.docs.length === 0) {
|
|
2396
2757
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2397
2758
|
}
|
|
2398
|
-
await payload.
|
|
2399
|
-
collection: slugs.chatMessages,
|
|
2759
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2400
2760
|
data: {
|
|
2401
2761
|
session,
|
|
2402
2762
|
client: req.user.id,
|
|
@@ -2438,8 +2798,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2438
2798
|
if (!sessionId) {
|
|
2439
2799
|
return Response.json({ error: "Missing session parameter" }, { status: 400 });
|
|
2440
2800
|
}
|
|
2441
|
-
const existing = await req.payload.
|
|
2442
|
-
collection: slugs.chatMessages,
|
|
2801
|
+
const existing = await dbFind(req.payload, slugs.chatMessages, {
|
|
2443
2802
|
where: {
|
|
2444
2803
|
session: { equals: sessionId },
|
|
2445
2804
|
client: { equals: userId }
|
|
@@ -2461,8 +2820,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2461
2820
|
);
|
|
2462
2821
|
const interval = setInterval(async () => {
|
|
2463
2822
|
try {
|
|
2464
|
-
const messages = await req.payload.
|
|
2465
|
-
collection: slugs.chatMessages,
|
|
2823
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2466
2824
|
where: {
|
|
2467
2825
|
session: { equals: sessionId },
|
|
2468
2826
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2543,8 +2901,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2543
2901
|
if (session) {
|
|
2544
2902
|
const where = { session: { equals: session } };
|
|
2545
2903
|
if (after) where.createdAt = { greater_than: after };
|
|
2546
|
-
const messages = await payload.
|
|
2547
|
-
collection: slugs.chatMessages,
|
|
2904
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2548
2905
|
where,
|
|
2549
2906
|
sort: "createdAt",
|
|
2550
2907
|
limit: 200,
|
|
@@ -2558,8 +2915,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2558
2915
|
}
|
|
2559
2916
|
});
|
|
2560
2917
|
}
|
|
2561
|
-
const recentMessages = await payload.
|
|
2562
|
-
collection: slugs.chatMessages,
|
|
2918
|
+
const recentMessages = await dbFind(payload, slugs.chatMessages, {
|
|
2563
2919
|
sort: "-createdAt",
|
|
2564
2920
|
limit: 50,
|
|
2565
2921
|
depth: 1,
|
|
@@ -2622,8 +2978,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2622
2978
|
if (!session) {
|
|
2623
2979
|
return Response.json({ error: "Session requise" }, { status: 400 });
|
|
2624
2980
|
}
|
|
2625
|
-
const sessionMsg = await payload.
|
|
2626
|
-
collection: slugs.chatMessages,
|
|
2981
|
+
const sessionMsg = await dbFind(payload, slugs.chatMessages, {
|
|
2627
2982
|
where: { session: { equals: session } },
|
|
2628
2983
|
limit: 1,
|
|
2629
2984
|
depth: 0,
|
|
@@ -2641,8 +2996,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2641
2996
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2642
2997
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2643
2998
|
}
|
|
2644
|
-
const newMsg = await payload.
|
|
2645
|
-
collection: slugs.chatMessages,
|
|
2999
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2646
3000
|
data: {
|
|
2647
3001
|
session,
|
|
2648
3002
|
client: clientId,
|
|
@@ -2654,8 +3008,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2654
3008
|
overrideAccess: true
|
|
2655
3009
|
});
|
|
2656
3010
|
try {
|
|
2657
|
-
const linkedTicket = await payload.
|
|
2658
|
-
collection: slugs.tickets,
|
|
3011
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2659
3012
|
where: { chatSession: { equals: session } },
|
|
2660
3013
|
limit: 1,
|
|
2661
3014
|
depth: 0,
|
|
@@ -2663,8 +3016,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2663
3016
|
});
|
|
2664
3017
|
if (linkedTicket.docs.length > 0) {
|
|
2665
3018
|
const ticketId = linkedTicket.docs[0].id;
|
|
2666
|
-
await payload.
|
|
2667
|
-
collection: slugs.ticketMessages,
|
|
3019
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2668
3020
|
data: {
|
|
2669
3021
|
ticket: ticketId,
|
|
2670
3022
|
body: trimmedMessage,
|
|
@@ -2673,8 +3025,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2673
3025
|
},
|
|
2674
3026
|
overrideAccess: true
|
|
2675
3027
|
});
|
|
2676
|
-
await payload.
|
|
2677
|
-
collection: slugs.chatMessages,
|
|
3028
|
+
await dbUpdate(payload, slugs.chatMessages, {
|
|
2678
3029
|
id: newMsg.id,
|
|
2679
3030
|
data: { ticket: ticketId },
|
|
2680
3031
|
overrideAccess: true
|
|
@@ -2686,8 +3037,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2686
3037
|
return Response.json({ message: newMsg });
|
|
2687
3038
|
}
|
|
2688
3039
|
if (action === "close") {
|
|
2689
|
-
await payload.
|
|
2690
|
-
collection: slugs.chatMessages,
|
|
3040
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2691
3041
|
data: {
|
|
2692
3042
|
session,
|
|
2693
3043
|
client: clientId,
|
|
@@ -2698,16 +3048,14 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2698
3048
|
overrideAccess: true
|
|
2699
3049
|
});
|
|
2700
3050
|
try {
|
|
2701
|
-
const linkedTicket = await payload.
|
|
2702
|
-
collection: slugs.tickets,
|
|
3051
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2703
3052
|
where: { chatSession: { equals: session } },
|
|
2704
3053
|
limit: 1,
|
|
2705
3054
|
depth: 0,
|
|
2706
3055
|
overrideAccess: true
|
|
2707
3056
|
});
|
|
2708
3057
|
if (linkedTicket.docs.length > 0) {
|
|
2709
|
-
await payload.
|
|
2710
|
-
collection: slugs.ticketMessages,
|
|
3058
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2711
3059
|
data: {
|
|
2712
3060
|
ticket: linkedTicket.docs[0].id,
|
|
2713
3061
|
body: "Session de chat termin\xE9e par un agent.",
|
|
@@ -2770,8 +3118,7 @@ function createSessionStream(req, slugs, sessionId) {
|
|
|
2770
3118
|
);
|
|
2771
3119
|
const interval = setInterval(async () => {
|
|
2772
3120
|
try {
|
|
2773
|
-
const messages = await req.payload.
|
|
2774
|
-
collection: slugs.chatMessages,
|
|
3121
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2775
3122
|
where: {
|
|
2776
3123
|
session: { equals: sessionId },
|
|
2777
3124
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2844,8 +3191,7 @@ function createSessionListStream(req, slugs) {
|
|
|
2844
3191
|
);
|
|
2845
3192
|
const interval = setInterval(async () => {
|
|
2846
3193
|
try {
|
|
2847
|
-
const recentMessages = await req.payload.
|
|
2848
|
-
collection: slugs.chatMessages,
|
|
3194
|
+
const recentMessages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2849
3195
|
sort: "-createdAt",
|
|
2850
3196
|
limit: 50,
|
|
2851
3197
|
depth: 1,
|
|
@@ -2932,8 +3278,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
2932
3278
|
const statuses = ["open", "waiting_client", "resolved"];
|
|
2933
3279
|
const statusCounts = await Promise.all(
|
|
2934
3280
|
statuses.map(async (status) => {
|
|
2935
|
-
const result = await payload.
|
|
2936
|
-
collection: slugs.tickets,
|
|
3281
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
2937
3282
|
where: { status: { equals: status } },
|
|
2938
3283
|
overrideAccess: true
|
|
2939
3284
|
});
|
|
@@ -2945,8 +3290,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
2945
3290
|
const priorities = ["low", "normal", "high", "urgent"];
|
|
2946
3291
|
const priorityCounts = await Promise.all(
|
|
2947
3292
|
priorities.map(async (priority) => {
|
|
2948
|
-
const result = await payload.
|
|
2949
|
-
collection: slugs.tickets,
|
|
3293
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
2950
3294
|
where: { priority: { equals: priority } },
|
|
2951
3295
|
overrideAccess: true
|
|
2952
3296
|
});
|
|
@@ -2957,8 +3301,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
2957
3301
|
const categories = ["bug", "content", "feature", "question", "hosting"];
|
|
2958
3302
|
const categoryCounts = await Promise.all(
|
|
2959
3303
|
categories.map(async (category) => {
|
|
2960
|
-
const result = await payload.
|
|
2961
|
-
collection: slugs.tickets,
|
|
3304
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
2962
3305
|
where: { category: { equals: category } },
|
|
2963
3306
|
overrideAccess: true
|
|
2964
3307
|
});
|
|
@@ -2972,17 +3315,34 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
2972
3315
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
|
|
2973
3316
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
2974
3317
|
const [created7, created30] = await Promise.all([
|
|
2975
|
-
payload.
|
|
2976
|
-
collection: slugs.tickets,
|
|
3318
|
+
dbCount(payload, slugs.tickets, {
|
|
2977
3319
|
where: { createdAt: { greater_than_equal: sevenDaysAgo.toISOString() } },
|
|
2978
3320
|
overrideAccess: true
|
|
2979
3321
|
}),
|
|
2980
|
-
payload.
|
|
2981
|
-
collection: slugs.tickets,
|
|
3322
|
+
dbCount(payload, slugs.tickets, {
|
|
2982
3323
|
where: { createdAt: { greater_than_equal: thirtyDaysAgo.toISOString() } },
|
|
2983
3324
|
overrideAccess: true
|
|
2984
3325
|
})
|
|
2985
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
|
+
);
|
|
2986
3346
|
const PAGE_SIZE2 = 100;
|
|
2987
3347
|
const MAX_PAGES2 = 50;
|
|
2988
3348
|
let totalResponseTimeMs = 0;
|
|
@@ -2993,8 +3353,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
2993
3353
|
let page = 1;
|
|
2994
3354
|
let hasMore = true;
|
|
2995
3355
|
while (hasMore && page <= MAX_PAGES2) {
|
|
2996
|
-
const batch = await payload.
|
|
2997
|
-
collection: slugs.tickets,
|
|
3356
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
2998
3357
|
limit: PAGE_SIZE2,
|
|
2999
3358
|
page,
|
|
3000
3359
|
depth: 0,
|
|
@@ -3027,43 +3386,62 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3027
3386
|
hasMore = batch.hasNextPage ?? false;
|
|
3028
3387
|
page++;
|
|
3029
3388
|
}
|
|
3030
|
-
const surveyCount = await payload.
|
|
3031
|
-
collection: slugs.satisfactionSurveys,
|
|
3389
|
+
const surveyCount = await dbCount(payload, slugs.satisfactionSurveys, {
|
|
3032
3390
|
overrideAccess: true
|
|
3033
3391
|
});
|
|
3034
3392
|
let satisfactionAvg = 0;
|
|
3393
|
+
let satisfactionCount = 0;
|
|
3394
|
+
let npsScore = null;
|
|
3395
|
+
let npsCount = 0;
|
|
3035
3396
|
if (surveyCount.totalDocs > 0) {
|
|
3036
|
-
let
|
|
3397
|
+
let ratingSum = 0;
|
|
3398
|
+
let ratingN = 0;
|
|
3399
|
+
let promoters = 0;
|
|
3400
|
+
let detractors = 0;
|
|
3401
|
+
let npsN = 0;
|
|
3037
3402
|
let surveyPage = 1;
|
|
3038
3403
|
let surveyHasMore = true;
|
|
3039
3404
|
while (surveyHasMore && surveyPage <= MAX_PAGES2) {
|
|
3040
|
-
const batch = await payload.
|
|
3041
|
-
collection: slugs.satisfactionSurveys,
|
|
3405
|
+
const batch = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
3042
3406
|
limit: PAGE_SIZE2,
|
|
3043
3407
|
page: surveyPage,
|
|
3044
3408
|
depth: 0,
|
|
3045
3409
|
overrideAccess: true,
|
|
3046
|
-
select: { rating: true }
|
|
3410
|
+
select: { rating: true, nps: true }
|
|
3047
3411
|
});
|
|
3048
3412
|
for (const s of batch.docs) {
|
|
3049
|
-
|
|
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
|
+
}
|
|
3050
3424
|
}
|
|
3051
3425
|
surveyHasMore = batch.hasNextPage ?? false;
|
|
3052
3426
|
surveyPage++;
|
|
3053
3427
|
}
|
|
3054
|
-
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
|
+
}
|
|
3055
3434
|
}
|
|
3056
|
-
const
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
}),
|
|
3061
|
-
payload.count({
|
|
3062
|
-
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, {
|
|
3063
3439
|
where: { status: { equals: "pending" } },
|
|
3064
3440
|
overrideAccess: true
|
|
3065
|
-
})
|
|
3066
|
-
|
|
3441
|
+
});
|
|
3442
|
+
pendingEmailsTotal = pe.totalDocs;
|
|
3443
|
+
} catch {
|
|
3444
|
+
}
|
|
3067
3445
|
return new Response(JSON.stringify({
|
|
3068
3446
|
total,
|
|
3069
3447
|
byStatus,
|
|
@@ -3071,13 +3449,16 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3071
3449
|
byCategory,
|
|
3072
3450
|
createdLast7Days: created7.totalDocs,
|
|
3073
3451
|
createdLast30Days: created30.totalDocs,
|
|
3452
|
+
volumeByDay,
|
|
3074
3453
|
avgResponseTimeHours: responseTimeCount > 0 ? Math.round(totalResponseTimeMs / responseTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3075
3454
|
avgResolutionTimeHours: resolutionTimeCount > 0 ? Math.round(totalResolutionTimeMs / resolutionTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3076
3455
|
totalTimeMinutes,
|
|
3077
3456
|
satisfactionAvg,
|
|
3078
|
-
satisfactionCount
|
|
3457
|
+
satisfactionCount,
|
|
3458
|
+
npsScore,
|
|
3459
|
+
npsCount,
|
|
3079
3460
|
clientCount: clientCount.totalDocs,
|
|
3080
|
-
pendingEmailsCount:
|
|
3461
|
+
pendingEmailsCount: pendingEmailsTotal
|
|
3081
3462
|
}), {
|
|
3082
3463
|
headers: {
|
|
3083
3464
|
"Content-Type": "application/json",
|
|
@@ -3147,13 +3528,26 @@ function createBillingEndpoint(slugs) {
|
|
|
3147
3528
|
let ticketPage = 1;
|
|
3148
3529
|
let ticketHasMore = true;
|
|
3149
3530
|
while (ticketHasMore && ticketPage <= MAX_PAGES) {
|
|
3150
|
-
const batch = await payload.
|
|
3151
|
-
collection: slugs.tickets,
|
|
3531
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3152
3532
|
where: ticketWhere,
|
|
3153
3533
|
limit: PAGE_SIZE,
|
|
3154
3534
|
page: ticketPage,
|
|
3535
|
+
// depth 2 is required: we read ticket.project.client.company below.
|
|
3155
3536
|
depth: 2,
|
|
3156
|
-
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
|
+
}
|
|
3157
3551
|
});
|
|
3158
3552
|
allTickets.push(...batch.docs);
|
|
3159
3553
|
ticketHasMore = batch.hasNextPage ?? false;
|
|
@@ -3163,8 +3557,7 @@ function createBillingEndpoint(slugs) {
|
|
|
3163
3557
|
let entryPage = 1;
|
|
3164
3558
|
let entryHasMore = true;
|
|
3165
3559
|
while (entryHasMore && entryPage <= MAX_PAGES) {
|
|
3166
|
-
const batch = await payload.
|
|
3167
|
-
collection: slugs.timeEntries,
|
|
3560
|
+
const batch = await dbFind(payload, slugs.timeEntries, {
|
|
3168
3561
|
where: {
|
|
3169
3562
|
and: [
|
|
3170
3563
|
{ date: { greater_than_equal: from } },
|
|
@@ -3262,9 +3655,174 @@ function createBillingEndpoint(slugs) {
|
|
|
3262
3655
|
}
|
|
3263
3656
|
};
|
|
3264
3657
|
}
|
|
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
|
+
}
|
|
3711
|
+
});
|
|
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
|
+
}
|
|
3265
3823
|
|
|
3266
3824
|
// src/utils/generateTicketSynthesis.ts
|
|
3267
|
-
function
|
|
3825
|
+
function getClient4(aiSettings) {
|
|
3268
3826
|
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
3269
3827
|
if (aiSettings.provider === "ollama") {
|
|
3270
3828
|
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
@@ -3281,8 +3839,7 @@ async function generateTicketSynthesis(args) {
|
|
|
3281
3839
|
if (settings.ai.enableSynthesis === false) {
|
|
3282
3840
|
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "skipped", reason: "synthesis disabled" };
|
|
3283
3841
|
}
|
|
3284
|
-
const ticket = await payload.
|
|
3285
|
-
collection: slugs.tickets,
|
|
3842
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
3286
3843
|
id: ticketId,
|
|
3287
3844
|
depth: 1,
|
|
3288
3845
|
overrideAccess: true
|
|
@@ -3290,15 +3847,13 @@ async function generateTicketSynthesis(args) {
|
|
|
3290
3847
|
if (!ticket) {
|
|
3291
3848
|
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "error", reason: "ticket not found" };
|
|
3292
3849
|
}
|
|
3293
|
-
await payload.
|
|
3294
|
-
collection: slugs.tickets,
|
|
3850
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3295
3851
|
id: ticketId,
|
|
3296
3852
|
data: { aiSummaryStatus: "pending" },
|
|
3297
3853
|
overrideAccess: true
|
|
3298
3854
|
}).catch(() => {
|
|
3299
3855
|
});
|
|
3300
|
-
const messagesResult = await payload.
|
|
3301
|
-
collection: slugs.ticketMessages,
|
|
3856
|
+
const messagesResult = await dbFind(payload, slugs.ticketMessages, {
|
|
3302
3857
|
where: { ticket: { equals: ticketId } },
|
|
3303
3858
|
sort: "createdAt",
|
|
3304
3859
|
limit: 500,
|
|
@@ -3308,8 +3863,7 @@ async function generateTicketSynthesis(args) {
|
|
|
3308
3863
|
const messages = messagesResult.docs;
|
|
3309
3864
|
if (messages.length === 0) {
|
|
3310
3865
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3311
|
-
await payload.
|
|
3312
|
-
collection: slugs.tickets,
|
|
3866
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3313
3867
|
id: ticketId,
|
|
3314
3868
|
data: {
|
|
3315
3869
|
aiSummary: "(Aucun message dans ce ticket)",
|
|
@@ -3354,7 +3908,7 @@ Regles strictes :
|
|
|
3354
3908
|
- Si le ticket n'a pas abouti, decris quand meme le travail d'analyse realise
|
|
3355
3909
|
|
|
3356
3910
|
Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
3357
|
-
const anthropic =
|
|
3911
|
+
const anthropic = getClient4(settings.ai);
|
|
3358
3912
|
const model = getModel3(settings.ai);
|
|
3359
3913
|
try {
|
|
3360
3914
|
const res = await anthropic.messages.create({
|
|
@@ -3364,8 +3918,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3364
3918
|
});
|
|
3365
3919
|
const summary = res.content[0]?.type === "text" ? res.content[0].text.trim() : "";
|
|
3366
3920
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3367
|
-
await payload.
|
|
3368
|
-
collection: slugs.tickets,
|
|
3921
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3369
3922
|
id: ticketId,
|
|
3370
3923
|
data: {
|
|
3371
3924
|
aiSummary: summary,
|
|
@@ -3377,8 +3930,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3377
3930
|
return { summary, generatedAt, status: "done" };
|
|
3378
3931
|
} catch (err) {
|
|
3379
3932
|
const message = err instanceof Error ? err.message : String(err);
|
|
3380
|
-
await payload.
|
|
3381
|
-
collection: slugs.tickets,
|
|
3933
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3382
3934
|
id: ticketId,
|
|
3383
3935
|
data: { aiSummaryStatus: "error" },
|
|
3384
3936
|
overrideAccess: true
|
|
@@ -3408,8 +3960,7 @@ function createTicketSynthesisEndpoint(slugs) {
|
|
|
3408
3960
|
return Response.json({ error: "ticketId must be a number" }, { status: 400 });
|
|
3409
3961
|
}
|
|
3410
3962
|
if (!force) {
|
|
3411
|
-
const existing = await payload.
|
|
3412
|
-
collection: slugs.tickets,
|
|
3963
|
+
const existing = await dbFindByID(payload, slugs.tickets, {
|
|
3413
3964
|
id: ticketId,
|
|
3414
3965
|
depth: 0,
|
|
3415
3966
|
overrideAccess: true
|
|
@@ -3453,8 +4004,7 @@ function createEmailStatsEndpoint(slugs) {
|
|
|
3453
4004
|
let page = 1;
|
|
3454
4005
|
let hasMore = true;
|
|
3455
4006
|
while (hasMore && page <= MAX_PAGES2) {
|
|
3456
|
-
const result = await payload.
|
|
3457
|
-
collection: slugs.emailLogs,
|
|
4007
|
+
const result = await dbFind(payload, slugs.emailLogs, {
|
|
3458
4008
|
where: { createdAt: { greater_than: cutoff } },
|
|
3459
4009
|
sort: "-createdAt",
|
|
3460
4010
|
limit: 500,
|
|
@@ -3528,184 +4078,72 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3528
4078
|
const payload = req.payload;
|
|
3529
4079
|
requireClient(req, slugs);
|
|
3530
4080
|
const body = await req.json();
|
|
3531
|
-
const { ticketId, rating, comment } = body;
|
|
3532
|
-
if (!ticketId
|
|
3533
|
-
return Response.json(
|
|
3534
|
-
{ error: "ticketId et rating (entier 1-5) sont requis." },
|
|
3535
|
-
{ status: 400 }
|
|
3536
|
-
);
|
|
3537
|
-
}
|
|
3538
|
-
if (comment && typeof comment === "string" && comment.length > 5e3) {
|
|
3539
|
-
return Response.json(
|
|
3540
|
-
{ error: "Le commentaire ne peut pas d\xE9passer 5000 caract\xE8res." },
|
|
3541
|
-
{ status: 400 }
|
|
3542
|
-
);
|
|
3543
|
-
}
|
|
3544
|
-
const ticket = await payload.findByID({
|
|
3545
|
-
collection: slugs.tickets,
|
|
3546
|
-
id: ticketId,
|
|
3547
|
-
depth: 0,
|
|
3548
|
-
overrideAccess: false,
|
|
3549
|
-
user: req.user
|
|
3550
|
-
});
|
|
3551
|
-
if (!ticket) {
|
|
3552
|
-
return Response.json({ error: "Ticket introuvable." }, { status: 404 });
|
|
4081
|
+
const { ticketId, rating, nps, comment } = body;
|
|
4082
|
+
if (!ticketId) {
|
|
4083
|
+
return Response.json({ error: "ticketId est requis." }, { status: 400 });
|
|
3553
4084
|
}
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
);
|
|
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 });
|
|
3559
4089
|
}
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
where: { ticket: { equals: ticketId } },
|
|
3563
|
-
limit: 1,
|
|
3564
|
-
depth: 0,
|
|
3565
|
-
overrideAccess: true
|
|
3566
|
-
});
|
|
3567
|
-
if (existing.docs.length > 0) {
|
|
3568
|
-
return Response.json(
|
|
3569
|
-
{ error: "Vous avez d\xE9j\xE0 \xE9valu\xE9 ce ticket." },
|
|
3570
|
-
{ status: 409 }
|
|
3571
|
-
);
|
|
4090
|
+
if (hasRating && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
4091
|
+
return Response.json({ error: "rating doit \xEAtre un entier 1-5." }, { status: 400 });
|
|
3572
4092
|
}
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
data: {
|
|
3576
|
-
source: "ticket",
|
|
3577
|
-
ticket: ticketId,
|
|
3578
|
-
client: req.user.id,
|
|
3579
|
-
rating: Math.round(rating),
|
|
3580
|
-
...comment ? { comment: comment.trim() } : {}
|
|
3581
|
-
},
|
|
3582
|
-
overrideAccess: true
|
|
3583
|
-
});
|
|
3584
|
-
return Response.json({ success: true, survey });
|
|
3585
|
-
} catch (error) {
|
|
3586
|
-
const authResponse = handleAuthError(error);
|
|
3587
|
-
if (authResponse) return authResponse;
|
|
3588
|
-
console.error("[satisfaction] Error:", error);
|
|
3589
|
-
return Response.json({ error: "Erreur interne." }, { status: 500 });
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
};
|
|
3593
|
-
}
|
|
3594
|
-
var TRANSPARENT_GIF = Buffer.from(
|
|
3595
|
-
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
3596
|
-
"base64"
|
|
3597
|
-
);
|
|
3598
|
-
function generateTrackingToken(ticketId, messageId, secret) {
|
|
3599
|
-
return crypto3.createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex").substring(0, 16);
|
|
3600
|
-
}
|
|
3601
|
-
function createTrackOpenEndpoint(slugs) {
|
|
3602
|
-
return {
|
|
3603
|
-
path: "/support/track-open",
|
|
3604
|
-
method: "get",
|
|
3605
|
-
handler: async (req) => {
|
|
3606
|
-
const url = new URL(req.url);
|
|
3607
|
-
const ticketId = url.searchParams.get("t");
|
|
3608
|
-
const messageId = url.searchParams.get("m");
|
|
3609
|
-
const sig = url.searchParams.get("sig");
|
|
3610
|
-
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
3611
|
-
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
3612
|
-
const secret = process.env.PAYLOAD_SECRET || "";
|
|
3613
|
-
if (secret && ticketId && messageId && sig) {
|
|
3614
|
-
const expected = generateTrackingToken(ticketId, messageId, secret);
|
|
3615
|
-
if (sig !== expected) {
|
|
3616
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3617
|
-
status: 200,
|
|
3618
|
-
headers: {
|
|
3619
|
-
"Content-Type": "image/gif",
|
|
3620
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3621
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3622
|
-
}
|
|
3623
|
-
});
|
|
4093
|
+
if (hasNps && (!Number.isInteger(nps) || nps < 0 || nps > 10)) {
|
|
4094
|
+
return Response.json({ error: "nps doit \xEAtre un entier 0-10." }, { status: 400 });
|
|
3624
4095
|
}
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3631
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3632
|
-
}
|
|
3633
|
-
});
|
|
3634
|
-
}
|
|
3635
|
-
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
3636
|
-
try {
|
|
3637
|
-
const payload = req.payload;
|
|
3638
|
-
const ticket = await payload.findByID({
|
|
3639
|
-
collection: slugs.tickets,
|
|
3640
|
-
id: parsedId,
|
|
3641
|
-
depth: 0,
|
|
3642
|
-
overrideAccess: true,
|
|
3643
|
-
select: { lastClientReadAt: true }
|
|
3644
|
-
});
|
|
3645
|
-
if (ticket) {
|
|
3646
|
-
const lastRead = ticket.lastClientReadAt ? new Date(ticket.lastClientReadAt).getTime() : 0;
|
|
3647
|
-
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
|
|
3648
|
-
if (lastRead < fiveMinAgo) {
|
|
3649
|
-
await payload.update({
|
|
3650
|
-
collection: slugs.tickets,
|
|
3651
|
-
id: parsedId,
|
|
3652
|
-
data: { lastClientReadAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3653
|
-
overrideAccess: true
|
|
3654
|
-
});
|
|
3655
|
-
}
|
|
3656
|
-
}
|
|
3657
|
-
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
3658
|
-
const msg = await payload.findByID({
|
|
3659
|
-
collection: slugs.ticketMessages,
|
|
3660
|
-
id: parsedMsgId,
|
|
3661
|
-
depth: 0,
|
|
3662
|
-
overrideAccess: true,
|
|
3663
|
-
select: { emailOpenedAt: true }
|
|
3664
|
-
});
|
|
3665
|
-
if (msg && !msg.emailOpenedAt) {
|
|
3666
|
-
await payload.update({
|
|
3667
|
-
collection: slugs.ticketMessages,
|
|
3668
|
-
id: parsedMsgId,
|
|
3669
|
-
data: { emailOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3670
|
-
overrideAccess: true
|
|
3671
|
-
});
|
|
3672
|
-
const ticketInfo = await payload.findByID({
|
|
3673
|
-
collection: slugs.tickets,
|
|
3674
|
-
id: parsedId,
|
|
3675
|
-
depth: 1,
|
|
3676
|
-
overrideAccess: true,
|
|
3677
|
-
select: { ticketNumber: true, subject: true, client: true }
|
|
3678
|
-
});
|
|
3679
|
-
const clientName = typeof ticketInfo?.client === "object" ? ticketInfo.client?.firstName || "Client" : "Client";
|
|
3680
|
-
try {
|
|
3681
|
-
await payload.create({
|
|
3682
|
-
collection: "admin-notifications",
|
|
3683
|
-
data: {
|
|
3684
|
-
title: `Email ouvert \u2014 ${ticketInfo?.ticketNumber || "TK-????"}`,
|
|
3685
|
-
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || "ticket"}"`,
|
|
3686
|
-
type: "email_opened",
|
|
3687
|
-
link: `/admin/ticket?id=${parsedId}`
|
|
3688
|
-
},
|
|
3689
|
-
overrideAccess: true
|
|
3690
|
-
});
|
|
3691
|
-
} catch {
|
|
3692
|
-
}
|
|
3693
|
-
}
|
|
3694
|
-
}
|
|
3695
|
-
} catch (err) {
|
|
3696
|
-
console.error("[track-open] Error:", err);
|
|
4096
|
+
if (comment && typeof comment === "string" && comment.length > 5e3) {
|
|
4097
|
+
return Response.json(
|
|
4098
|
+
{ error: "Le commentaire ne peut pas d\xE9passer 5000 caract\xE8res." },
|
|
4099
|
+
{ status: 400 }
|
|
4100
|
+
);
|
|
3697
4101
|
}
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
"Expires": "0"
|
|
4102
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
4103
|
+
id: ticketId,
|
|
4104
|
+
depth: 0,
|
|
4105
|
+
overrideAccess: false,
|
|
4106
|
+
user: req.user
|
|
4107
|
+
});
|
|
4108
|
+
if (!ticket) {
|
|
4109
|
+
return Response.json({ error: "Ticket introuvable." }, { status: 404 });
|
|
3707
4110
|
}
|
|
3708
|
-
|
|
4111
|
+
if (ticket.status !== "resolved") {
|
|
4112
|
+
return Response.json(
|
|
4113
|
+
{ error: "Le ticket doit \xEAtre r\xE9solu pour laisser un avis." },
|
|
4114
|
+
{ status: 400 }
|
|
4115
|
+
);
|
|
4116
|
+
}
|
|
4117
|
+
const existing = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
4118
|
+
where: { ticket: { equals: ticketId } },
|
|
4119
|
+
limit: 1,
|
|
4120
|
+
depth: 0,
|
|
4121
|
+
overrideAccess: true
|
|
4122
|
+
});
|
|
4123
|
+
if (existing.docs.length > 0) {
|
|
4124
|
+
return Response.json(
|
|
4125
|
+
{ error: "Vous avez d\xE9j\xE0 \xE9valu\xE9 ce ticket." },
|
|
4126
|
+
{ status: 409 }
|
|
4127
|
+
);
|
|
4128
|
+
}
|
|
4129
|
+
const survey = await dbCreate(payload, slugs.satisfactionSurveys, {
|
|
4130
|
+
data: {
|
|
4131
|
+
source: "ticket",
|
|
4132
|
+
ticket: ticketId,
|
|
4133
|
+
client: req.user.id,
|
|
4134
|
+
...hasRating ? { rating: Math.round(rating) } : {},
|
|
4135
|
+
...hasNps ? { nps: Math.round(nps) } : {},
|
|
4136
|
+
...comment ? { comment: comment.trim() } : {}
|
|
4137
|
+
},
|
|
4138
|
+
overrideAccess: true
|
|
4139
|
+
});
|
|
4140
|
+
return Response.json({ success: true, survey });
|
|
4141
|
+
} catch (error) {
|
|
4142
|
+
const authResponse = handleAuthError(error);
|
|
4143
|
+
if (authResponse) return authResponse;
|
|
4144
|
+
console.error("[satisfaction] Error:", error);
|
|
4145
|
+
return Response.json({ error: "Erreur interne." }, { status: 500 });
|
|
4146
|
+
}
|
|
3709
4147
|
}
|
|
3710
4148
|
};
|
|
3711
4149
|
}
|
|
@@ -3724,8 +4162,7 @@ function createExportCsvEndpoint(slugs) {
|
|
|
3724
4162
|
let hasMore = true;
|
|
3725
4163
|
const allDocs = [];
|
|
3726
4164
|
while (hasMore) {
|
|
3727
|
-
const batch = await payload.
|
|
3728
|
-
collection: slugs.tickets,
|
|
4165
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3729
4166
|
limit: PAGE_SIZE2,
|
|
3730
4167
|
page,
|
|
3731
4168
|
depth: 1,
|
|
@@ -3801,21 +4238,18 @@ function createExportDataEndpoint(slugs) {
|
|
|
3801
4238
|
const payload = req.payload;
|
|
3802
4239
|
requireClient(req, slugs);
|
|
3803
4240
|
const [clientData, ticketsResult, messagesResult, surveysResult] = await Promise.all([
|
|
3804
|
-
payload.
|
|
3805
|
-
collection: slugs.supportClients,
|
|
4241
|
+
dbFindByID(payload, slugs.supportClients, {
|
|
3806
4242
|
id: req.user.id,
|
|
3807
4243
|
depth: 0,
|
|
3808
4244
|
overrideAccess: true
|
|
3809
4245
|
}),
|
|
3810
|
-
payload.
|
|
3811
|
-
collection: slugs.tickets,
|
|
4246
|
+
dbFind(payload, slugs.tickets, {
|
|
3812
4247
|
where: { client: { equals: req.user.id } },
|
|
3813
4248
|
limit: 1e3,
|
|
3814
4249
|
depth: 0,
|
|
3815
4250
|
overrideAccess: true
|
|
3816
4251
|
}),
|
|
3817
|
-
payload.
|
|
3818
|
-
collection: slugs.ticketMessages,
|
|
4252
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
3819
4253
|
where: {
|
|
3820
4254
|
"ticket.client": { equals: req.user.id },
|
|
3821
4255
|
authorType: { equals: "client" }
|
|
@@ -3824,8 +4258,7 @@ function createExportDataEndpoint(slugs) {
|
|
|
3824
4258
|
depth: 0,
|
|
3825
4259
|
overrideAccess: true
|
|
3826
4260
|
}),
|
|
3827
|
-
payload.
|
|
3828
|
-
collection: slugs.satisfactionSurveys,
|
|
4261
|
+
dbFind(payload, slugs.satisfactionSurveys, {
|
|
3829
4262
|
where: { client: { equals: req.user.id } },
|
|
3830
4263
|
limit: 500,
|
|
3831
4264
|
depth: 0,
|
|
@@ -3899,8 +4332,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3899
4332
|
if (!["create_ticket", "add_to_ticket", "ignore"].includes(action)) {
|
|
3900
4333
|
return Response.json({ error: "Invalid action" }, { status: 400 });
|
|
3901
4334
|
}
|
|
3902
|
-
const pendingEmail = await payload.
|
|
3903
|
-
collection: slugs.pendingEmails,
|
|
4335
|
+
const pendingEmail = await dbFindByID(payload, slugs.pendingEmails, {
|
|
3904
4336
|
id: Number(id),
|
|
3905
4337
|
depth: 1,
|
|
3906
4338
|
overrideAccess: true
|
|
@@ -3914,14 +4346,12 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3914
4346
|
let clientId = overrideClientId || (typeof pendingEmail.client === "object" ? pendingEmail.client?.id : pendingEmail.client);
|
|
3915
4347
|
let clientDoc = typeof pendingEmail.client === "object" ? pendingEmail.client : null;
|
|
3916
4348
|
if (overrideClientId && overrideClientId !== clientDoc?.id) {
|
|
3917
|
-
clientDoc = await payload.
|
|
3918
|
-
collection: slugs.supportClients,
|
|
4349
|
+
clientDoc = await dbFindByID(payload, slugs.supportClients, {
|
|
3919
4350
|
id: overrideClientId,
|
|
3920
4351
|
depth: 0,
|
|
3921
4352
|
overrideAccess: true
|
|
3922
4353
|
});
|
|
3923
|
-
await payload.
|
|
3924
|
-
collection: slugs.pendingEmails,
|
|
4354
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
3925
4355
|
id: Number(id),
|
|
3926
4356
|
data: { client: overrideClientId },
|
|
3927
4357
|
overrideAccess: true
|
|
@@ -3938,8 +4368,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3938
4368
|
file: typeof a.file === "object" ? a.file.id : a.file
|
|
3939
4369
|
})) || [];
|
|
3940
4370
|
if (action === "ignore") {
|
|
3941
|
-
await payload.
|
|
3942
|
-
collection: slugs.pendingEmails,
|
|
4371
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
3943
4372
|
id: Number(id),
|
|
3944
4373
|
data: {
|
|
3945
4374
|
status: "ignored",
|
|
@@ -3951,8 +4380,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3951
4380
|
return Response.json({ action: "ignored", pendingEmailId: Number(id) });
|
|
3952
4381
|
}
|
|
3953
4382
|
if (action === "create_ticket") {
|
|
3954
|
-
const newTicket = await payload.
|
|
3955
|
-
collection: slugs.tickets,
|
|
4383
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
3956
4384
|
data: {
|
|
3957
4385
|
subject: pendingEmail.subject,
|
|
3958
4386
|
client: clientId,
|
|
@@ -3963,8 +4391,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3963
4391
|
},
|
|
3964
4392
|
overrideAccess: true
|
|
3965
4393
|
});
|
|
3966
|
-
await payload.
|
|
3967
|
-
collection: slugs.ticketMessages,
|
|
4394
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
3968
4395
|
data: {
|
|
3969
4396
|
ticket: newTicket.id,
|
|
3970
4397
|
body: pendingEmail.body,
|
|
@@ -3991,8 +4418,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3991
4418
|
} catch (err) {
|
|
3992
4419
|
console.error("[pending-email-process] Failed to send notification:", err);
|
|
3993
4420
|
}
|
|
3994
|
-
await payload.
|
|
3995
|
-
collection: slugs.pendingEmails,
|
|
4421
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
3996
4422
|
id: Number(id),
|
|
3997
4423
|
data: {
|
|
3998
4424
|
status: "processed",
|
|
@@ -4012,8 +4438,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4012
4438
|
if (!ticketId) {
|
|
4013
4439
|
return Response.json({ error: "ticketId is required for add_to_ticket" }, { status: 400 });
|
|
4014
4440
|
}
|
|
4015
|
-
const targetTicket = await payload.
|
|
4016
|
-
collection: slugs.tickets,
|
|
4441
|
+
const targetTicket = await dbFindByID(payload, slugs.tickets, {
|
|
4017
4442
|
id: ticketId,
|
|
4018
4443
|
depth: 0,
|
|
4019
4444
|
overrideAccess: true
|
|
@@ -4021,8 +4446,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4021
4446
|
if (!targetTicket) {
|
|
4022
4447
|
return Response.json({ error: "Target ticket not found" }, { status: 404 });
|
|
4023
4448
|
}
|
|
4024
|
-
await payload.
|
|
4025
|
-
collection: slugs.ticketMessages,
|
|
4449
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
4026
4450
|
data: {
|
|
4027
4451
|
ticket: ticketId,
|
|
4028
4452
|
body: pendingEmail.body,
|
|
@@ -4034,15 +4458,13 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4034
4458
|
overrideAccess: true
|
|
4035
4459
|
});
|
|
4036
4460
|
if (targetTicket.status === "resolved") {
|
|
4037
|
-
await payload.
|
|
4038
|
-
collection: slugs.tickets,
|
|
4461
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
4039
4462
|
id: ticketId,
|
|
4040
4463
|
data: { status: "open" },
|
|
4041
4464
|
overrideAccess: true
|
|
4042
4465
|
});
|
|
4043
4466
|
}
|
|
4044
|
-
await payload.
|
|
4045
|
-
collection: slugs.pendingEmails,
|
|
4467
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4046
4468
|
id: Number(id),
|
|
4047
4469
|
data: {
|
|
4048
4470
|
status: "processed",
|
|
@@ -4095,8 +4517,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4095
4517
|
if (!messageId) {
|
|
4096
4518
|
return Response.json({ error: "messageId requis" }, { status: 400 });
|
|
4097
4519
|
}
|
|
4098
|
-
const message = await payload.
|
|
4099
|
-
collection: slugs.ticketMessages,
|
|
4520
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
4100
4521
|
id: messageId,
|
|
4101
4522
|
depth: 0,
|
|
4102
4523
|
overrideAccess: true
|
|
@@ -4105,8 +4526,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4105
4526
|
return Response.json({ error: "Message introuvable" }, { status: 404 });
|
|
4106
4527
|
}
|
|
4107
4528
|
const ticketId = typeof message.ticket === "object" ? message.ticket.id : message.ticket;
|
|
4108
|
-
const ticket = await payload.
|
|
4109
|
-
collection: slugs.tickets,
|
|
4529
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
4110
4530
|
id: ticketId,
|
|
4111
4531
|
depth: 1,
|
|
4112
4532
|
overrideAccess: true
|
|
@@ -4232,8 +4652,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4232
4652
|
let created = 0;
|
|
4233
4653
|
let skipped = 0;
|
|
4234
4654
|
for (const article of KB_ARTICLES) {
|
|
4235
|
-
const existing = await payload.
|
|
4236
|
-
collection: slugs.knowledgeBase,
|
|
4655
|
+
const existing = await dbFind(payload, slugs.knowledgeBase, {
|
|
4237
4656
|
where: { slug: { equals: article.slug } },
|
|
4238
4657
|
limit: 1,
|
|
4239
4658
|
depth: 0,
|
|
@@ -4260,8 +4679,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4260
4679
|
version: 1
|
|
4261
4680
|
}
|
|
4262
4681
|
};
|
|
4263
|
-
await payload.
|
|
4264
|
-
collection: slugs.knowledgeBase,
|
|
4682
|
+
await dbCreate(payload, slugs.knowledgeBase, {
|
|
4265
4683
|
data: {
|
|
4266
4684
|
title: article.title,
|
|
4267
4685
|
slug: article.slug,
|
|
@@ -4316,8 +4734,7 @@ function createLoginEndpoint(slugs) {
|
|
|
4316
4734
|
collection: slugs.supportClients,
|
|
4317
4735
|
data: { email, password }
|
|
4318
4736
|
});
|
|
4319
|
-
payload.
|
|
4320
|
-
collection: slugs.authLogs,
|
|
4737
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4321
4738
|
data: { email, success: true, action: "login", ipAddress: ip, userAgent },
|
|
4322
4739
|
overrideAccess: true
|
|
4323
4740
|
}).catch(() => {
|
|
@@ -4341,12 +4758,14 @@ function createLoginEndpoint(slugs) {
|
|
|
4341
4758
|
);
|
|
4342
4759
|
} catch (err) {
|
|
4343
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
|
+
}
|
|
4344
4764
|
let errorReason = "Identifiants incorrects";
|
|
4345
4765
|
if (errorMessage.includes("locked") || errorMessage.includes("verrouill\xE9") || errorMessage.includes("Too many")) {
|
|
4346
4766
|
errorReason = "Compte verrouill\xE9 (trop de tentatives)";
|
|
4347
4767
|
}
|
|
4348
|
-
payload.
|
|
4349
|
-
collection: slugs.authLogs,
|
|
4768
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4350
4769
|
data: { email, success: false, action: "login", errorReason, ipAddress: ip, userAgent },
|
|
4351
4770
|
overrideAccess: true
|
|
4352
4771
|
}).catch(() => {
|
|
@@ -4367,7 +4786,10 @@ function generateSecureCode() {
|
|
|
4367
4786
|
return String(num);
|
|
4368
4787
|
}
|
|
4369
4788
|
function hashCode(code) {
|
|
4370
|
-
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
|
+
}
|
|
4371
4793
|
return crypto3.createHmac("sha256", secret).update(code).digest("hex");
|
|
4372
4794
|
}
|
|
4373
4795
|
function createAuth2faEndpoint(slugs) {
|
|
@@ -4392,8 +4814,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4392
4814
|
if (sendLimiter.check(email)) {
|
|
4393
4815
|
return Response.json(genericSendResponse);
|
|
4394
4816
|
}
|
|
4395
|
-
const clients = await payload.
|
|
4396
|
-
collection: slugs.supportClients,
|
|
4817
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4397
4818
|
where: { email: { equals: email } },
|
|
4398
4819
|
limit: 1,
|
|
4399
4820
|
depth: 0,
|
|
@@ -4406,8 +4827,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4406
4827
|
const plainCode = generateSecureCode();
|
|
4407
4828
|
const twoFactorCode = hashCode(plainCode);
|
|
4408
4829
|
const twoFactorExpiry = new Date(Date.now() + 10 * 60 * 1e3).toISOString();
|
|
4409
|
-
await payload.
|
|
4410
|
-
collection: slugs.supportClients,
|
|
4830
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4411
4831
|
id: client.id,
|
|
4412
4832
|
data: { twoFactorCode, twoFactorExpiry },
|
|
4413
4833
|
overrideAccess: true
|
|
@@ -4438,8 +4858,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4438
4858
|
{ status: 429 }
|
|
4439
4859
|
);
|
|
4440
4860
|
}
|
|
4441
|
-
const clients = await payload.
|
|
4442
|
-
collection: slugs.supportClients,
|
|
4861
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4443
4862
|
where: { email: { equals: email } },
|
|
4444
4863
|
limit: 1,
|
|
4445
4864
|
depth: 0,
|
|
@@ -4455,8 +4874,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4455
4874
|
return Response.json({ error: "Aucun code en attente" }, { status: 400 });
|
|
4456
4875
|
}
|
|
4457
4876
|
if (/* @__PURE__ */ new Date() > new Date(storedExpiry)) {
|
|
4458
|
-
await payload.
|
|
4459
|
-
collection: slugs.supportClients,
|
|
4877
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4460
4878
|
id: client.id,
|
|
4461
4879
|
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4462
4880
|
overrideAccess: true
|
|
@@ -4470,10 +4888,9 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4470
4888
|
if (submittedBuffer.length !== storedBuffer.length || !crypto3__default.default.timingSafeEqual(submittedBuffer, storedBuffer)) {
|
|
4471
4889
|
return Response.json({ error: "Code incorrect" }, { status: 400 });
|
|
4472
4890
|
}
|
|
4473
|
-
await payload.
|
|
4474
|
-
collection: slugs.supportClients,
|
|
4891
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4475
4892
|
id: client.id,
|
|
4476
|
-
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4893
|
+
data: { twoFactorCode: "", twoFactorExpiry: "", twoFactorVerifiedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4477
4894
|
overrideAccess: true
|
|
4478
4895
|
});
|
|
4479
4896
|
verifyLimiter.reset(email);
|
|
@@ -4547,15 +4964,33 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4547
4964
|
if (!profile.email) {
|
|
4548
4965
|
return Response.json({ error: "no_email" }, { status: 400 });
|
|
4549
4966
|
}
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
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 } },
|
|
4554
4973
|
limit: 1,
|
|
4555
4974
|
depth: 0,
|
|
4556
4975
|
overrideAccess: true
|
|
4557
4976
|
});
|
|
4558
|
-
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
|
+
}
|
|
4559
4994
|
if (!clientDoc) {
|
|
4560
4995
|
const allowedDomains = options?.allowedEmailDomains;
|
|
4561
4996
|
if (allowedDomains && allowedDomains.length > 0) {
|
|
@@ -4573,43 +5008,64 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4573
5008
|
const autoPassword = crypto3__default.default.randomBytes(48).toString("base64url");
|
|
4574
5009
|
const fullName = profile.name || profile.email.split("@")[0];
|
|
4575
5010
|
const nameParts = fullName.split(" ");
|
|
4576
|
-
clientDoc = await payload.
|
|
4577
|
-
collection: slugs.supportClients,
|
|
5011
|
+
clientDoc = await dbCreate(payload$1, slugs.supportClients, {
|
|
4578
5012
|
data: {
|
|
4579
5013
|
email: profile.email,
|
|
4580
5014
|
firstName: nameParts[0] || fullName,
|
|
4581
5015
|
lastName: nameParts.slice(1).join(" ") || "-",
|
|
4582
5016
|
company: fullName,
|
|
5017
|
+
googleId: profile.id,
|
|
4583
5018
|
password: autoPassword
|
|
4584
5019
|
},
|
|
4585
5020
|
overrideAccess: true
|
|
4586
5021
|
});
|
|
4587
5022
|
}
|
|
4588
|
-
const
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
id: clientDoc.id,
|
|
4592
|
-
data: { password: tempPassword },
|
|
4593
|
-
overrideAccess: true
|
|
4594
|
-
});
|
|
4595
|
-
const loginResult = await payload.login({
|
|
4596
|
-
collection: slugs.supportClients,
|
|
4597
|
-
data: { email: profile.email, password: tempPassword }
|
|
4598
|
-
});
|
|
4599
|
-
const postLoginPassword = crypto3__default.default.randomBytes(48).toString("base64url");
|
|
4600
|
-
await payload.update({
|
|
4601
|
-
collection: slugs.supportClients,
|
|
4602
|
-
id: clientDoc.id,
|
|
4603
|
-
data: { password: postLoginPassword },
|
|
4604
|
-
overrideAccess: true
|
|
4605
|
-
});
|
|
4606
|
-
if (!loginResult.token) {
|
|
4607
|
-
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 });
|
|
4608
5026
|
}
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
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
|
|
4613
5069
|
});
|
|
4614
5070
|
}
|
|
4615
5071
|
return Response.json({ error: "Action invalide" }, { status: 400 });
|
|
@@ -4650,8 +5106,7 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4650
5106
|
);
|
|
4651
5107
|
}
|
|
4652
5108
|
const clientId = req.user.id;
|
|
4653
|
-
const tickets = await payload.
|
|
4654
|
-
collection: slugs.tickets,
|
|
5109
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4655
5110
|
where: { client: { equals: clientId } },
|
|
4656
5111
|
limit: 1e4,
|
|
4657
5112
|
depth: 0,
|
|
@@ -4660,39 +5115,32 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4660
5115
|
});
|
|
4661
5116
|
const ticketIds = tickets.docs.map((t) => t.id);
|
|
4662
5117
|
if (ticketIds.length > 0) {
|
|
4663
|
-
await payload.
|
|
4664
|
-
collection: slugs.ticketMessages,
|
|
5118
|
+
await dbDelete(payload, slugs.ticketMessages, {
|
|
4665
5119
|
where: { ticket: { in: ticketIds } },
|
|
4666
5120
|
overrideAccess: true
|
|
4667
5121
|
});
|
|
4668
|
-
await payload.
|
|
4669
|
-
collection: slugs.ticketActivityLog,
|
|
5122
|
+
await dbDelete(payload, slugs.ticketActivityLog, {
|
|
4670
5123
|
where: { ticket: { in: ticketIds } },
|
|
4671
5124
|
overrideAccess: true
|
|
4672
5125
|
});
|
|
4673
|
-
await payload.
|
|
4674
|
-
collection: slugs.timeEntries,
|
|
5126
|
+
await dbDelete(payload, slugs.timeEntries, {
|
|
4675
5127
|
where: { ticket: { in: ticketIds } },
|
|
4676
5128
|
overrideAccess: true
|
|
4677
5129
|
});
|
|
4678
|
-
await payload.
|
|
4679
|
-
collection: slugs.tickets,
|
|
5130
|
+
await dbDelete(payload, slugs.tickets, {
|
|
4680
5131
|
where: { client: { equals: clientId } },
|
|
4681
5132
|
overrideAccess: true
|
|
4682
5133
|
});
|
|
4683
5134
|
}
|
|
4684
|
-
await payload.
|
|
4685
|
-
collection: slugs.satisfactionSurveys,
|
|
5135
|
+
await dbDelete(payload, slugs.satisfactionSurveys, {
|
|
4686
5136
|
where: { client: { equals: clientId } },
|
|
4687
5137
|
overrideAccess: true
|
|
4688
5138
|
});
|
|
4689
|
-
await payload.
|
|
4690
|
-
collection: slugs.chatMessages,
|
|
5139
|
+
await dbDelete(payload, slugs.chatMessages, {
|
|
4691
5140
|
where: { client: { equals: clientId } },
|
|
4692
5141
|
overrideAccess: true
|
|
4693
5142
|
});
|
|
4694
|
-
await payload.
|
|
4695
|
-
collection: slugs.supportClients,
|
|
5143
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4696
5144
|
id: clientId,
|
|
4697
5145
|
overrideAccess: true
|
|
4698
5146
|
});
|
|
@@ -4733,8 +5181,8 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4733
5181
|
return Response.json({ error: "sourceId and targetId are required and must be different" }, { status: 400 });
|
|
4734
5182
|
}
|
|
4735
5183
|
const [source, target] = await Promise.all([
|
|
4736
|
-
payload
|
|
4737
|
-
payload
|
|
5184
|
+
dbFindByID(payload, slugs.supportClients, { id: sourceId, depth: 0, overrideAccess: true }),
|
|
5185
|
+
dbFindByID(payload, slugs.supportClients, { id: targetId, depth: 0, overrideAccess: true })
|
|
4738
5186
|
]);
|
|
4739
5187
|
if (!source || !target) {
|
|
4740
5188
|
return Response.json({ error: "Source or target client not found" }, { status: 404 });
|
|
@@ -4746,63 +5194,57 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4746
5194
|
pendingEmails: 0,
|
|
4747
5195
|
satisfactionSurveys: 0
|
|
4748
5196
|
};
|
|
4749
|
-
const tickets = await payload.
|
|
4750
|
-
collection: slugs.tickets,
|
|
5197
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4751
5198
|
where: { client: { equals: sourceId } },
|
|
4752
5199
|
limit: 500,
|
|
4753
5200
|
depth: 0,
|
|
4754
5201
|
overrideAccess: true
|
|
4755
5202
|
});
|
|
4756
5203
|
for (const ticket of tickets.docs) {
|
|
4757
|
-
await payload
|
|
5204
|
+
await dbUpdate(payload, slugs.tickets, { id: ticket.id, data: { client: targetId }, overrideAccess: true });
|
|
4758
5205
|
results.tickets++;
|
|
4759
5206
|
}
|
|
4760
|
-
const messages = await payload.
|
|
4761
|
-
collection: slugs.ticketMessages,
|
|
5207
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
4762
5208
|
where: { authorClient: { equals: sourceId } },
|
|
4763
5209
|
limit: 1e3,
|
|
4764
5210
|
depth: 0,
|
|
4765
5211
|
overrideAccess: true
|
|
4766
5212
|
});
|
|
4767
5213
|
for (const msg of messages.docs) {
|
|
4768
|
-
await payload
|
|
5214
|
+
await dbUpdate(payload, slugs.ticketMessages, { id: msg.id, data: { authorClient: targetId }, overrideAccess: true });
|
|
4769
5215
|
results.ticketMessages++;
|
|
4770
5216
|
}
|
|
4771
|
-
const chats = await payload.
|
|
4772
|
-
collection: slugs.chatMessages,
|
|
5217
|
+
const chats = await dbFind(payload, slugs.chatMessages, {
|
|
4773
5218
|
where: { client: { equals: sourceId } },
|
|
4774
5219
|
limit: 1e3,
|
|
4775
5220
|
depth: 0,
|
|
4776
5221
|
overrideAccess: true
|
|
4777
5222
|
});
|
|
4778
5223
|
for (const chat of chats.docs) {
|
|
4779
|
-
await payload
|
|
5224
|
+
await dbUpdate(payload, slugs.chatMessages, { id: chat.id, data: { client: targetId }, overrideAccess: true });
|
|
4780
5225
|
results.chatMessages++;
|
|
4781
5226
|
}
|
|
4782
|
-
const pendingEmails = await payload.
|
|
4783
|
-
collection: slugs.pendingEmails,
|
|
5227
|
+
const pendingEmails = await dbFind(payload, slugs.pendingEmails, {
|
|
4784
5228
|
where: { client: { equals: sourceId } },
|
|
4785
5229
|
limit: 500,
|
|
4786
5230
|
depth: 0,
|
|
4787
5231
|
overrideAccess: true
|
|
4788
5232
|
});
|
|
4789
5233
|
for (const pe of pendingEmails.docs) {
|
|
4790
|
-
await payload
|
|
5234
|
+
await dbUpdate(payload, slugs.pendingEmails, { id: pe.id, data: { client: targetId }, overrideAccess: true });
|
|
4791
5235
|
results.pendingEmails++;
|
|
4792
5236
|
}
|
|
4793
|
-
const surveys = await payload.
|
|
4794
|
-
collection: slugs.satisfactionSurveys,
|
|
5237
|
+
const surveys = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
4795
5238
|
where: { client: { equals: sourceId } },
|
|
4796
5239
|
limit: 500,
|
|
4797
5240
|
depth: 0,
|
|
4798
5241
|
overrideAccess: true
|
|
4799
5242
|
});
|
|
4800
5243
|
for (const survey of surveys.docs) {
|
|
4801
|
-
await payload
|
|
5244
|
+
await dbUpdate(payload, slugs.satisfactionSurveys, { id: survey.id, data: { client: targetId }, overrideAccess: true });
|
|
4802
5245
|
results.satisfactionSurveys++;
|
|
4803
5246
|
}
|
|
4804
|
-
await payload.
|
|
4805
|
-
collection: slugs.supportClients,
|
|
5247
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4806
5248
|
id: sourceId,
|
|
4807
5249
|
overrideAccess: true
|
|
4808
5250
|
});
|
|
@@ -4996,8 +5438,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
4996
5438
|
if (blockedEmails.includes(conversation.client.email)) {
|
|
4997
5439
|
return Response.json({ error: "Cannot create ticket from system email address" }, { status: 400 });
|
|
4998
5440
|
}
|
|
4999
|
-
const clientResult = await payload.
|
|
5000
|
-
collection: slugs.supportClients,
|
|
5441
|
+
const clientResult = await dbFind(payload, slugs.supportClients, {
|
|
5001
5442
|
where: { email: { equals: conversation.client.email } },
|
|
5002
5443
|
limit: 1,
|
|
5003
5444
|
depth: 0,
|
|
@@ -5008,8 +5449,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5008
5449
|
if (!client) {
|
|
5009
5450
|
const nameParts = conversation.client.name.split(" ");
|
|
5010
5451
|
const randomPassword = crypto3__default.default.randomBytes(48).toString("base64url");
|
|
5011
|
-
client = await payload.
|
|
5012
|
-
collection: slugs.supportClients,
|
|
5452
|
+
client = await dbCreate(payload, slugs.supportClients, {
|
|
5013
5453
|
data: {
|
|
5014
5454
|
email: conversation.client.email,
|
|
5015
5455
|
password: randomPassword,
|
|
@@ -5021,15 +5461,13 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5021
5461
|
});
|
|
5022
5462
|
isNewClient = true;
|
|
5023
5463
|
}
|
|
5024
|
-
const adminUsers = await payload.
|
|
5025
|
-
collection: slugs.users,
|
|
5464
|
+
const adminUsers = await dbFind(payload, slugs.users, {
|
|
5026
5465
|
limit: 1,
|
|
5027
5466
|
depth: 0,
|
|
5028
5467
|
overrideAccess: true
|
|
5029
5468
|
});
|
|
5030
5469
|
const adminUserId = adminUsers.docs[0]?.id;
|
|
5031
|
-
const ticket = await payload.
|
|
5032
|
-
collection: slugs.tickets,
|
|
5470
|
+
const ticket = await dbCreate(payload, slugs.tickets, {
|
|
5033
5471
|
data: {
|
|
5034
5472
|
subject: conversation.subject,
|
|
5035
5473
|
client: client.id,
|
|
@@ -5042,8 +5480,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5042
5480
|
let importedCount = 0;
|
|
5043
5481
|
for (const msg of conversation.messages) {
|
|
5044
5482
|
const isAdmin = msg.from === "admin";
|
|
5045
|
-
await payload.
|
|
5046
|
-
collection: slugs.ticketMessages,
|
|
5483
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
5047
5484
|
data: {
|
|
5048
5485
|
ticket: ticket.id,
|
|
5049
5486
|
body: msg.content,
|
|
@@ -5079,8 +5516,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5079
5516
|
// src/utils/fireWebhooks.ts
|
|
5080
5517
|
async function fireWebhooks(payload, slugs, event, data) {
|
|
5081
5518
|
try {
|
|
5082
|
-
const endpoints = await payload.
|
|
5083
|
-
collection: slugs.webhookEndpoints,
|
|
5519
|
+
const endpoints = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5084
5520
|
where: {
|
|
5085
5521
|
active: { equals: true },
|
|
5086
5522
|
events: { contains: event }
|
|
@@ -5104,8 +5540,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5104
5540
|
signal: AbortSignal.timeout(1e4)
|
|
5105
5541
|
});
|
|
5106
5542
|
try {
|
|
5107
|
-
await payload.
|
|
5108
|
-
collection: slugs.webhookEndpoints,
|
|
5543
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5109
5544
|
id: endpoint.id,
|
|
5110
5545
|
data: {
|
|
5111
5546
|
lastTriggeredAt: timestamp,
|
|
@@ -5118,8 +5553,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5118
5553
|
} catch (error) {
|
|
5119
5554
|
console.warn(`[support] Webhook delivery failed: ${endpoint.url}`, error);
|
|
5120
5555
|
try {
|
|
5121
|
-
await payload.
|
|
5122
|
-
collection: slugs.webhookEndpoints,
|
|
5556
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5123
5557
|
id: endpoint.id,
|
|
5124
5558
|
data: {
|
|
5125
5559
|
lastTriggeredAt: timestamp,
|
|
@@ -5154,8 +5588,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5154
5588
|
const settings = await readSupportSettings(payload);
|
|
5155
5589
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
|
|
5156
5590
|
const results = { processed: 0, errors: 0 };
|
|
5157
|
-
const scheduled = await payload.
|
|
5158
|
-
collection: slugs.ticketMessages,
|
|
5591
|
+
const scheduled = await dbFind(payload, slugs.ticketMessages, {
|
|
5159
5592
|
where: {
|
|
5160
5593
|
and: [
|
|
5161
5594
|
{ scheduledAt: { less_than_equal: now.toISOString() } },
|
|
@@ -5175,8 +5608,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5175
5608
|
try {
|
|
5176
5609
|
const msg = message;
|
|
5177
5610
|
const ticketId = typeof msg.ticket === "object" ? msg.ticket.id : msg.ticket;
|
|
5178
|
-
const ticket = await payload.
|
|
5179
|
-
collection: slugs.tickets,
|
|
5611
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5180
5612
|
id: ticketId,
|
|
5181
5613
|
depth: 1,
|
|
5182
5614
|
overrideAccess: true
|
|
@@ -5199,8 +5631,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5199
5631
|
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
5200
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>`
|
|
5201
5633
|
});
|
|
5202
|
-
await payload.
|
|
5203
|
-
collection: slugs.ticketMessages,
|
|
5634
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5204
5635
|
id: msg.id,
|
|
5205
5636
|
data: {
|
|
5206
5637
|
scheduledSent: true,
|
|
@@ -5210,16 +5641,14 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5210
5641
|
overrideAccess: true
|
|
5211
5642
|
});
|
|
5212
5643
|
} else {
|
|
5213
|
-
await payload.
|
|
5214
|
-
collection: slugs.ticketMessages,
|
|
5644
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5215
5645
|
id: msg.id,
|
|
5216
5646
|
data: { scheduledSent: true },
|
|
5217
5647
|
overrideAccess: true
|
|
5218
5648
|
});
|
|
5219
5649
|
}
|
|
5220
5650
|
if (msg.authorType === "admin" && !msg.isInternal) {
|
|
5221
|
-
await payload.
|
|
5222
|
-
collection: slugs.tickets,
|
|
5651
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
5223
5652
|
id: ticketId,
|
|
5224
5653
|
data: { status: "waiting_client" },
|
|
5225
5654
|
overrideAccess: true
|
|
@@ -5251,6 +5680,208 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5251
5680
|
};
|
|
5252
5681
|
}
|
|
5253
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
|
+
|
|
5254
5885
|
// src/endpoints/user-prefs.ts
|
|
5255
5886
|
var PREF_KEY_PREFIX = "support-user-prefs";
|
|
5256
5887
|
var DEFAULT_USER_PREFS2 = {
|
|
@@ -5266,8 +5897,7 @@ function createUserPrefsGetEndpoint(slugs) {
|
|
|
5266
5897
|
const payload = req.payload;
|
|
5267
5898
|
requireAdmin(req, slugs);
|
|
5268
5899
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5269
|
-
const prefs = await payload
|
|
5270
|
-
collection: "payload-preferences",
|
|
5900
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
5271
5901
|
where: { key: { equals: key } },
|
|
5272
5902
|
limit: 1,
|
|
5273
5903
|
depth: 0,
|
|
@@ -5301,8 +5931,7 @@ function createUserPrefsPostEndpoint(slugs) {
|
|
|
5301
5931
|
requireAdmin(req, slugs);
|
|
5302
5932
|
const body = await req.json();
|
|
5303
5933
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5304
|
-
const existing = await payload
|
|
5305
|
-
collection: "payload-preferences",
|
|
5934
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
5306
5935
|
where: { key: { equals: key } },
|
|
5307
5936
|
limit: 1,
|
|
5308
5937
|
depth: 0,
|
|
@@ -5363,8 +5992,7 @@ function createEscalateEndpoint(slugs) {
|
|
|
5363
5992
|
if (Number.isNaN(ticketId)) {
|
|
5364
5993
|
return Response.json({ error: "invalid-id" }, { status: 400 });
|
|
5365
5994
|
}
|
|
5366
|
-
const ticket = await req.payload.
|
|
5367
|
-
collection: slugs.tickets,
|
|
5995
|
+
const ticket = await dbUpdate(req.payload, slugs.tickets, {
|
|
5368
5996
|
id: ticketId,
|
|
5369
5997
|
data: { status: "escalated" },
|
|
5370
5998
|
overrideAccess: true
|
|
@@ -5412,8 +6040,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5412
6040
|
{ status: 400 }
|
|
5413
6041
|
);
|
|
5414
6042
|
}
|
|
5415
|
-
const ticket = await payload.
|
|
5416
|
-
collection: slugs.tickets,
|
|
6043
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5417
6044
|
id: ticketId,
|
|
5418
6045
|
depth: 0,
|
|
5419
6046
|
overrideAccess: true
|
|
@@ -5425,8 +6052,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5425
6052
|
if (ticketClientId !== req.user.id) {
|
|
5426
6053
|
return Response.json({ error: "forbidden" }, { status: 403 });
|
|
5427
6054
|
}
|
|
5428
|
-
const existing = await payload
|
|
5429
|
-
collection: "ticket-feedback",
|
|
6055
|
+
const existing = await dbFind(payload, "ticket-feedback", {
|
|
5430
6056
|
where: { ticket: { equals: ticketId } },
|
|
5431
6057
|
limit: 1,
|
|
5432
6058
|
depth: 0,
|
|
@@ -5438,8 +6064,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5438
6064
|
{ status: 409 }
|
|
5439
6065
|
);
|
|
5440
6066
|
}
|
|
5441
|
-
const feedback = await payload
|
|
5442
|
-
collection: "ticket-feedback",
|
|
6067
|
+
const feedback = await dbCreate(payload, "ticket-feedback", {
|
|
5443
6068
|
data: {
|
|
5444
6069
|
ticket: ticketId,
|
|
5445
6070
|
client: req.user.id,
|
|
@@ -5463,6 +6088,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5463
6088
|
// src/endpoints/transfer-ticket.ts
|
|
5464
6089
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5465
6090
|
var MAX_TRANSFERS_PER_DAY = 5;
|
|
6091
|
+
var transferLimiter = new RateLimiter(24 * 60 * 60 * 1e3, MAX_TRANSFERS_PER_DAY);
|
|
5466
6092
|
function createTransferTicketEndpoint(slugs) {
|
|
5467
6093
|
return {
|
|
5468
6094
|
path: "/support/tickets/:id/transfer",
|
|
@@ -5487,8 +6113,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5487
6113
|
const includeAttachments = body.includeAttachments !== false;
|
|
5488
6114
|
const customMessage = typeof body.message === "string" ? body.message.slice(0, 1e3) : "";
|
|
5489
6115
|
const payload = req.payload;
|
|
5490
|
-
const ticket = await payload.
|
|
5491
|
-
collection: slugs.tickets,
|
|
6116
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5492
6117
|
id: ticketId,
|
|
5493
6118
|
depth: 1,
|
|
5494
6119
|
overrideAccess: true
|
|
@@ -5499,10 +6124,15 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5499
6124
|
if (!isAdmin && !isOwner) {
|
|
5500
6125
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5501
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
|
+
}
|
|
5502
6133
|
try {
|
|
5503
6134
|
const since = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
5504
|
-
const existing = await payload.
|
|
5505
|
-
collection: slugs.emailLogs,
|
|
6135
|
+
const existing = await dbCount(payload, slugs.emailLogs, {
|
|
5506
6136
|
where: {
|
|
5507
6137
|
and: [
|
|
5508
6138
|
{ action: { equals: "transfer" } },
|
|
@@ -5520,8 +6150,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5520
6150
|
}
|
|
5521
6151
|
} catch {
|
|
5522
6152
|
}
|
|
5523
|
-
const messages = await payload.
|
|
5524
|
-
collection: slugs.ticketMessages,
|
|
6153
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
5525
6154
|
where: {
|
|
5526
6155
|
and: [
|
|
5527
6156
|
{ ticket: { equals: ticketId } },
|
|
@@ -5601,8 +6230,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5601
6230
|
html
|
|
5602
6231
|
});
|
|
5603
6232
|
try {
|
|
5604
|
-
await payload.
|
|
5605
|
-
collection: slugs.emailLogs,
|
|
6233
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5606
6234
|
data: {
|
|
5607
6235
|
status: "success",
|
|
5608
6236
|
action: "transfer",
|
|
@@ -5642,6 +6270,8 @@ function statusToLabel(status) {
|
|
|
5642
6270
|
}
|
|
5643
6271
|
}
|
|
5644
6272
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6273
|
+
var MAX_COLLABORATORS_PER_TICKET = 20;
|
|
6274
|
+
var inviteLimiter = new RateLimiter(60 * 60 * 1e3, 10);
|
|
5645
6275
|
function createInviteCollaboratorEndpoint(slugs) {
|
|
5646
6276
|
return {
|
|
5647
6277
|
path: "/support/tickets/:id/invite",
|
|
@@ -5665,8 +6295,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5665
6295
|
}
|
|
5666
6296
|
const role = body.role === "collaborator" ? "collaborator" : "viewer";
|
|
5667
6297
|
const payload = req.payload;
|
|
5668
|
-
const ticket = await payload.
|
|
5669
|
-
collection: slugs.tickets,
|
|
6298
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5670
6299
|
id: ticketId,
|
|
5671
6300
|
depth: 1,
|
|
5672
6301
|
overrideAccess: true
|
|
@@ -5678,13 +6307,22 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5678
6307
|
if (!isAdmin && !isOwner) {
|
|
5679
6308
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5680
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
|
+
}
|
|
5681
6320
|
const ownerEmail = typeof ticket.client === "object" ? ticket.client?.email : void 0;
|
|
5682
6321
|
if (ownerEmail && ownerEmail.toLowerCase() === email) {
|
|
5683
6322
|
return Response.json({ error: "Ce client est d\xE9j\xE0 propri\xE9taire du ticket" }, { status: 400 });
|
|
5684
6323
|
}
|
|
5685
6324
|
let inviteeId = null;
|
|
5686
|
-
const existing = await payload.
|
|
5687
|
-
collection: slugs.supportClients,
|
|
6325
|
+
const existing = await dbFind(payload, slugs.supportClients, {
|
|
5688
6326
|
where: { email: { equals: email } },
|
|
5689
6327
|
limit: 1,
|
|
5690
6328
|
depth: 0,
|
|
@@ -5694,8 +6332,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5694
6332
|
inviteeId = existing.docs[0].id;
|
|
5695
6333
|
} else {
|
|
5696
6334
|
const tempPassword = crypto3.randomBytes(16).toString("hex");
|
|
5697
|
-
const created = await payload.
|
|
5698
|
-
collection: slugs.supportClients,
|
|
6335
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
5699
6336
|
data: {
|
|
5700
6337
|
email,
|
|
5701
6338
|
firstName: "Invit\xE9",
|
|
@@ -5712,8 +6349,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5712
6349
|
if (!inviteeId) {
|
|
5713
6350
|
return Response.json({ error: "Impossible de cr\xE9er le client invit\xE9" }, { status: 500 });
|
|
5714
6351
|
}
|
|
5715
|
-
const dupe = await payload
|
|
5716
|
-
collection: "ticket-collaborators",
|
|
6352
|
+
const dupe = await dbFind(payload, "ticket-collaborators", {
|
|
5717
6353
|
where: {
|
|
5718
6354
|
and: [
|
|
5719
6355
|
{ ticket: { equals: ticketId } },
|
|
@@ -5726,15 +6362,13 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5726
6362
|
});
|
|
5727
6363
|
const invitationToken = crypto3.randomBytes(24).toString("hex");
|
|
5728
6364
|
if (dupe.docs.length > 0) {
|
|
5729
|
-
await payload
|
|
5730
|
-
collection: "ticket-collaborators",
|
|
6365
|
+
await dbUpdate(payload, "ticket-collaborators", {
|
|
5731
6366
|
id: dupe.docs[0].id,
|
|
5732
6367
|
data: { role, invitationToken },
|
|
5733
6368
|
overrideAccess: true
|
|
5734
6369
|
});
|
|
5735
6370
|
} else {
|
|
5736
|
-
await payload
|
|
5737
|
-
collection: "ticket-collaborators",
|
|
6371
|
+
await dbCreate(payload, "ticket-collaborators", {
|
|
5738
6372
|
data: {
|
|
5739
6373
|
ticket: ticketId,
|
|
5740
6374
|
client: inviteeId,
|
|
@@ -5777,8 +6411,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5777
6411
|
)
|
|
5778
6412
|
});
|
|
5779
6413
|
try {
|
|
5780
|
-
await payload.
|
|
5781
|
-
collection: slugs.emailLogs,
|
|
6414
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5782
6415
|
data: {
|
|
5783
6416
|
status: "success",
|
|
5784
6417
|
action: "invite",
|
|
@@ -5831,6 +6464,7 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5831
6464
|
];
|
|
5832
6465
|
if (!f || f.ai !== false) {
|
|
5833
6466
|
endpoints.push(createAiEndpoint(slugs));
|
|
6467
|
+
endpoints.push(createAiAgentEndpoint(slugs));
|
|
5834
6468
|
endpoints.push(...createClientIntelligenceEndpoint(slugs));
|
|
5835
6469
|
endpoints.push(createTicketSynthesisEndpoint(slugs));
|
|
5836
6470
|
}
|
|
@@ -5845,7 +6479,10 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5845
6479
|
endpoints.push(createSignatureGetEndpoint(slugs), createSignaturePostEndpoint(slugs));
|
|
5846
6480
|
}
|
|
5847
6481
|
if (!f || f.sla !== false) endpoints.push(createSlaCheckEndpoint(slugs));
|
|
5848
|
-
if (!f || f.autoClose !== false)
|
|
6482
|
+
if (!f || f.autoClose !== false) {
|
|
6483
|
+
endpoints.push(createAutoCloseEndpoint(slugs));
|
|
6484
|
+
endpoints.push(createSendReminderEndpoint(slugs));
|
|
6485
|
+
}
|
|
5849
6486
|
if (!f || f.customStatuses !== false) endpoints.push(createStatusesEndpoint(slugs));
|
|
5850
6487
|
if (!f || f.macros !== false) endpoints.push(createApplyMacroEndpoint(slugs));
|
|
5851
6488
|
if (!f || f.roundRobin !== false) {
|
|
@@ -5858,21 +6495,26 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5858
6495
|
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs));
|
|
5859
6496
|
endpoints.push(createAdminChatStreamEndpoint(slugs));
|
|
5860
6497
|
}
|
|
5861
|
-
if (!f || f.timeTracking !== false)
|
|
6498
|
+
if (!f || f.timeTracking !== false) {
|
|
6499
|
+
endpoints.push(createBillingEndpoint(slugs));
|
|
6500
|
+
endpoints.push(createInvoiceEndpoint(slugs));
|
|
6501
|
+
}
|
|
5862
6502
|
if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs));
|
|
5863
6503
|
if (!f || f.emailTracking !== false) {
|
|
5864
6504
|
endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs));
|
|
5865
6505
|
}
|
|
5866
6506
|
if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs));
|
|
5867
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));
|
|
5868
6511
|
return endpoints;
|
|
5869
6512
|
}
|
|
5870
6513
|
|
|
5871
6514
|
// src/utils/adminNotification.ts
|
|
5872
6515
|
async function createAdminNotification(payload, data, collectionSlug = "admin-notifications") {
|
|
5873
6516
|
try {
|
|
5874
|
-
await payload
|
|
5875
|
-
collection: collectionSlug,
|
|
6517
|
+
await dbCreate(payload, collectionSlug, {
|
|
5876
6518
|
data: {
|
|
5877
6519
|
title: data.title,
|
|
5878
6520
|
message: data.message,
|
|
@@ -5892,8 +6534,7 @@ function dispatchWebhook(data, event, payload, slugs) {
|
|
|
5892
6534
|
}
|
|
5893
6535
|
async function _dispatch(data, event, payload, slugs) {
|
|
5894
6536
|
try {
|
|
5895
|
-
const { docs: endpoints } = await payload.
|
|
5896
|
-
collection: slugs.webhookEndpoints,
|
|
6537
|
+
const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5897
6538
|
where: {
|
|
5898
6539
|
and: [
|
|
5899
6540
|
{ active: { equals: true } },
|
|
@@ -5930,8 +6571,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
5930
6571
|
signal: AbortSignal.timeout(1e4)
|
|
5931
6572
|
// 10s timeout
|
|
5932
6573
|
});
|
|
5933
|
-
await payload.
|
|
5934
|
-
collection: slugs.webhookEndpoints,
|
|
6574
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5935
6575
|
id: endpoint.id,
|
|
5936
6576
|
data: {
|
|
5937
6577
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5945,8 +6585,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
5945
6585
|
} catch (err) {
|
|
5946
6586
|
console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
|
|
5947
6587
|
try {
|
|
5948
|
-
await payload.
|
|
5949
|
-
collection: slugs.webhookEndpoints,
|
|
6588
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5950
6589
|
id: endpoint.id,
|
|
5951
6590
|
data: {
|
|
5952
6591
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6180,8 +6819,7 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6180
6819
|
if (typeof policyRef === "object" && policyRef.firstResponseTime) {
|
|
6181
6820
|
return policyRef;
|
|
6182
6821
|
}
|
|
6183
|
-
return await payload.
|
|
6184
|
-
collection: slugs.slaPolicies,
|
|
6822
|
+
return await dbFindByID(payload, slugs.slaPolicies, {
|
|
6185
6823
|
id: policyId,
|
|
6186
6824
|
depth: 0,
|
|
6187
6825
|
overrideAccess: true
|
|
@@ -6192,8 +6830,7 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6192
6830
|
}
|
|
6193
6831
|
const priority = field(ticket, "priority") || "normal";
|
|
6194
6832
|
try {
|
|
6195
|
-
const defaults = await payload.
|
|
6196
|
-
collection: slugs.slaPolicies,
|
|
6833
|
+
const defaults = await dbFind(payload, slugs.slaPolicies, {
|
|
6197
6834
|
where: {
|
|
6198
6835
|
and: [
|
|
6199
6836
|
{ isDefault: { equals: true } },
|
|
@@ -6230,8 +6867,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6230
6867
|
if (typeof policyRef === "object" && policyRef.escalateOnBreach !== void 0) {
|
|
6231
6868
|
policy = policyRef;
|
|
6232
6869
|
} else {
|
|
6233
|
-
policy = await payload.
|
|
6234
|
-
collection: slugs.slaPolicies,
|
|
6870
|
+
policy = await dbFindByID(payload, slugs.slaPolicies, {
|
|
6235
6871
|
id: policyId,
|
|
6236
6872
|
depth: 0,
|
|
6237
6873
|
overrideAccess: true
|
|
@@ -6244,8 +6880,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6244
6880
|
try {
|
|
6245
6881
|
const escalateToId = typeof policy.escalateTo === "object" ? policy.escalateTo.id : policy.escalateTo;
|
|
6246
6882
|
if (!escalateToId) return;
|
|
6247
|
-
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await payload.
|
|
6248
|
-
collection: slugs.users,
|
|
6883
|
+
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await dbFindByID(payload, slugs.users, {
|
|
6249
6884
|
id: escalateToId,
|
|
6250
6885
|
depth: 0,
|
|
6251
6886
|
overrideAccess: true
|
|
@@ -6293,8 +6928,7 @@ function createAssignSlaDeadlines(slugs, notificationSlug = "admin-notifications
|
|
|
6293
6928
|
if (!field(doc, "slaPolicy") && policy.id) {
|
|
6294
6929
|
updateData.slaPolicy = policy.id;
|
|
6295
6930
|
}
|
|
6296
|
-
await payload.
|
|
6297
|
-
collection: slugs.tickets,
|
|
6931
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6298
6932
|
id: doc.id,
|
|
6299
6933
|
data: updateData,
|
|
6300
6934
|
overrideAccess: true
|
|
@@ -6317,8 +6951,7 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6317
6951
|
const now = /* @__PURE__ */ new Date();
|
|
6318
6952
|
const deadline = new Date(doc.slaResolutionDue);
|
|
6319
6953
|
const breached = now > deadline;
|
|
6320
|
-
await payload.
|
|
6321
|
-
collection: slugs.tickets,
|
|
6954
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6322
6955
|
id: doc.id,
|
|
6323
6956
|
data: { slaResolutionBreached: breached },
|
|
6324
6957
|
overrideAccess: true
|
|
@@ -6332,6 +6965,35 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6332
6965
|
return doc;
|
|
6333
6966
|
};
|
|
6334
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
|
+
}
|
|
6335
6997
|
function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications") {
|
|
6336
6998
|
return async ({ doc, operation, req }) => {
|
|
6337
6999
|
if (operation !== "create") return doc;
|
|
@@ -6339,8 +7001,7 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6339
7001
|
try {
|
|
6340
7002
|
const { payload } = req;
|
|
6341
7003
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
6342
|
-
const ticket = await payload.
|
|
6343
|
-
collection: slugs.tickets,
|
|
7004
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
6344
7005
|
id: ticketId,
|
|
6345
7006
|
depth: 1,
|
|
6346
7007
|
overrideAccess: true
|
|
@@ -6351,8 +7012,7 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6351
7012
|
const now = /* @__PURE__ */ new Date();
|
|
6352
7013
|
const deadline = new Date(ticket.slaFirstResponseDue);
|
|
6353
7014
|
const breached = now > deadline;
|
|
6354
|
-
await payload.
|
|
6355
|
-
collection: slugs.tickets,
|
|
7015
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6356
7016
|
id: ticketId,
|
|
6357
7017
|
data: { slaFirstResponseBreached: breached },
|
|
6358
7018
|
overrideAccess: true
|
|
@@ -6367,6 +7027,108 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6367
7027
|
};
|
|
6368
7028
|
}
|
|
6369
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
|
+
|
|
6370
7132
|
// src/collections/Tickets.ts
|
|
6371
7133
|
function createAssignTicketNumber(slugs) {
|
|
6372
7134
|
return async ({ data, operation, req }) => {
|
|
@@ -6629,56 +7391,6 @@ function createNotifyClientOnResolve(slugs) {
|
|
|
6629
7391
|
return doc;
|
|
6630
7392
|
};
|
|
6631
7393
|
}
|
|
6632
|
-
function createAutoCalculateSLA(slugs) {
|
|
6633
|
-
return async ({ doc, operation, req }) => {
|
|
6634
|
-
if (operation !== "create") return doc;
|
|
6635
|
-
try {
|
|
6636
|
-
const { payload } = req;
|
|
6637
|
-
const ticketPriority = doc.priority || "normal";
|
|
6638
|
-
let policy = null;
|
|
6639
|
-
const byPriority = await payload.find({
|
|
6640
|
-
collection: slugs.slaPolicies,
|
|
6641
|
-
where: { priority: { equals: ticketPriority } },
|
|
6642
|
-
limit: 1,
|
|
6643
|
-
depth: 0,
|
|
6644
|
-
overrideAccess: true
|
|
6645
|
-
});
|
|
6646
|
-
if (byPriority.docs.length > 0) {
|
|
6647
|
-
policy = byPriority.docs[0];
|
|
6648
|
-
} else {
|
|
6649
|
-
const defaults = await payload.find({
|
|
6650
|
-
collection: slugs.slaPolicies,
|
|
6651
|
-
where: { isDefault: { equals: true } },
|
|
6652
|
-
limit: 1,
|
|
6653
|
-
depth: 0,
|
|
6654
|
-
overrideAccess: true
|
|
6655
|
-
});
|
|
6656
|
-
if (defaults.docs.length > 0) {
|
|
6657
|
-
policy = defaults.docs[0];
|
|
6658
|
-
}
|
|
6659
|
-
}
|
|
6660
|
-
if (!policy) return doc;
|
|
6661
|
-
const now = /* @__PURE__ */ new Date();
|
|
6662
|
-
const firstResponseMinutes = policy.firstResponseTime || 240;
|
|
6663
|
-
const resolutionMinutes = policy.resolutionTime || 1440;
|
|
6664
|
-
const firstResponseDue = new Date(now.getTime() + firstResponseMinutes * 6e4);
|
|
6665
|
-
const resolutionDue = new Date(now.getTime() + resolutionMinutes * 6e4);
|
|
6666
|
-
await payload.update({
|
|
6667
|
-
collection: slugs.tickets,
|
|
6668
|
-
id: doc.id,
|
|
6669
|
-
data: {
|
|
6670
|
-
slaPolicy: policy.id,
|
|
6671
|
-
slaFirstResponseDue: firstResponseDue.toISOString(),
|
|
6672
|
-
slaResolutionDue: resolutionDue.toISOString()
|
|
6673
|
-
},
|
|
6674
|
-
overrideAccess: true
|
|
6675
|
-
});
|
|
6676
|
-
} catch (err) {
|
|
6677
|
-
console.error("[support] Failed to auto-calculate SLA:", err);
|
|
6678
|
-
}
|
|
6679
|
-
return doc;
|
|
6680
|
-
};
|
|
6681
|
-
}
|
|
6682
7394
|
function createFireTicketWebhooks(slugs) {
|
|
6683
7395
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
6684
7396
|
const { payload } = req;
|
|
@@ -6770,8 +7482,7 @@ function createCascadeDelete(slugs) {
|
|
|
6770
7482
|
return async ({ id, req }) => {
|
|
6771
7483
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys];
|
|
6772
7484
|
for (const slug of collections) {
|
|
6773
|
-
await req.payload
|
|
6774
|
-
collection: slug,
|
|
7485
|
+
await dbDelete(req.payload, slug, {
|
|
6775
7486
|
where: { ticket: { equals: id } },
|
|
6776
7487
|
overrideAccess: true
|
|
6777
7488
|
});
|
|
@@ -6896,6 +7607,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
6896
7607
|
{
|
|
6897
7608
|
name: "status",
|
|
6898
7609
|
type: "select",
|
|
7610
|
+
index: true,
|
|
6899
7611
|
defaultValue: "open",
|
|
6900
7612
|
label: "Statut",
|
|
6901
7613
|
options: [
|
|
@@ -6910,6 +7622,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
6910
7622
|
{
|
|
6911
7623
|
name: "priority",
|
|
6912
7624
|
type: "select",
|
|
7625
|
+
index: true,
|
|
6913
7626
|
defaultValue: "normal",
|
|
6914
7627
|
label: "Priorite",
|
|
6915
7628
|
options: [
|
|
@@ -6925,7 +7638,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
6925
7638
|
{
|
|
6926
7639
|
type: "row",
|
|
6927
7640
|
fields: [
|
|
6928
|
-
{ 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%" } }
|
|
6929
7642
|
]
|
|
6930
7643
|
},
|
|
6931
7644
|
// Billing collapsible
|
|
@@ -6994,15 +7707,16 @@ function createTicketsCollection(slugs, options) {
|
|
|
6994
7707
|
{
|
|
6995
7708
|
type: "row",
|
|
6996
7709
|
fields: [
|
|
6997
|
-
{ name: "slaFirstResponseDue", type: "date", label: "Echeance 1ere reponse (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
6998
|
-
{ 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" } } }
|
|
6999
7712
|
]
|
|
7000
7713
|
},
|
|
7001
7714
|
{
|
|
7002
7715
|
type: "row",
|
|
7003
7716
|
fields: [
|
|
7004
7717
|
{ name: "slaFirstResponseBreached", type: "checkbox", label: "1ere reponse depassee", admin: { readOnly: true, width: "50%" } },
|
|
7005
|
-
{ 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 } }
|
|
7006
7720
|
]
|
|
7007
7721
|
}
|
|
7008
7722
|
]
|
|
@@ -7023,7 +7737,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7023
7737
|
]
|
|
7024
7738
|
},
|
|
7025
7739
|
{ name: "mergedInto", type: "relationship", relationTo: slugs.tickets, label: "Fusionne dans", admin: { readOnly: true } },
|
|
7026
|
-
{ name: "autoCloseRemindedAt", type: "date", label: "Rappel auto-close envoye", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
7740
|
+
{ name: "autoCloseRemindedAt", type: "date", label: "Rappel auto-close envoye", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
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." } }
|
|
7027
7742
|
]
|
|
7028
7743
|
},
|
|
7029
7744
|
// Sidebar
|
|
@@ -7032,6 +7747,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7032
7747
|
{
|
|
7033
7748
|
name: "category",
|
|
7034
7749
|
type: "select",
|
|
7750
|
+
index: true,
|
|
7035
7751
|
label: "Categorie",
|
|
7036
7752
|
options: [
|
|
7037
7753
|
{ label: "Bug / Dysfonctionnement", value: "bug" },
|
|
@@ -7042,7 +7758,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7042
7758
|
],
|
|
7043
7759
|
admin: { position: "sidebar" }
|
|
7044
7760
|
},
|
|
7045
|
-
{ 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" } },
|
|
7046
7763
|
{
|
|
7047
7764
|
name: "source",
|
|
7048
7765
|
type: "select",
|
|
@@ -7052,7 +7769,9 @@ function createTicketsCollection(slugs, options) {
|
|
|
7052
7769
|
{ label: "Portail client", value: "portal" },
|
|
7053
7770
|
{ label: "Chat en direct", value: "live-chat" },
|
|
7054
7771
|
{ label: "Email", value: "email" },
|
|
7055
|
-
{ label: "Admin", value: "admin" }
|
|
7772
|
+
{ label: "Admin", value: "admin" },
|
|
7773
|
+
{ label: "WhatsApp", value: "whatsapp" },
|
|
7774
|
+
{ label: "Messenger", value: "messenger" }
|
|
7056
7775
|
],
|
|
7057
7776
|
admin: { position: "sidebar" }
|
|
7058
7777
|
},
|
|
@@ -7103,8 +7822,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7103
7822
|
afterChange: [
|
|
7104
7823
|
createTrackSLA(slugs),
|
|
7105
7824
|
createTrackAiSummaryOnResolve(slugs),
|
|
7106
|
-
createAutoCalculateSLA(slugs),
|
|
7107
7825
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
7826
|
+
createPauseSlaOnHold(slugs),
|
|
7108
7827
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
7109
7828
|
createLogTicketActivity(slugs),
|
|
7110
7829
|
createNotifyOnAssignment(slugs),
|
|
@@ -7112,7 +7831,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7112
7831
|
createTicketStatusEmail(slugs),
|
|
7113
7832
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
7114
7833
|
createFireTicketWebhooks(slugs),
|
|
7115
|
-
createDispatchWebhookOnTicket(slugs)
|
|
7834
|
+
createDispatchWebhookOnTicket(slugs),
|
|
7835
|
+
createApplyAutomationRules(slugs)
|
|
7116
7836
|
],
|
|
7117
7837
|
beforeDelete: [createCascadeDelete(slugs)]
|
|
7118
7838
|
},
|
|
@@ -7123,12 +7843,20 @@ function createTicketsCollection(slugs, options) {
|
|
|
7123
7843
|
return false;
|
|
7124
7844
|
},
|
|
7125
7845
|
read: async ({ req }) => {
|
|
7126
|
-
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
|
+
}
|
|
7127
7856
|
if (req.user?.collection === slugs.supportClients) {
|
|
7128
7857
|
let collabTicketIds = [];
|
|
7129
7858
|
try {
|
|
7130
|
-
const rows = await req.payload
|
|
7131
|
-
collection: "ticket-collaborators",
|
|
7859
|
+
const rows = await dbFind(req.payload, "ticket-collaborators", {
|
|
7132
7860
|
where: { client: { equals: req.user.id } },
|
|
7133
7861
|
limit: 1e3,
|
|
7134
7862
|
depth: 0,
|
|
@@ -7163,6 +7891,125 @@ function createTicketsCollection(slugs, options) {
|
|
|
7163
7891
|
};
|
|
7164
7892
|
}
|
|
7165
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
|
+
|
|
7166
8013
|
// src/collections/TicketMessages.ts
|
|
7167
8014
|
function createAssignAuthor(slugs) {
|
|
7168
8015
|
return async ({ data, operation, req }) => {
|
|
@@ -7186,8 +8033,12 @@ function createAutoUpdateStatus(slugs) {
|
|
|
7186
8033
|
if (!doc.isInternal) {
|
|
7187
8034
|
if (doc.authorType === "admin") {
|
|
7188
8035
|
updateData.status = "waiting_client";
|
|
8036
|
+
updateData.autoCloseScheduledAt = null;
|
|
8037
|
+
updateData.autoCloseRemindedAt = null;
|
|
7189
8038
|
} else if (doc.authorType === "client" || doc.authorType === "email") {
|
|
7190
8039
|
updateData.lastClientMessageAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8040
|
+
updateData.autoCloseScheduledAt = null;
|
|
8041
|
+
updateData.autoCloseRemindedAt = null;
|
|
7191
8042
|
if (ticket.status && ["waiting_client", "resolved"].includes(ticket.status)) {
|
|
7192
8043
|
updateData.status = "open";
|
|
7193
8044
|
}
|
|
@@ -7221,29 +8072,46 @@ function createNotifyClient(slugs) {
|
|
|
7221
8072
|
const internalBcc = process.env.SUPPORT_CLIENT_BCC || void 0;
|
|
7222
8073
|
const rawContent = doc.bodyHtml ? emailRichContent(doc.bodyHtml) : emailQuote(doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body);
|
|
7223
8074
|
const replyPreview = (doc.body || "").replace(/\s+/g, " ").trim().slice(0, 90);
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
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
|
+
})();
|
|
7247
8115
|
} catch (err) {
|
|
7248
8116
|
console.error("[support] Failed to notify client:", err);
|
|
7249
8117
|
}
|
|
@@ -7281,8 +8149,7 @@ function createSyncTicketReplyToChat(slugs) {
|
|
|
7281
8149
|
if (!ticket?.chatSession) return doc;
|
|
7282
8150
|
if (doc.skipNotification) return doc;
|
|
7283
8151
|
const clientId = typeof ticket.client === "object" ? ticket.client.id : ticket.client;
|
|
7284
|
-
await payload.
|
|
7285
|
-
collection: slugs.chatMessages,
|
|
8152
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
7286
8153
|
data: {
|
|
7287
8154
|
session: ticket.chatSession,
|
|
7288
8155
|
client: clientId,
|
|
@@ -7399,6 +8266,57 @@ function createDispatchWebhookOnReply(slugs) {
|
|
|
7399
8266
|
return doc;
|
|
7400
8267
|
};
|
|
7401
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
|
+
}
|
|
7402
8320
|
function createTicketMessagesCollection(slugs, options) {
|
|
7403
8321
|
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
7404
8322
|
return {
|
|
@@ -7406,7 +8324,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7406
8324
|
labels: { singular: "Message", plural: "Messages" },
|
|
7407
8325
|
admin: { hidden: true, group: "Support", defaultColumns: ["ticket", "authorType", "createdAt"] },
|
|
7408
8326
|
fields: [
|
|
7409
|
-
{ 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" },
|
|
7410
8328
|
{ name: "body", type: "textarea", required: true, label: "Message" },
|
|
7411
8329
|
{ name: "bodyHtml", type: "textarea", label: "Message HTML", admin: { hidden: true } },
|
|
7412
8330
|
{
|
|
@@ -7415,6 +8333,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7415
8333
|
{
|
|
7416
8334
|
name: "authorType",
|
|
7417
8335
|
type: "select",
|
|
8336
|
+
index: true,
|
|
7418
8337
|
label: "Type d'auteur",
|
|
7419
8338
|
defaultValue: "admin",
|
|
7420
8339
|
options: [
|
|
@@ -7450,8 +8369,9 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7450
8369
|
},
|
|
7451
8370
|
{ name: "isInternal", type: "checkbox", defaultValue: false, label: "Note interne", admin: { position: "sidebar" } },
|
|
7452
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 } },
|
|
7453
8373
|
{ name: "skipNotification", type: "checkbox", defaultValue: false, label: "Sans notification", admin: { position: "sidebar", condition: (data) => data?.skipNotification === true } },
|
|
7454
|
-
{ 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 } },
|
|
7455
8375
|
{ name: "scheduledSent", type: "checkbox", defaultValue: false, admin: { hidden: true } },
|
|
7456
8376
|
{ name: "editedAt", type: "date", label: "Modifie le", admin: { hidden: true } },
|
|
7457
8377
|
{ name: "deletedAt", type: "date", label: "Supprime le", admin: { hidden: true } },
|
|
@@ -7460,7 +8380,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7460
8380
|
{ name: "emailOpenedAt", type: "date", label: "Email ouvert le", admin: { hidden: true } }
|
|
7461
8381
|
],
|
|
7462
8382
|
hooks: {
|
|
7463
|
-
beforeChange: [createAssignAuthor(slugs)],
|
|
8383
|
+
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
7464
8384
|
afterChange: [
|
|
7465
8385
|
createAutoUpdateStatus(slugs),
|
|
7466
8386
|
createNotifyClient(slugs),
|
|
@@ -7469,17 +8389,19 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7469
8389
|
createSyncTicketReplyToChat(slugs),
|
|
7470
8390
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
7471
8391
|
createFireMessageWebhooks(slugs),
|
|
7472
|
-
createDispatchWebhookOnReply(slugs)
|
|
8392
|
+
createDispatchWebhookOnReply(slugs),
|
|
8393
|
+
createNotifyMentions(slugs)
|
|
7473
8394
|
]
|
|
7474
8395
|
},
|
|
7475
8396
|
access: {
|
|
7476
8397
|
create: ({ req }) => req.user?.collection === slugs.users || req.user?.collection === slugs.supportClients,
|
|
7477
|
-
read: ({ req }) => {
|
|
8398
|
+
read: async ({ req }) => {
|
|
7478
8399
|
if (req.user?.collection === slugs.users) return true;
|
|
7479
8400
|
if (req.user?.collection === slugs.supportClients) {
|
|
8401
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
7480
8402
|
return {
|
|
7481
8403
|
and: [
|
|
7482
|
-
{
|
|
8404
|
+
{ ticket: { in: ids.length > 0 ? ids : [-1] } },
|
|
7483
8405
|
{ isInternal: { equals: false } },
|
|
7484
8406
|
{ or: [{ scheduledAt: { exists: false } }, { scheduledSent: { equals: true } }] }
|
|
7485
8407
|
]
|
|
@@ -7499,8 +8421,6 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7499
8421
|
timestamps: true
|
|
7500
8422
|
};
|
|
7501
8423
|
}
|
|
7502
|
-
|
|
7503
|
-
// src/collections/SupportClients.ts
|
|
7504
8424
|
function createSendInvitationOnCreate(slugs) {
|
|
7505
8425
|
return async ({ doc, operation, req }) => {
|
|
7506
8426
|
if (operation !== "create") return doc;
|
|
@@ -7540,6 +8460,26 @@ function createSendInvitationOnCreate(slugs) {
|
|
|
7540
8460
|
return doc;
|
|
7541
8461
|
};
|
|
7542
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
|
+
}
|
|
7543
8483
|
function createSupportClientsCollection(slugs) {
|
|
7544
8484
|
return {
|
|
7545
8485
|
slug: slugs.supportClients,
|
|
@@ -7668,6 +8608,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7668
8608
|
name: "opportunities",
|
|
7669
8609
|
type: "textarea",
|
|
7670
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
|
+
},
|
|
7671
8615
|
admin: {
|
|
7672
8616
|
description: "Notes commerciales (visible uniquement par les admins)"
|
|
7673
8617
|
}
|
|
@@ -7711,6 +8655,19 @@ function createSupportClientsCollection(slugs) {
|
|
|
7711
8655
|
type: "date",
|
|
7712
8656
|
admin: { hidden: true }
|
|
7713
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
|
+
},
|
|
7714
8671
|
{
|
|
7715
8672
|
name: "notifyOnReply",
|
|
7716
8673
|
type: "checkbox",
|
|
@@ -7777,6 +8734,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7777
8734
|
name: "notes",
|
|
7778
8735
|
type: "textarea",
|
|
7779
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
|
+
},
|
|
7780
8741
|
admin: {
|
|
7781
8742
|
description: "Visible uniquement par les admins",
|
|
7782
8743
|
position: "sidebar"
|
|
@@ -7784,6 +8745,7 @@ function createSupportClientsCollection(slugs) {
|
|
|
7784
8745
|
}
|
|
7785
8746
|
],
|
|
7786
8747
|
hooks: {
|
|
8748
|
+
beforeLogin: [createEnforce2FA(slugs)],
|
|
7787
8749
|
afterChange: [createSendInvitationOnCreate(slugs)]
|
|
7788
8750
|
},
|
|
7789
8751
|
access: {
|
|
@@ -7997,6 +8959,7 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
7997
8959
|
type: "relationship",
|
|
7998
8960
|
relationTo: slugs.tickets,
|
|
7999
8961
|
required: true,
|
|
8962
|
+
index: true,
|
|
8000
8963
|
label: "Ticket"
|
|
8001
8964
|
},
|
|
8002
8965
|
{
|
|
@@ -8042,10 +9005,11 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
8042
9005
|
access: {
|
|
8043
9006
|
create: () => false,
|
|
8044
9007
|
// Created only by hooks via overrideAccess
|
|
8045
|
-
read: ({ req }) => {
|
|
9008
|
+
read: async ({ req }) => {
|
|
8046
9009
|
if (req.user?.collection === slugs.users) return true;
|
|
8047
9010
|
if (req.user?.collection === slugs.supportClients) {
|
|
8048
|
-
|
|
9011
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
9012
|
+
return { ticket: { in: ids.length > 0 ? ids : [-1] } };
|
|
8049
9013
|
}
|
|
8050
9014
|
return false;
|
|
8051
9015
|
},
|
|
@@ -8114,12 +9078,21 @@ function createSatisfactionSurveysCollection(slugs) {
|
|
|
8114
9078
|
{
|
|
8115
9079
|
name: "rating",
|
|
8116
9080
|
type: "number",
|
|
8117
|
-
required: true,
|
|
8118
9081
|
min: 1,
|
|
8119
9082
|
max: 5,
|
|
8120
9083
|
label: "Note (1-5)",
|
|
8121
9084
|
admin: {
|
|
8122
|
-
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"
|
|
8123
9096
|
}
|
|
8124
9097
|
},
|
|
8125
9098
|
{
|
|
@@ -9365,6 +10338,124 @@ function createTicketCollaboratorsCollection(slugs) {
|
|
|
9365
10338
|
};
|
|
9366
10339
|
}
|
|
9367
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
|
+
|
|
9368
10459
|
// src/plugin.ts
|
|
9369
10460
|
function viewConfig(component, path) {
|
|
9370
10461
|
return { Component: component, path };
|
|
@@ -9400,7 +10491,10 @@ function supportPlugin(config) {
|
|
|
9400
10491
|
createSatisfactionSurveysCollection(slugs),
|
|
9401
10492
|
createKnowledgeBaseCollection(slugs),
|
|
9402
10493
|
createTicketFeedbackCollection(slugs, { notificationSlug: config?.notificationSlug }),
|
|
9403
|
-
createTicketCollaboratorsCollection(slugs)
|
|
10494
|
+
createTicketCollaboratorsCollection(slugs),
|
|
10495
|
+
createNotificationQueueCollection(slugs),
|
|
10496
|
+
createAutomationRulesCollection(slugs),
|
|
10497
|
+
createSupportTeamCollection(slugs)
|
|
9404
10498
|
];
|
|
9405
10499
|
if (features.authLogs !== false) supportCollections.push(createAuthLogsCollection(slugs));
|
|
9406
10500
|
if (features.timeTracking !== false) supportCollections.push(createTimeEntriesCollection(slugs));
|