@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.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import crypto3, {
|
|
1
|
+
import crypto3, { createHmac, randomBytes } from 'crypto';
|
|
2
|
+
import PDFDocument from 'pdfkit';
|
|
3
|
+
import { APIError, getFieldsToSign, jwtSign } from 'payload';
|
|
4
|
+
import sanitizeHtml from 'sanitize-html';
|
|
2
5
|
|
|
3
6
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
7
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -58,6 +61,9 @@ var DEFAULT_SLUGS = {
|
|
|
58
61
|
macros: "macros",
|
|
59
62
|
ticketStatuses: "ticket-statuses",
|
|
60
63
|
ticketFeedback: "ticket-feedback",
|
|
64
|
+
notificationQueue: "notification-queue",
|
|
65
|
+
automationRules: "automation-rules",
|
|
66
|
+
supportTeams: "support-teams",
|
|
61
67
|
users: "users",
|
|
62
68
|
media: "media"
|
|
63
69
|
};
|
|
@@ -89,6 +95,26 @@ function handleAuthError(error) {
|
|
|
89
95
|
return null;
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
// src/utils/db.ts
|
|
99
|
+
function dbFind(payload, slug, options = {}) {
|
|
100
|
+
return payload.find({ collection: slug, ...options });
|
|
101
|
+
}
|
|
102
|
+
function dbFindByID(payload, slug, options) {
|
|
103
|
+
return payload.findByID({ collection: slug, ...options });
|
|
104
|
+
}
|
|
105
|
+
function dbCreate(payload, slug, options) {
|
|
106
|
+
return payload.create({ collection: slug, ...options });
|
|
107
|
+
}
|
|
108
|
+
function dbUpdate(payload, slug, options) {
|
|
109
|
+
return payload.update({ collection: slug, ...options });
|
|
110
|
+
}
|
|
111
|
+
function dbCount(payload, slug, options = {}) {
|
|
112
|
+
return payload.count({ collection: slug, ...options });
|
|
113
|
+
}
|
|
114
|
+
function dbDelete(payload, slug, options) {
|
|
115
|
+
return payload.delete({ collection: slug, ...options });
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
// src/utils/readSettings.ts
|
|
93
119
|
var PREF_KEY = "support-settings";
|
|
94
120
|
var USER_PREFS_KEY_PREFIX = "support-user-prefs";
|
|
@@ -102,10 +128,18 @@ var DEFAULT_USER_PREFS = {
|
|
|
102
128
|
locale: "fr",
|
|
103
129
|
signature: ""
|
|
104
130
|
};
|
|
131
|
+
var settingsCache = null;
|
|
132
|
+
var SETTINGS_TTL_MS = 6e4;
|
|
133
|
+
function invalidateSupportSettingsCache() {
|
|
134
|
+
settingsCache = null;
|
|
135
|
+
}
|
|
105
136
|
async function readSupportSettings(payload) {
|
|
137
|
+
if (settingsCache && Date.now() - settingsCache.ts < SETTINGS_TTL_MS) {
|
|
138
|
+
return settingsCache.value;
|
|
139
|
+
}
|
|
140
|
+
let value = { ...DEFAULT_SETTINGS };
|
|
106
141
|
try {
|
|
107
|
-
const prefs = await payload
|
|
108
|
-
collection: "payload-preferences",
|
|
142
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
109
143
|
where: { key: { equals: PREF_KEY } },
|
|
110
144
|
limit: 1,
|
|
111
145
|
depth: 0,
|
|
@@ -113,7 +147,7 @@ async function readSupportSettings(payload) {
|
|
|
113
147
|
});
|
|
114
148
|
if (prefs.docs.length > 0) {
|
|
115
149
|
const stored = prefs.docs[0].value;
|
|
116
|
-
|
|
150
|
+
value = {
|
|
117
151
|
email: { ...DEFAULT_SETTINGS.email, ...stored.email },
|
|
118
152
|
ai: { ...DEFAULT_SETTINGS.ai, ...stored.ai },
|
|
119
153
|
sla: { ...DEFAULT_SETTINGS.sla, ...stored.sla },
|
|
@@ -122,13 +156,13 @@ async function readSupportSettings(payload) {
|
|
|
122
156
|
}
|
|
123
157
|
} catch {
|
|
124
158
|
}
|
|
125
|
-
|
|
159
|
+
settingsCache = { value, ts: Date.now() };
|
|
160
|
+
return value;
|
|
126
161
|
}
|
|
127
162
|
async function readUserPrefs(payload, userId) {
|
|
128
163
|
try {
|
|
129
164
|
const key = `${USER_PREFS_KEY_PREFIX}-${userId}`;
|
|
130
|
-
const prefs = await payload
|
|
131
|
-
collection: "payload-preferences",
|
|
165
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
132
166
|
where: { key: { equals: key } },
|
|
133
167
|
limit: 1,
|
|
134
168
|
depth: 0,
|
|
@@ -314,8 +348,130 @@ ${text}`;
|
|
|
314
348
|
};
|
|
315
349
|
}
|
|
316
350
|
|
|
317
|
-
// src/
|
|
351
|
+
// src/utils/aiAgent.ts
|
|
352
|
+
async function applyAgentDecision(payload, slugs, ticketId, decision) {
|
|
353
|
+
if (decision.action === "reply" && decision.reply && decision.reply.trim()) {
|
|
354
|
+
const msg = await dbCreate(payload, slugs.ticketMessages, {
|
|
355
|
+
data: {
|
|
356
|
+
ticket: ticketId,
|
|
357
|
+
body: decision.reply.trim(),
|
|
358
|
+
authorType: "admin",
|
|
359
|
+
isInternal: false,
|
|
360
|
+
fromAlias: "Assistant IA"
|
|
361
|
+
},
|
|
362
|
+
overrideAccess: true
|
|
363
|
+
});
|
|
364
|
+
return { acted: "reply", messageId: msg.id, confidence: decision.confidence };
|
|
365
|
+
}
|
|
366
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
367
|
+
data: {
|
|
368
|
+
ticket: ticketId,
|
|
369
|
+
body: `[Agent IA] Escalade vers un humain. Raison : ${decision.reason || "non d\xE9termin\xE9e"} (confiance ${Math.round((decision.confidence ?? 0) * 100)} %).`,
|
|
370
|
+
authorType: "admin",
|
|
371
|
+
isInternal: true,
|
|
372
|
+
skipNotification: true
|
|
373
|
+
},
|
|
374
|
+
overrideAccess: true
|
|
375
|
+
});
|
|
376
|
+
await dbUpdate(payload, slugs.tickets, { id: ticketId, data: { status: "escalated" }, overrideAccess: true });
|
|
377
|
+
return { acted: "escalate", confidence: decision.confidence };
|
|
378
|
+
}
|
|
318
379
|
function getClient2(aiSettings) {
|
|
380
|
+
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
381
|
+
if (aiSettings.provider === "ollama") {
|
|
382
|
+
return new Anthropic({ apiKey: "ollama", baseURL: process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1" });
|
|
383
|
+
}
|
|
384
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
385
|
+
}
|
|
386
|
+
function stripHtml(s) {
|
|
387
|
+
return (s || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
388
|
+
}
|
|
389
|
+
function parseDecision(text) {
|
|
390
|
+
try {
|
|
391
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
392
|
+
if (match) {
|
|
393
|
+
const obj = JSON.parse(match[0]);
|
|
394
|
+
return {
|
|
395
|
+
action: obj.action === "reply" ? "reply" : "escalate",
|
|
396
|
+
reply: typeof obj.reply === "string" ? obj.reply : void 0,
|
|
397
|
+
reason: typeof obj.reason === "string" ? obj.reason : void 0,
|
|
398
|
+
confidence: typeof obj.confidence === "number" ? obj.confidence : 0
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
return { action: "escalate", reason: "R\xE9ponse IA non interpr\xE9table", confidence: 0 };
|
|
404
|
+
}
|
|
405
|
+
async function runAiAgent(payload, slugs, ticketId, opts) {
|
|
406
|
+
const settings = await readSupportSettings(payload);
|
|
407
|
+
const threshold = opts?.confidenceThreshold ?? 0.7;
|
|
408
|
+
const ticket = await dbFindByID(payload, slugs.tickets, { id: ticketId, depth: 0, overrideAccess: true });
|
|
409
|
+
const messages = await dbFind(payload, slugs.ticketMessages, { where: { ticket: { equals: ticketId } }, sort: "createdAt", limit: 50, depth: 0, overrideAccess: true });
|
|
410
|
+
const kb = await dbFind(payload, slugs.knowledgeBase, { where: { published: { equals: true } }, limit: 20, depth: 0, overrideAccess: true });
|
|
411
|
+
const conversation = messages.docs.map((m) => `${m.authorType}: ${m.body || ""}`).join("\n");
|
|
412
|
+
const kbText = kb.docs.map((a) => `# ${a.title || ""}
|
|
413
|
+
${stripHtml(String(a.content ?? ""))}`).join("\n\n").slice(0, 8e3);
|
|
414
|
+
const prompt = `Tu es un agent de support IA. \xC0 partir du ticket et de la base de connaissances ci-dessous, d\xE9cide :
|
|
415
|
+
- "reply" si tu peux r\xE9pondre avec certitude UNIQUEMENT \xE0 partir de la base de connaissances ;
|
|
416
|
+
- "escalate" sinon (question hors KB, ambigu\xEB, sensible, ou n\xE9cessitant une action humaine).
|
|
417
|
+
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>}.
|
|
418
|
+
|
|
419
|
+
TICKET : ${ticket.subject || ""}
|
|
420
|
+
CONVERSATION :
|
|
421
|
+
${conversation}
|
|
422
|
+
|
|
423
|
+
BASE DE CONNAISSANCES :
|
|
424
|
+
${kbText || "(vide)"}`;
|
|
425
|
+
let decision;
|
|
426
|
+
try {
|
|
427
|
+
const client = getClient2(settings.ai);
|
|
428
|
+
const resp = await client.messages.create({
|
|
429
|
+
model: settings.ai.model || "claude-haiku-4-5-20251001",
|
|
430
|
+
max_tokens: 1024,
|
|
431
|
+
messages: [{ role: "user", content: prompt }]
|
|
432
|
+
});
|
|
433
|
+
const text = resp?.content?.[0]?.text || "";
|
|
434
|
+
decision = parseDecision(text);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.error("[support] AI agent decision failed:", err);
|
|
437
|
+
decision = { action: "escalate", reason: "Erreur du fournisseur IA", confidence: 0 };
|
|
438
|
+
}
|
|
439
|
+
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 };
|
|
440
|
+
return applyAgentDecision(payload, slugs, ticketId, finalDecision);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/endpoints/ai-agent.ts
|
|
444
|
+
function createAiAgentEndpoint(slugs) {
|
|
445
|
+
return {
|
|
446
|
+
path: "/support/ai-agent",
|
|
447
|
+
method: "post",
|
|
448
|
+
handler: async (req) => {
|
|
449
|
+
try {
|
|
450
|
+
requireAdmin(req, slugs);
|
|
451
|
+
let body = {};
|
|
452
|
+
try {
|
|
453
|
+
body = await req.json();
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
if (!body.ticketId) {
|
|
457
|
+
return Response.json({ error: "ticketId requis." }, { status: 400 });
|
|
458
|
+
}
|
|
459
|
+
const result = await runAiAgent(req.payload, slugs, body.ticketId, {
|
|
460
|
+
confidenceThreshold: body.confidenceThreshold
|
|
461
|
+
});
|
|
462
|
+
return Response.json({ ok: true, ...result });
|
|
463
|
+
} catch (error) {
|
|
464
|
+
const authResponse = handleAuthError(error);
|
|
465
|
+
if (authResponse) return authResponse;
|
|
466
|
+
console.error("[ai-agent] Error:", error);
|
|
467
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/endpoints/client-intelligence.ts
|
|
474
|
+
function getClient3(aiSettings) {
|
|
319
475
|
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
320
476
|
if (aiSettings.provider === "ollama") {
|
|
321
477
|
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
@@ -469,7 +625,7 @@ R\xE9ponds en JSON strict (pas de markdown, pas de commentaires) avec cette stru
|
|
|
469
625
|
}
|
|
470
626
|
|
|
471
627
|
Sois factuel. Ne d\xE9passe pas 5 items par tableau. R\xE9ponds UNIQUEMENT avec le JSON.`;
|
|
472
|
-
const anthropic =
|
|
628
|
+
const anthropic = getClient3(aiSettings);
|
|
473
629
|
const model = getModel2(aiSettings);
|
|
474
630
|
const res = await anthropic.messages.create({
|
|
475
631
|
model,
|
|
@@ -533,8 +689,7 @@ function createSearchEndpoint(slugs) {
|
|
|
533
689
|
return Response.json({ tickets: [], clients: [], messages: [], articles: [] });
|
|
534
690
|
}
|
|
535
691
|
const [ticketsRes, clientsRes, messagesRes, articlesRes] = await Promise.all([
|
|
536
|
-
payload.
|
|
537
|
-
collection: slugs.tickets,
|
|
692
|
+
dbFind(payload, slugs.tickets, {
|
|
538
693
|
where: {
|
|
539
694
|
or: [
|
|
540
695
|
{ ticketNumber: { contains: q } },
|
|
@@ -546,8 +701,7 @@ function createSearchEndpoint(slugs) {
|
|
|
546
701
|
depth: 1,
|
|
547
702
|
overrideAccess: true
|
|
548
703
|
}),
|
|
549
|
-
payload.
|
|
550
|
-
collection: slugs.supportClients,
|
|
704
|
+
dbFind(payload, slugs.supportClients, {
|
|
551
705
|
where: {
|
|
552
706
|
or: [
|
|
553
707
|
{ firstName: { contains: q } },
|
|
@@ -560,16 +714,14 @@ function createSearchEndpoint(slugs) {
|
|
|
560
714
|
depth: 0,
|
|
561
715
|
overrideAccess: true
|
|
562
716
|
}),
|
|
563
|
-
payload.
|
|
564
|
-
collection: slugs.ticketMessages,
|
|
717
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
565
718
|
where: { body: { contains: q } },
|
|
566
719
|
sort: "-createdAt",
|
|
567
720
|
limit: 5,
|
|
568
721
|
depth: 1,
|
|
569
722
|
overrideAccess: true
|
|
570
723
|
}),
|
|
571
|
-
payload.
|
|
572
|
-
collection: slugs.knowledgeBase,
|
|
724
|
+
dbFind(payload, slugs.knowledgeBase, {
|
|
573
725
|
where: { title: { contains: q } },
|
|
574
726
|
limit: 5,
|
|
575
727
|
depth: 0,
|
|
@@ -669,8 +821,7 @@ function createKbSearchEndpoint(slugs) {
|
|
|
669
821
|
orClauses.push({ category: { contains: t } });
|
|
670
822
|
}
|
|
671
823
|
orClauses.push({ title: { contains: q } });
|
|
672
|
-
const res = await payload.
|
|
673
|
-
collection: slugs.knowledgeBase,
|
|
824
|
+
const res = await dbFind(payload, slugs.knowledgeBase, {
|
|
674
825
|
where: {
|
|
675
826
|
and: [
|
|
676
827
|
{ published: { equals: true } },
|
|
@@ -729,16 +880,14 @@ function createBulkActionEndpoint(slugs) {
|
|
|
729
880
|
try {
|
|
730
881
|
switch (action) {
|
|
731
882
|
case "close":
|
|
732
|
-
await payload.
|
|
733
|
-
collection: slugs.tickets,
|
|
883
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
734
884
|
id: ticketId,
|
|
735
885
|
data: { status: "resolved", resolvedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
736
886
|
overrideAccess: true
|
|
737
887
|
});
|
|
738
888
|
break;
|
|
739
889
|
case "reopen":
|
|
740
|
-
await payload.
|
|
741
|
-
collection: slugs.tickets,
|
|
890
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
742
891
|
id: ticketId,
|
|
743
892
|
data: { status: "open" },
|
|
744
893
|
overrideAccess: true
|
|
@@ -746,8 +895,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
746
895
|
break;
|
|
747
896
|
case "assign":
|
|
748
897
|
if (value) {
|
|
749
|
-
await payload.
|
|
750
|
-
collection: slugs.tickets,
|
|
898
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
751
899
|
id: ticketId,
|
|
752
900
|
data: { assignedTo: Number(value) },
|
|
753
901
|
overrideAccess: true
|
|
@@ -756,8 +904,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
756
904
|
break;
|
|
757
905
|
case "set_priority":
|
|
758
906
|
if (value) {
|
|
759
|
-
await payload.
|
|
760
|
-
collection: slugs.tickets,
|
|
907
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
761
908
|
id: ticketId,
|
|
762
909
|
data: { priority: String(value) },
|
|
763
910
|
overrideAccess: true
|
|
@@ -766,8 +913,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
766
913
|
break;
|
|
767
914
|
case "set_category":
|
|
768
915
|
if (value) {
|
|
769
|
-
await payload.
|
|
770
|
-
collection: slugs.tickets,
|
|
916
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
771
917
|
id: ticketId,
|
|
772
918
|
data: { category: String(value) },
|
|
773
919
|
overrideAccess: true
|
|
@@ -775,8 +921,7 @@ function createBulkActionEndpoint(slugs) {
|
|
|
775
921
|
}
|
|
776
922
|
break;
|
|
777
923
|
case "delete":
|
|
778
|
-
await payload.
|
|
779
|
-
collection: slugs.tickets,
|
|
924
|
+
await dbDelete(payload, slugs.tickets, {
|
|
780
925
|
id: ticketId,
|
|
781
926
|
overrideAccess: true
|
|
782
927
|
});
|
|
@@ -815,8 +960,8 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
815
960
|
return Response.json({ error: "Impossible de fusionner un ticket avec lui-m\xEAme" }, { status: 400 });
|
|
816
961
|
}
|
|
817
962
|
const [source, target] = await Promise.all([
|
|
818
|
-
payload
|
|
819
|
-
payload
|
|
963
|
+
dbFindByID(payload, slugs.tickets, { id: sourceTicketId, depth: 0, overrideAccess: true }),
|
|
964
|
+
dbFindByID(payload, slugs.tickets, { id: targetTicketId, depth: 0, overrideAccess: true })
|
|
820
965
|
]);
|
|
821
966
|
if (!source) return Response.json({ error: "Ticket source introuvable" }, { status: 404 });
|
|
822
967
|
if (!target) return Response.json({ error: "Ticket cible introuvable" }, { status: 404 });
|
|
@@ -825,39 +970,34 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
825
970
|
if (sourceClient !== targetClient) {
|
|
826
971
|
return Response.json({ error: "Les deux tickets doivent appartenir au m\xEAme client" }, { status: 400 });
|
|
827
972
|
}
|
|
828
|
-
const messages = await payload.
|
|
829
|
-
collection: slugs.ticketMessages,
|
|
973
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
830
974
|
where: { ticket: { equals: sourceTicketId } },
|
|
831
975
|
limit: 500,
|
|
832
976
|
depth: 0,
|
|
833
977
|
overrideAccess: true
|
|
834
978
|
});
|
|
835
979
|
for (const msg of messages.docs) {
|
|
836
|
-
await payload.
|
|
837
|
-
collection: slugs.ticketMessages,
|
|
980
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
838
981
|
id: msg.id,
|
|
839
982
|
data: { ticket: targetTicketId },
|
|
840
983
|
overrideAccess: true
|
|
841
984
|
});
|
|
842
985
|
}
|
|
843
|
-
const timeEntries = await payload.
|
|
844
|
-
collection: slugs.timeEntries,
|
|
986
|
+
const timeEntries = await dbFind(payload, slugs.timeEntries, {
|
|
845
987
|
where: { ticket: { equals: sourceTicketId } },
|
|
846
988
|
limit: 500,
|
|
847
989
|
depth: 0,
|
|
848
990
|
overrideAccess: true
|
|
849
991
|
});
|
|
850
992
|
for (const entry of timeEntries.docs) {
|
|
851
|
-
await payload.
|
|
852
|
-
collection: slugs.timeEntries,
|
|
993
|
+
await dbUpdate(payload, slugs.timeEntries, {
|
|
853
994
|
id: entry.id,
|
|
854
995
|
data: { ticket: targetTicketId },
|
|
855
996
|
overrideAccess: true
|
|
856
997
|
});
|
|
857
998
|
}
|
|
858
999
|
const sourceNumber = source.ticketNumber || `#${sourceTicketId}`;
|
|
859
|
-
await payload.
|
|
860
|
-
collection: slugs.ticketMessages,
|
|
1000
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
861
1001
|
data: {
|
|
862
1002
|
ticket: targetTicketId,
|
|
863
1003
|
body: `Messages fusionn\xE9s depuis ${sourceNumber} (${messages.totalDocs} messages, ${timeEntries.totalDocs} entr\xE9es de temps)`,
|
|
@@ -867,13 +1007,11 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
867
1007
|
},
|
|
868
1008
|
overrideAccess: true
|
|
869
1009
|
});
|
|
870
|
-
await payload.
|
|
871
|
-
collection: slugs.tickets,
|
|
1010
|
+
await dbDelete(payload, slugs.tickets, {
|
|
872
1011
|
id: sourceTicketId,
|
|
873
1012
|
overrideAccess: true
|
|
874
1013
|
});
|
|
875
|
-
const allTimeEntries = await payload.
|
|
876
|
-
collection: slugs.timeEntries,
|
|
1014
|
+
const allTimeEntries = await dbFind(payload, slugs.timeEntries, {
|
|
877
1015
|
where: { ticket: { equals: targetTicketId } },
|
|
878
1016
|
limit: 500,
|
|
879
1017
|
depth: 0,
|
|
@@ -883,8 +1021,7 @@ function createMergeTicketsEndpoint(slugs) {
|
|
|
883
1021
|
(sum, e) => sum + (e.duration || 0),
|
|
884
1022
|
0
|
|
885
1023
|
);
|
|
886
|
-
await payload.
|
|
887
|
-
collection: slugs.tickets,
|
|
1024
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
888
1025
|
id: targetTicketId,
|
|
889
1026
|
data: { totalTimeMinutes: totalMinutes },
|
|
890
1027
|
overrideAccess: true
|
|
@@ -919,8 +1056,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
919
1056
|
if (!messageId) {
|
|
920
1057
|
return Response.json({ error: "messageId required" }, { status: 400 });
|
|
921
1058
|
}
|
|
922
|
-
const message = await payload.
|
|
923
|
-
collection: slugs.ticketMessages,
|
|
1059
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
924
1060
|
id: messageId,
|
|
925
1061
|
depth: 1,
|
|
926
1062
|
overrideAccess: true
|
|
@@ -933,8 +1069,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
933
1069
|
return Response.json({ error: "Cannot resolve source ticket" }, { status: 400 });
|
|
934
1070
|
}
|
|
935
1071
|
const clientId = typeof sourceTicket.client === "object" ? sourceTicket.client.id : sourceTicket.client;
|
|
936
|
-
const newTicket = await payload.
|
|
937
|
-
collection: slugs.tickets,
|
|
1072
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
938
1073
|
data: {
|
|
939
1074
|
subject: subject || `Split: ${sourceTicket.subject}`,
|
|
940
1075
|
client: clientId,
|
|
@@ -949,8 +1084,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
949
1084
|
const attachments = message.attachments?.map((a) => ({
|
|
950
1085
|
file: typeof a.file === "object" ? a.file.id : a.file
|
|
951
1086
|
})) || [];
|
|
952
|
-
await payload.
|
|
953
|
-
collection: slugs.ticketMessages,
|
|
1087
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
954
1088
|
data: {
|
|
955
1089
|
ticket: newTicket.id,
|
|
956
1090
|
body: message.body,
|
|
@@ -963,8 +1097,7 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
963
1097
|
},
|
|
964
1098
|
overrideAccess: true
|
|
965
1099
|
});
|
|
966
|
-
await payload.
|
|
967
|
-
collection: slugs.ticketMessages,
|
|
1100
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
968
1101
|
data: {
|
|
969
1102
|
ticket: sourceTicket.id,
|
|
970
1103
|
body: `Message extrait vers le ticket ${newTicket.ticketNumber}`,
|
|
@@ -976,15 +1109,13 @@ function createSplitTicketEndpoint(slugs) {
|
|
|
976
1109
|
});
|
|
977
1110
|
const existingRelated = Array.isArray(sourceTicket.relatedTickets) ? sourceTicket.relatedTickets.map((t) => typeof t === "object" ? t.id : t) : [];
|
|
978
1111
|
if (!existingRelated.includes(newTicket.id)) {
|
|
979
|
-
await payload.
|
|
980
|
-
collection: slugs.tickets,
|
|
1112
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
981
1113
|
id: sourceTicket.id,
|
|
982
1114
|
data: { relatedTickets: [...existingRelated, newTicket.id] },
|
|
983
1115
|
overrideAccess: true
|
|
984
1116
|
});
|
|
985
1117
|
}
|
|
986
|
-
await payload.
|
|
987
|
-
collection: slugs.ticketActivityLog,
|
|
1118
|
+
await dbCreate(payload, slugs.ticketActivityLog, {
|
|
988
1119
|
data: {
|
|
989
1120
|
ticket: sourceTicket.id,
|
|
990
1121
|
action: "split",
|
|
@@ -1199,8 +1330,7 @@ function createSettingsGetEndpoint(slugs) {
|
|
|
1199
1330
|
try {
|
|
1200
1331
|
const payload = req.payload;
|
|
1201
1332
|
requireAdmin(req, slugs);
|
|
1202
|
-
const prefs = await payload
|
|
1203
|
-
collection: "payload-preferences",
|
|
1333
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1204
1334
|
where: { key: { equals: PREF_KEY2 } },
|
|
1205
1335
|
limit: 1,
|
|
1206
1336
|
depth: 0,
|
|
@@ -1257,6 +1387,7 @@ function createSettingsPostEndpoint(slugs) {
|
|
|
1257
1387
|
]
|
|
1258
1388
|
}
|
|
1259
1389
|
});
|
|
1390
|
+
invalidateSupportSettingsCache();
|
|
1260
1391
|
return Response.json(merged);
|
|
1261
1392
|
} catch (error) {
|
|
1262
1393
|
const authResponse = handleAuthError(error);
|
|
@@ -1278,8 +1409,7 @@ function createSignatureGetEndpoint(slugs) {
|
|
|
1278
1409
|
try {
|
|
1279
1410
|
const payload = req.payload;
|
|
1280
1411
|
requireAdmin(req, slugs);
|
|
1281
|
-
const prefs = await payload
|
|
1282
|
-
collection: "payload-preferences",
|
|
1412
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1283
1413
|
where: { key: { equals: `${PREF_KEY3}-${req.user.id}` } },
|
|
1284
1414
|
limit: 1,
|
|
1285
1415
|
depth: 0,
|
|
@@ -1306,8 +1436,7 @@ function createSignaturePostEndpoint(slugs) {
|
|
|
1306
1436
|
requireAdmin(req, slugs);
|
|
1307
1437
|
const { signature } = await req.json();
|
|
1308
1438
|
const key = `${PREF_KEY3}-${req.user.id}`;
|
|
1309
|
-
const existing = await payload
|
|
1310
|
-
collection: "payload-preferences",
|
|
1439
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
1311
1440
|
where: { key: { equals: key } },
|
|
1312
1441
|
limit: 1,
|
|
1313
1442
|
depth: 0,
|
|
@@ -1350,8 +1479,7 @@ function createRoundRobinConfigGetEndpoint(slugs) {
|
|
|
1350
1479
|
try {
|
|
1351
1480
|
const payload = req.payload;
|
|
1352
1481
|
requireAdmin(req, slugs);
|
|
1353
|
-
const prefs = await payload
|
|
1354
|
-
collection: "payload-preferences",
|
|
1482
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1355
1483
|
where: { key: { equals: PREF_KEY4 } },
|
|
1356
1484
|
limit: 1,
|
|
1357
1485
|
depth: 0,
|
|
@@ -1377,8 +1505,7 @@ function createRoundRobinConfigPostEndpoint(slugs) {
|
|
|
1377
1505
|
const payload = req.payload;
|
|
1378
1506
|
requireAdmin(req, slugs);
|
|
1379
1507
|
const { enabled } = await req.json();
|
|
1380
|
-
const existing = await payload
|
|
1381
|
-
collection: "payload-preferences",
|
|
1508
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
1382
1509
|
where: { key: { equals: PREF_KEY4 } },
|
|
1383
1510
|
limit: 1,
|
|
1384
1511
|
depth: 0,
|
|
@@ -1422,8 +1549,7 @@ function createSlaCheckEndpoint(slugs) {
|
|
|
1422
1549
|
requireAdmin(req, slugs);
|
|
1423
1550
|
const now = /* @__PURE__ */ new Date();
|
|
1424
1551
|
const nowISO = now.toISOString();
|
|
1425
|
-
const { docs: tickets } = await payload.
|
|
1426
|
-
collection: slugs.tickets,
|
|
1552
|
+
const { docs: tickets } = await dbFind(payload, slugs.tickets, {
|
|
1427
1553
|
where: {
|
|
1428
1554
|
and: [
|
|
1429
1555
|
{ status: { in: ["open", "waiting_client"] } },
|
|
@@ -1517,6 +1643,107 @@ function createSlaCheckEndpoint(slugs) {
|
|
|
1517
1643
|
}
|
|
1518
1644
|
};
|
|
1519
1645
|
}
|
|
1646
|
+
var TRANSPARENT_GIF = Buffer.from(
|
|
1647
|
+
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
1648
|
+
"base64"
|
|
1649
|
+
);
|
|
1650
|
+
function generateTrackingToken(ticketId, messageId, secret) {
|
|
1651
|
+
return createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex").substring(0, 16);
|
|
1652
|
+
}
|
|
1653
|
+
function createTrackOpenEndpoint(slugs) {
|
|
1654
|
+
return {
|
|
1655
|
+
path: "/support/track-open",
|
|
1656
|
+
method: "get",
|
|
1657
|
+
handler: async (req) => {
|
|
1658
|
+
const url = new URL(req.url);
|
|
1659
|
+
const ticketId = url.searchParams.get("t");
|
|
1660
|
+
const messageId = url.searchParams.get("m");
|
|
1661
|
+
const sig = url.searchParams.get("sig");
|
|
1662
|
+
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
1663
|
+
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
1664
|
+
const secret = process.env.PAYLOAD_SECRET || "";
|
|
1665
|
+
const validSig = !!secret && !!ticketId && !!messageId && !!sig && sig === generateTrackingToken(ticketId, messageId, secret);
|
|
1666
|
+
if (!validSig) {
|
|
1667
|
+
return new Response(TRANSPARENT_GIF, {
|
|
1668
|
+
status: 200,
|
|
1669
|
+
headers: {
|
|
1670
|
+
"Content-Type": "image/gif",
|
|
1671
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
1672
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
1677
|
+
try {
|
|
1678
|
+
const payload = req.payload;
|
|
1679
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
1680
|
+
id: parsedId,
|
|
1681
|
+
depth: 0,
|
|
1682
|
+
overrideAccess: true,
|
|
1683
|
+
select: { lastClientReadAt: true }
|
|
1684
|
+
});
|
|
1685
|
+
if (ticket) {
|
|
1686
|
+
const lastRead = ticket.lastClientReadAt ? new Date(ticket.lastClientReadAt).getTime() : 0;
|
|
1687
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
|
|
1688
|
+
if (lastRead < fiveMinAgo) {
|
|
1689
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1690
|
+
id: parsedId,
|
|
1691
|
+
data: { lastClientReadAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1692
|
+
overrideAccess: true
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
1697
|
+
const msg = await dbFindByID(payload, slugs.ticketMessages, {
|
|
1698
|
+
id: parsedMsgId,
|
|
1699
|
+
depth: 0,
|
|
1700
|
+
overrideAccess: true,
|
|
1701
|
+
select: { emailOpenedAt: true }
|
|
1702
|
+
});
|
|
1703
|
+
if (msg && !msg.emailOpenedAt) {
|
|
1704
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
1705
|
+
id: parsedMsgId,
|
|
1706
|
+
data: { emailOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1707
|
+
overrideAccess: true
|
|
1708
|
+
});
|
|
1709
|
+
const ticketInfo = await dbFindByID(payload, slugs.tickets, {
|
|
1710
|
+
id: parsedId,
|
|
1711
|
+
depth: 1,
|
|
1712
|
+
overrideAccess: true,
|
|
1713
|
+
select: { ticketNumber: true, subject: true, client: true }
|
|
1714
|
+
});
|
|
1715
|
+
const clientName = typeof ticketInfo?.client === "object" ? ticketInfo.client?.firstName || "Client" : "Client";
|
|
1716
|
+
try {
|
|
1717
|
+
await dbCreate(payload, "admin-notifications", {
|
|
1718
|
+
data: {
|
|
1719
|
+
title: `Email ouvert \u2014 ${ticketInfo?.ticketNumber || "TK-????"}`,
|
|
1720
|
+
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || "ticket"}"`,
|
|
1721
|
+
type: "email_opened",
|
|
1722
|
+
link: `/admin/ticket?id=${parsedId}`
|
|
1723
|
+
},
|
|
1724
|
+
overrideAccess: true
|
|
1725
|
+
});
|
|
1726
|
+
} catch {
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
console.error("[track-open] Error:", err);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return new Response(TRANSPARENT_GIF, {
|
|
1735
|
+
status: 200,
|
|
1736
|
+
headers: {
|
|
1737
|
+
"Content-Type": "image/gif",
|
|
1738
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
1739
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
1740
|
+
"Pragma": "no-cache",
|
|
1741
|
+
"Expires": "0"
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1520
1747
|
|
|
1521
1748
|
// src/utils/emailTemplate.ts
|
|
1522
1749
|
var COLORS = {
|
|
@@ -1553,7 +1780,10 @@ function escapeHtml(str) {
|
|
|
1553
1780
|
}
|
|
1554
1781
|
function emailTrackingPixel(ticketId, messageId, baseUrl) {
|
|
1555
1782
|
const url = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
1556
|
-
const
|
|
1783
|
+
const secret = process.env.PAYLOAD_SECRET || "";
|
|
1784
|
+
if (!secret || messageId === void 0 || messageId === null) return "";
|
|
1785
|
+
const sig = generateTrackingToken(String(ticketId), String(messageId), secret);
|
|
1786
|
+
const params = `t=${ticketId}&m=${messageId}&sig=${sig}`;
|
|
1557
1787
|
return `<img src="${url}/api/support/track-open?${params}" width="1" height="1" alt="" style="display:block;width:1px;height:1px;border:0;" />`;
|
|
1558
1788
|
}
|
|
1559
1789
|
function emailRichContent(html, config) {
|
|
@@ -1818,8 +2048,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1818
2048
|
const CLOSE_AFTER_REMIND_DAYS = settings.autoClose.reminderDaysBefore;
|
|
1819
2049
|
const results = { reminded: 0, closed: 0, errors: 0 };
|
|
1820
2050
|
const remindCutoff = new Date(now.getTime() - REMIND_AFTER_DAYS * 24 * 60 * 60 * 1e3);
|
|
1821
|
-
const ticketsToRemind = await payload.
|
|
1822
|
-
collection: slugs.tickets,
|
|
2051
|
+
const ticketsToRemind = await dbFind(payload, slugs.tickets, {
|
|
1823
2052
|
where: {
|
|
1824
2053
|
and: [
|
|
1825
2054
|
{ status: { equals: "waiting_client" } },
|
|
@@ -1850,8 +2079,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1850
2079
|
<p><a href="${portalUrl}">R\xE9pondre au ticket</a></p>
|
|
1851
2080
|
</div>`
|
|
1852
2081
|
});
|
|
1853
|
-
await payload.
|
|
1854
|
-
collection: slugs.tickets,
|
|
2082
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1855
2083
|
id: t.id,
|
|
1856
2084
|
data: { autoCloseRemindedAt: now.toISOString() },
|
|
1857
2085
|
overrideAccess: true
|
|
@@ -1863,16 +2091,24 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1863
2091
|
}
|
|
1864
2092
|
}
|
|
1865
2093
|
const closeCutoff = new Date(now.getTime() - CLOSE_AFTER_REMIND_DAYS * 24 * 60 * 60 * 1e3);
|
|
1866
|
-
const ticketsToClose = await payload.
|
|
1867
|
-
collection: slugs.tickets,
|
|
2094
|
+
const ticketsToClose = await dbFind(payload, slugs.tickets, {
|
|
1868
2095
|
where: {
|
|
1869
2096
|
and: [
|
|
1870
2097
|
{ status: { equals: "waiting_client" } },
|
|
1871
|
-
{ autoCloseRemindedAt: { less_than: closeCutoff.toISOString() } },
|
|
1872
2098
|
{
|
|
1873
2099
|
or: [
|
|
1874
|
-
{
|
|
1875
|
-
|
|
2100
|
+
{
|
|
2101
|
+
and: [
|
|
2102
|
+
{ autoCloseRemindedAt: { less_than: closeCutoff.toISOString() } },
|
|
2103
|
+
{
|
|
2104
|
+
or: [
|
|
2105
|
+
{ lastClientMessageAt: { exists: false } },
|
|
2106
|
+
{ lastClientMessageAt: { less_than_equal: closeCutoff.toISOString() } }
|
|
2107
|
+
]
|
|
2108
|
+
}
|
|
2109
|
+
]
|
|
2110
|
+
},
|
|
2111
|
+
{ autoCloseScheduledAt: { less_than_equal: now.toISOString() } }
|
|
1876
2112
|
]
|
|
1877
2113
|
}
|
|
1878
2114
|
]
|
|
@@ -1888,21 +2124,23 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1888
2124
|
const ticketNumber = t.ticketNumber || "TK-????";
|
|
1889
2125
|
const subject = t.subject || "Support";
|
|
1890
2126
|
const closeTotalDays = REMIND_AFTER_DAYS + CLOSE_AFTER_REMIND_DAYS;
|
|
1891
|
-
|
|
1892
|
-
|
|
2127
|
+
const viaSchedule = !!t.autoCloseScheduledAt && new Date(t.autoCloseScheduledAt).getTime() <= now.getTime();
|
|
2128
|
+
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`;
|
|
2129
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
1893
2130
|
data: {
|
|
1894
2131
|
ticket: t.id,
|
|
1895
|
-
body:
|
|
2132
|
+
body: noteBody,
|
|
1896
2133
|
authorType: "admin",
|
|
1897
2134
|
isInternal: true,
|
|
1898
2135
|
skipNotification: true
|
|
1899
2136
|
},
|
|
1900
2137
|
overrideAccess: true
|
|
1901
2138
|
});
|
|
1902
|
-
await payload.
|
|
1903
|
-
collection: slugs.tickets,
|
|
2139
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1904
2140
|
id: t.id,
|
|
1905
|
-
|
|
2141
|
+
// Clear the armed deadline so a later manual reopen can't be
|
|
2142
|
+
// closed instantly by a stale timestamp.
|
|
2143
|
+
data: { status: "resolved", autoCloseScheduledAt: null },
|
|
1906
2144
|
overrideAccess: true
|
|
1907
2145
|
});
|
|
1908
2146
|
if (client?.email) {
|
|
@@ -1913,7 +2151,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1913
2151
|
subject: `[${ticketNumber}] Ticket r\xE9solu \u2014 ${subject}`,
|
|
1914
2152
|
html: `<div style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
1915
2153
|
<p>Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,</p>
|
|
1916
|
-
<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>
|
|
2154
|
+
<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>
|
|
1917
2155
|
<p>Si vous avez encore besoin d'aide, n'h\xE9sitez pas \xE0 rouvrir ce ticket ou \xE0 en cr\xE9er un nouveau.</p>
|
|
1918
2156
|
<p><a href="${portalUrl}">Consulter le ticket</a></p>
|
|
1919
2157
|
</div>`
|
|
@@ -1938,27 +2176,206 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1938
2176
|
};
|
|
1939
2177
|
}
|
|
1940
2178
|
|
|
1941
|
-
// src/
|
|
1942
|
-
|
|
2179
|
+
// src/utils/rateLimiter.ts
|
|
2180
|
+
var RateLimiter = class {
|
|
2181
|
+
constructor(windowMs, maxRequests) {
|
|
2182
|
+
this.windowMs = windowMs;
|
|
2183
|
+
this.maxRequests = maxRequests;
|
|
2184
|
+
const timer = setInterval(() => this.cleanup(), windowMs);
|
|
2185
|
+
timer.unref();
|
|
2186
|
+
}
|
|
2187
|
+
windowMs;
|
|
2188
|
+
maxRequests;
|
|
2189
|
+
store = /* @__PURE__ */ new Map();
|
|
2190
|
+
/**
|
|
2191
|
+
* Check if a key has exceeded the rate limit.
|
|
2192
|
+
* Returns true if the request should be blocked.
|
|
2193
|
+
*/
|
|
2194
|
+
check(key) {
|
|
2195
|
+
const now = Date.now();
|
|
2196
|
+
const entry = this.store.get(key);
|
|
2197
|
+
if (!entry || now > entry.resetAt) {
|
|
2198
|
+
this.store.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
2199
|
+
return false;
|
|
2200
|
+
}
|
|
2201
|
+
entry.count++;
|
|
2202
|
+
return entry.count > this.maxRequests;
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Reset the counter for a specific key.
|
|
2206
|
+
*/
|
|
2207
|
+
reset(key) {
|
|
2208
|
+
this.store.delete(key);
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Remove expired entries from the store.
|
|
2212
|
+
*/
|
|
2213
|
+
cleanup() {
|
|
2214
|
+
const now = Date.now();
|
|
2215
|
+
for (const [key, entry] of this.store) {
|
|
2216
|
+
if (now > entry.resetAt) {
|
|
2217
|
+
this.store.delete(key);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
|
|
2223
|
+
// src/endpoints/send-reminder.ts
|
|
2224
|
+
var reminderLimiter = new RateLimiter(60 * 60 * 1e3, 30);
|
|
2225
|
+
var MIN_HOURS = 1;
|
|
2226
|
+
var MAX_HOURS = 24 * 30;
|
|
2227
|
+
function formatFr(date, withTime) {
|
|
2228
|
+
return date.toLocaleString("fr-FR", {
|
|
2229
|
+
day: "numeric",
|
|
2230
|
+
month: "long",
|
|
2231
|
+
year: "numeric",
|
|
2232
|
+
...withTime ? { hour: "2-digit", minute: "2-digit" } : {},
|
|
2233
|
+
timeZone: "Europe/Paris"
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
function createSendReminderEndpoint(slugs) {
|
|
1943
2237
|
return {
|
|
1944
|
-
path: "/support/
|
|
1945
|
-
method: "
|
|
2238
|
+
path: "/support/send-reminder",
|
|
2239
|
+
method: "post",
|
|
1946
2240
|
handler: async (req) => {
|
|
1947
2241
|
try {
|
|
1948
2242
|
const payload = req.payload;
|
|
1949
|
-
|
|
1950
|
-
|
|
2243
|
+
requireAdmin(req, slugs);
|
|
2244
|
+
if (reminderLimiter.check(String(req.user.id))) {
|
|
2245
|
+
return Response.json(
|
|
2246
|
+
{ error: "Trop de relances. R\xE9essayez dans une heure." },
|
|
2247
|
+
{ status: 429 }
|
|
2248
|
+
);
|
|
1951
2249
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2250
|
+
let body;
|
|
2251
|
+
try {
|
|
2252
|
+
body = await req.json();
|
|
2253
|
+
} catch {
|
|
2254
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
2255
|
+
}
|
|
2256
|
+
const { ticketId } = body;
|
|
2257
|
+
if (!ticketId) {
|
|
2258
|
+
return Response.json({ error: "ticketId requis" }, { status: 400 });
|
|
2259
|
+
}
|
|
2260
|
+
const rawHours = Number(body.hours);
|
|
2261
|
+
const hours = Number.isFinite(rawHours) ? Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.round(rawHours))) : 24;
|
|
2262
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2263
|
+
id: ticketId,
|
|
2264
|
+
depth: 1,
|
|
1957
2265
|
overrideAccess: true
|
|
1958
2266
|
});
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2267
|
+
if (!ticket) {
|
|
2268
|
+
return Response.json({ error: "Ticket introuvable" }, { status: 404 });
|
|
2269
|
+
}
|
|
2270
|
+
const client = typeof ticket.client === "object" ? ticket.client : null;
|
|
2271
|
+
if (!client?.email) {
|
|
2272
|
+
return Response.json({ error: "Client sans email" }, { status: 400 });
|
|
2273
|
+
}
|
|
2274
|
+
const now = /* @__PURE__ */ new Date();
|
|
2275
|
+
const deadline = new Date(now.getTime() + hours * 60 * 60 * 1e3);
|
|
2276
|
+
const settings = await readSupportSettings(payload);
|
|
2277
|
+
const ticketNumber = ticket.ticketNumber || "TK-????";
|
|
2278
|
+
const subject = ticket.subject || "Support";
|
|
2279
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
2280
|
+
const portalUrl = `${baseUrl}/support/tickets/${ticketId}`;
|
|
2281
|
+
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_REPLY_TO || "";
|
|
2282
|
+
let waitingSince = ticket.firstResponseAt || ticket.createdAt;
|
|
2283
|
+
try {
|
|
2284
|
+
const lastAdminMsg = await dbFind(payload, slugs.ticketMessages, {
|
|
2285
|
+
where: {
|
|
2286
|
+
and: [
|
|
2287
|
+
{ ticket: { equals: ticketId } },
|
|
2288
|
+
{ authorType: { equals: "admin" } },
|
|
2289
|
+
{ isInternal: { equals: false } }
|
|
2290
|
+
]
|
|
2291
|
+
},
|
|
2292
|
+
sort: "-createdAt",
|
|
2293
|
+
limit: 1,
|
|
2294
|
+
depth: 0,
|
|
2295
|
+
overrideAccess: true
|
|
2296
|
+
});
|
|
2297
|
+
if (lastAdminMsg.docs.length > 0 && lastAdminMsg.docs[0].createdAt) {
|
|
2298
|
+
waitingSince = lastAdminMsg.docs[0].createdAt;
|
|
2299
|
+
}
|
|
2300
|
+
} catch {
|
|
2301
|
+
}
|
|
2302
|
+
const sinceLabel = waitingSince ? formatFr(new Date(waitingSince), false) : null;
|
|
2303
|
+
const deadlineLabel = formatFr(deadline, true);
|
|
2304
|
+
await payload.sendEmail({
|
|
2305
|
+
to: client.email,
|
|
2306
|
+
...replyTo ? { replyTo } : {},
|
|
2307
|
+
subject: `Rappel : [${ticketNumber}] ${subject} \u2014 votre r\xE9ponse est attendue`,
|
|
2308
|
+
html: emailWrapper(`Votre ticket attend votre r\xE9ponse`, [
|
|
2309
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
|
|
2310
|
+
emailParagraph(
|
|
2311
|
+
`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)}` : ""}.`
|
|
2312
|
+
),
|
|
2313
|
+
emailParagraph(
|
|
2314
|
+
`<strong>Sans retour de votre part, ce ticket sera automatiquement cl\xF4tur\xE9 le ${escapeHtml(deadlineLabel)}.</strong>`
|
|
2315
|
+
),
|
|
2316
|
+
emailParagraph(
|
|
2317
|
+
`Si vous avez encore besoin d'assistance, il vous suffit de r\xE9pondre \xE0 ce message \u2014 votre r\xE9ponse maintiendra le ticket ouvert.`
|
|
2318
|
+
),
|
|
2319
|
+
emailButton("R\xE9pondre au ticket", portalUrl, "primary")
|
|
2320
|
+
].join(""), {
|
|
2321
|
+
kind: "alert",
|
|
2322
|
+
preheader: `Sans r\xE9ponse de votre part, votre ticket ${ticketNumber} sera cl\xF4tur\xE9 le ${deadlineLabel}.`
|
|
2323
|
+
})
|
|
2324
|
+
});
|
|
2325
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2326
|
+
data: {
|
|
2327
|
+
ticket: ticketId,
|
|
2328
|
+
body: `Relance envoy\xE9e \xE0 ${client.email}. Fermeture automatique programm\xE9e le ${deadlineLabel} sans r\xE9ponse du client.`,
|
|
2329
|
+
authorType: "admin",
|
|
2330
|
+
isInternal: true,
|
|
2331
|
+
skipNotification: true
|
|
2332
|
+
},
|
|
2333
|
+
overrideAccess: true
|
|
2334
|
+
});
|
|
2335
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2336
|
+
id: ticketId,
|
|
2337
|
+
data: {
|
|
2338
|
+
status: "waiting_client",
|
|
2339
|
+
autoCloseRemindedAt: now.toISOString(),
|
|
2340
|
+
autoCloseScheduledAt: deadline.toISOString()
|
|
2341
|
+
},
|
|
2342
|
+
overrideAccess: true
|
|
2343
|
+
});
|
|
2344
|
+
return Response.json({
|
|
2345
|
+
success: true,
|
|
2346
|
+
sentTo: client.email,
|
|
2347
|
+
scheduledCloseAt: deadline.toISOString()
|
|
2348
|
+
});
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
const authResponse = handleAuthError(error);
|
|
2351
|
+
if (authResponse) return authResponse;
|
|
2352
|
+
console.error("[send-reminder] Error:", error);
|
|
2353
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// src/endpoints/statuses.ts
|
|
2360
|
+
function createStatusesEndpoint(slugs) {
|
|
2361
|
+
return {
|
|
2362
|
+
path: "/support/statuses",
|
|
2363
|
+
method: "get",
|
|
2364
|
+
handler: async (req) => {
|
|
2365
|
+
try {
|
|
2366
|
+
const payload = req.payload;
|
|
2367
|
+
if (!req.user) {
|
|
2368
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2369
|
+
}
|
|
2370
|
+
const { docs } = await dbFind(payload, slugs.ticketStatuses, {
|
|
2371
|
+
sort: "sortOrder",
|
|
2372
|
+
limit: 100,
|
|
2373
|
+
depth: 0,
|
|
2374
|
+
overrideAccess: true
|
|
2375
|
+
});
|
|
2376
|
+
return Response.json({
|
|
2377
|
+
statuses: docs.map((s) => ({
|
|
2378
|
+
id: s.id,
|
|
1962
2379
|
name: s.name,
|
|
1963
2380
|
slug: s.slug,
|
|
1964
2381
|
color: s.color,
|
|
@@ -1989,16 +2406,14 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
1989
2406
|
if (!macroId || !ticketId) {
|
|
1990
2407
|
return Response.json({ error: "macroId and ticketId are required" }, { status: 400 });
|
|
1991
2408
|
}
|
|
1992
|
-
const macro = await payload.
|
|
1993
|
-
collection: slugs.macros,
|
|
2409
|
+
const macro = await dbFindByID(payload, slugs.macros, {
|
|
1994
2410
|
id: macroId,
|
|
1995
2411
|
depth: 0,
|
|
1996
2412
|
overrideAccess: true
|
|
1997
2413
|
});
|
|
1998
2414
|
if (!macro) return Response.json({ error: "Macro not found" }, { status: 404 });
|
|
1999
2415
|
if (!macro.isActive) return Response.json({ error: "Macro is disabled" }, { status: 400 });
|
|
2000
|
-
const ticket = await payload.
|
|
2001
|
-
collection: slugs.tickets,
|
|
2416
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2002
2417
|
id: ticketId,
|
|
2003
2418
|
depth: 0,
|
|
2004
2419
|
overrideAccess: true
|
|
@@ -2010,8 +2425,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2010
2425
|
try {
|
|
2011
2426
|
switch (action.type) {
|
|
2012
2427
|
case "set_status":
|
|
2013
|
-
await payload.
|
|
2014
|
-
collection: slugs.tickets,
|
|
2428
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2015
2429
|
id: ticketId,
|
|
2016
2430
|
data: { status: action.value },
|
|
2017
2431
|
overrideAccess: true
|
|
@@ -2019,8 +2433,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2019
2433
|
appliedActions.push({ type: action.type, value: action.value, success: true });
|
|
2020
2434
|
break;
|
|
2021
2435
|
case "set_priority":
|
|
2022
|
-
await payload.
|
|
2023
|
-
collection: slugs.tickets,
|
|
2436
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2024
2437
|
id: ticketId,
|
|
2025
2438
|
data: { priority: action.value },
|
|
2026
2439
|
overrideAccess: true
|
|
@@ -2032,8 +2445,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2032
2445
|
if (!currentTags.includes(action.value)) {
|
|
2033
2446
|
currentTags.push(action.value);
|
|
2034
2447
|
}
|
|
2035
|
-
await payload.
|
|
2036
|
-
collection: slugs.tickets,
|
|
2448
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2037
2449
|
id: ticketId,
|
|
2038
2450
|
data: { tags: currentTags },
|
|
2039
2451
|
overrideAccess: true
|
|
@@ -2042,8 +2454,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2042
2454
|
break;
|
|
2043
2455
|
}
|
|
2044
2456
|
case "send_reply":
|
|
2045
|
-
await payload.
|
|
2046
|
-
collection: slugs.ticketMessages,
|
|
2457
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2047
2458
|
data: {
|
|
2048
2459
|
ticket: ticketId,
|
|
2049
2460
|
body: action.value,
|
|
@@ -2060,8 +2471,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2060
2471
|
appliedActions.push({ type: action.type, value: action.value, success: false, error: "Invalid user ID" });
|
|
2061
2472
|
break;
|
|
2062
2473
|
}
|
|
2063
|
-
await payload.
|
|
2064
|
-
collection: slugs.tickets,
|
|
2474
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2065
2475
|
id: ticketId,
|
|
2066
2476
|
data: { assignedTo: userId },
|
|
2067
2477
|
overrideAccess: true
|
|
@@ -2113,8 +2523,7 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2113
2523
|
return Response.json({ error: "Invalid collection. Use email-logs or auth-logs." }, { status: 400 });
|
|
2114
2524
|
}
|
|
2115
2525
|
const cutoff = days > 0 ? new Date(Date.now() - days * 864e5).toISOString() : null;
|
|
2116
|
-
const result = await payload
|
|
2117
|
-
collection: allowedCollections[collection],
|
|
2526
|
+
const result = await dbDelete(payload, allowedCollections[collection], {
|
|
2118
2527
|
where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
|
|
2119
2528
|
overrideAccess: true
|
|
2120
2529
|
});
|
|
@@ -2134,50 +2543,6 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2134
2543
|
};
|
|
2135
2544
|
}
|
|
2136
2545
|
|
|
2137
|
-
// src/utils/rateLimiter.ts
|
|
2138
|
-
var RateLimiter = class {
|
|
2139
|
-
constructor(windowMs, maxRequests) {
|
|
2140
|
-
this.windowMs = windowMs;
|
|
2141
|
-
this.maxRequests = maxRequests;
|
|
2142
|
-
const timer = setInterval(() => this.cleanup(), windowMs);
|
|
2143
|
-
timer.unref();
|
|
2144
|
-
}
|
|
2145
|
-
windowMs;
|
|
2146
|
-
maxRequests;
|
|
2147
|
-
store = /* @__PURE__ */ new Map();
|
|
2148
|
-
/**
|
|
2149
|
-
* Check if a key has exceeded the rate limit.
|
|
2150
|
-
* Returns true if the request should be blocked.
|
|
2151
|
-
*/
|
|
2152
|
-
check(key) {
|
|
2153
|
-
const now = Date.now();
|
|
2154
|
-
const entry = this.store.get(key);
|
|
2155
|
-
if (!entry || now > entry.resetAt) {
|
|
2156
|
-
this.store.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
2157
|
-
return false;
|
|
2158
|
-
}
|
|
2159
|
-
entry.count++;
|
|
2160
|
-
return entry.count > this.maxRequests;
|
|
2161
|
-
}
|
|
2162
|
-
/**
|
|
2163
|
-
* Reset the counter for a specific key.
|
|
2164
|
-
*/
|
|
2165
|
-
reset(key) {
|
|
2166
|
-
this.store.delete(key);
|
|
2167
|
-
}
|
|
2168
|
-
/**
|
|
2169
|
-
* Remove expired entries from the store.
|
|
2170
|
-
*/
|
|
2171
|
-
cleanup() {
|
|
2172
|
-
const now = Date.now();
|
|
2173
|
-
for (const [key, entry] of this.store) {
|
|
2174
|
-
if (now > entry.resetAt) {
|
|
2175
|
-
this.store.delete(key);
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
};
|
|
2180
|
-
|
|
2181
2546
|
// src/endpoints/chatbot.ts
|
|
2182
2547
|
var chatbotLimiter = new RateLimiter(6e4, 10);
|
|
2183
2548
|
function createChatbotEndpoint(slugs) {
|
|
@@ -2201,8 +2566,7 @@ function createChatbotEndpoint(slugs) {
|
|
|
2201
2566
|
return Response.json({ error: "Question too short" }, { status: 400 });
|
|
2202
2567
|
}
|
|
2203
2568
|
const payload = req.payload;
|
|
2204
|
-
const articles = await payload.
|
|
2205
|
-
collection: slugs.knowledgeBase,
|
|
2569
|
+
const articles = await dbFind(payload, slugs.knowledgeBase, {
|
|
2206
2570
|
where: { published: { equals: true } },
|
|
2207
2571
|
limit: 100,
|
|
2208
2572
|
depth: 0,
|
|
@@ -2292,8 +2656,7 @@ function createChatGetEndpoint(slugs) {
|
|
|
2292
2656
|
if (after) {
|
|
2293
2657
|
where.createdAt = { greater_than: after };
|
|
2294
2658
|
}
|
|
2295
|
-
const messages = await payload.
|
|
2296
|
-
collection: slugs.chatMessages,
|
|
2659
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2297
2660
|
where,
|
|
2298
2661
|
sort: "createdAt",
|
|
2299
2662
|
limit: 100,
|
|
@@ -2336,8 +2699,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2336
2699
|
return Response.json({ error: "Trop de sessions cr\xE9\xE9es. R\xE9essayez plus tard." }, { status: 429 });
|
|
2337
2700
|
}
|
|
2338
2701
|
const sessionId = `chat_${crypto3.randomBytes(16).toString("hex")}`;
|
|
2339
|
-
const systemMsg = await payload.
|
|
2340
|
-
collection: slugs.chatMessages,
|
|
2702
|
+
const systemMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2341
2703
|
data: {
|
|
2342
2704
|
session: sessionId,
|
|
2343
2705
|
client: req.user.id,
|
|
@@ -2357,8 +2719,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2357
2719
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2358
2720
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2359
2721
|
}
|
|
2360
|
-
const existing = await payload.
|
|
2361
|
-
collection: slugs.chatMessages,
|
|
2722
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2362
2723
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2363
2724
|
limit: 1,
|
|
2364
2725
|
overrideAccess: true
|
|
@@ -2366,8 +2727,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2366
2727
|
if (existing.docs.length === 0) {
|
|
2367
2728
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2368
2729
|
}
|
|
2369
|
-
const newMsg = await payload.
|
|
2370
|
-
collection: slugs.chatMessages,
|
|
2730
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2371
2731
|
data: {
|
|
2372
2732
|
session,
|
|
2373
2733
|
client: req.user.id,
|
|
@@ -2380,8 +2740,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2380
2740
|
return Response.json({ message: newMsg });
|
|
2381
2741
|
}
|
|
2382
2742
|
if (action === "close" && session) {
|
|
2383
|
-
const existing = await payload.
|
|
2384
|
-
collection: slugs.chatMessages,
|
|
2743
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2385
2744
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2386
2745
|
limit: 1,
|
|
2387
2746
|
overrideAccess: true
|
|
@@ -2389,8 +2748,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2389
2748
|
if (existing.docs.length === 0) {
|
|
2390
2749
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2391
2750
|
}
|
|
2392
|
-
await payload.
|
|
2393
|
-
collection: slugs.chatMessages,
|
|
2751
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2394
2752
|
data: {
|
|
2395
2753
|
session,
|
|
2396
2754
|
client: req.user.id,
|
|
@@ -2432,8 +2790,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2432
2790
|
if (!sessionId) {
|
|
2433
2791
|
return Response.json({ error: "Missing session parameter" }, { status: 400 });
|
|
2434
2792
|
}
|
|
2435
|
-
const existing = await req.payload.
|
|
2436
|
-
collection: slugs.chatMessages,
|
|
2793
|
+
const existing = await dbFind(req.payload, slugs.chatMessages, {
|
|
2437
2794
|
where: {
|
|
2438
2795
|
session: { equals: sessionId },
|
|
2439
2796
|
client: { equals: userId }
|
|
@@ -2455,8 +2812,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2455
2812
|
);
|
|
2456
2813
|
const interval = setInterval(async () => {
|
|
2457
2814
|
try {
|
|
2458
|
-
const messages = await req.payload.
|
|
2459
|
-
collection: slugs.chatMessages,
|
|
2815
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2460
2816
|
where: {
|
|
2461
2817
|
session: { equals: sessionId },
|
|
2462
2818
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2537,8 +2893,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2537
2893
|
if (session) {
|
|
2538
2894
|
const where = { session: { equals: session } };
|
|
2539
2895
|
if (after) where.createdAt = { greater_than: after };
|
|
2540
|
-
const messages = await payload.
|
|
2541
|
-
collection: slugs.chatMessages,
|
|
2896
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2542
2897
|
where,
|
|
2543
2898
|
sort: "createdAt",
|
|
2544
2899
|
limit: 200,
|
|
@@ -2552,8 +2907,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2552
2907
|
}
|
|
2553
2908
|
});
|
|
2554
2909
|
}
|
|
2555
|
-
const recentMessages = await payload.
|
|
2556
|
-
collection: slugs.chatMessages,
|
|
2910
|
+
const recentMessages = await dbFind(payload, slugs.chatMessages, {
|
|
2557
2911
|
sort: "-createdAt",
|
|
2558
2912
|
limit: 50,
|
|
2559
2913
|
depth: 1,
|
|
@@ -2616,8 +2970,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2616
2970
|
if (!session) {
|
|
2617
2971
|
return Response.json({ error: "Session requise" }, { status: 400 });
|
|
2618
2972
|
}
|
|
2619
|
-
const sessionMsg = await payload.
|
|
2620
|
-
collection: slugs.chatMessages,
|
|
2973
|
+
const sessionMsg = await dbFind(payload, slugs.chatMessages, {
|
|
2621
2974
|
where: { session: { equals: session } },
|
|
2622
2975
|
limit: 1,
|
|
2623
2976
|
depth: 0,
|
|
@@ -2635,8 +2988,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2635
2988
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2636
2989
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2637
2990
|
}
|
|
2638
|
-
const newMsg = await payload.
|
|
2639
|
-
collection: slugs.chatMessages,
|
|
2991
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2640
2992
|
data: {
|
|
2641
2993
|
session,
|
|
2642
2994
|
client: clientId,
|
|
@@ -2648,8 +3000,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2648
3000
|
overrideAccess: true
|
|
2649
3001
|
});
|
|
2650
3002
|
try {
|
|
2651
|
-
const linkedTicket = await payload.
|
|
2652
|
-
collection: slugs.tickets,
|
|
3003
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2653
3004
|
where: { chatSession: { equals: session } },
|
|
2654
3005
|
limit: 1,
|
|
2655
3006
|
depth: 0,
|
|
@@ -2657,8 +3008,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2657
3008
|
});
|
|
2658
3009
|
if (linkedTicket.docs.length > 0) {
|
|
2659
3010
|
const ticketId = linkedTicket.docs[0].id;
|
|
2660
|
-
await payload.
|
|
2661
|
-
collection: slugs.ticketMessages,
|
|
3011
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2662
3012
|
data: {
|
|
2663
3013
|
ticket: ticketId,
|
|
2664
3014
|
body: trimmedMessage,
|
|
@@ -2667,8 +3017,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2667
3017
|
},
|
|
2668
3018
|
overrideAccess: true
|
|
2669
3019
|
});
|
|
2670
|
-
await payload.
|
|
2671
|
-
collection: slugs.chatMessages,
|
|
3020
|
+
await dbUpdate(payload, slugs.chatMessages, {
|
|
2672
3021
|
id: newMsg.id,
|
|
2673
3022
|
data: { ticket: ticketId },
|
|
2674
3023
|
overrideAccess: true
|
|
@@ -2680,8 +3029,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2680
3029
|
return Response.json({ message: newMsg });
|
|
2681
3030
|
}
|
|
2682
3031
|
if (action === "close") {
|
|
2683
|
-
await payload.
|
|
2684
|
-
collection: slugs.chatMessages,
|
|
3032
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2685
3033
|
data: {
|
|
2686
3034
|
session,
|
|
2687
3035
|
client: clientId,
|
|
@@ -2692,16 +3040,14 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2692
3040
|
overrideAccess: true
|
|
2693
3041
|
});
|
|
2694
3042
|
try {
|
|
2695
|
-
const linkedTicket = await payload.
|
|
2696
|
-
collection: slugs.tickets,
|
|
3043
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2697
3044
|
where: { chatSession: { equals: session } },
|
|
2698
3045
|
limit: 1,
|
|
2699
3046
|
depth: 0,
|
|
2700
3047
|
overrideAccess: true
|
|
2701
3048
|
});
|
|
2702
3049
|
if (linkedTicket.docs.length > 0) {
|
|
2703
|
-
await payload.
|
|
2704
|
-
collection: slugs.ticketMessages,
|
|
3050
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2705
3051
|
data: {
|
|
2706
3052
|
ticket: linkedTicket.docs[0].id,
|
|
2707
3053
|
body: "Session de chat termin\xE9e par un agent.",
|
|
@@ -2764,8 +3110,7 @@ function createSessionStream(req, slugs, sessionId) {
|
|
|
2764
3110
|
);
|
|
2765
3111
|
const interval = setInterval(async () => {
|
|
2766
3112
|
try {
|
|
2767
|
-
const messages = await req.payload.
|
|
2768
|
-
collection: slugs.chatMessages,
|
|
3113
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2769
3114
|
where: {
|
|
2770
3115
|
session: { equals: sessionId },
|
|
2771
3116
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2838,8 +3183,7 @@ function createSessionListStream(req, slugs) {
|
|
|
2838
3183
|
);
|
|
2839
3184
|
const interval = setInterval(async () => {
|
|
2840
3185
|
try {
|
|
2841
|
-
const recentMessages = await req.payload.
|
|
2842
|
-
collection: slugs.chatMessages,
|
|
3186
|
+
const recentMessages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2843
3187
|
sort: "-createdAt",
|
|
2844
3188
|
limit: 50,
|
|
2845
3189
|
depth: 1,
|
|
@@ -2926,8 +3270,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
2926
3270
|
const statuses = ["open", "waiting_client", "resolved"];
|
|
2927
3271
|
const statusCounts = await Promise.all(
|
|
2928
3272
|
statuses.map(async (status) => {
|
|
2929
|
-
const result = await payload.
|
|
2930
|
-
collection: slugs.tickets,
|
|
3273
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
2931
3274
|
where: { status: { equals: status } },
|
|
2932
3275
|
overrideAccess: true
|
|
2933
3276
|
});
|
|
@@ -2939,8 +3282,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
2939
3282
|
const priorities = ["low", "normal", "high", "urgent"];
|
|
2940
3283
|
const priorityCounts = await Promise.all(
|
|
2941
3284
|
priorities.map(async (priority) => {
|
|
2942
|
-
const result = await payload.
|
|
2943
|
-
collection: slugs.tickets,
|
|
3285
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
2944
3286
|
where: { priority: { equals: priority } },
|
|
2945
3287
|
overrideAccess: true
|
|
2946
3288
|
});
|
|
@@ -2951,8 +3293,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
2951
3293
|
const categories = ["bug", "content", "feature", "question", "hosting"];
|
|
2952
3294
|
const categoryCounts = await Promise.all(
|
|
2953
3295
|
categories.map(async (category) => {
|
|
2954
|
-
const result = await payload.
|
|
2955
|
-
collection: slugs.tickets,
|
|
3296
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
2956
3297
|
where: { category: { equals: category } },
|
|
2957
3298
|
overrideAccess: true
|
|
2958
3299
|
});
|
|
@@ -2966,17 +3307,34 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
2966
3307
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
|
|
2967
3308
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
2968
3309
|
const [created7, created30] = await Promise.all([
|
|
2969
|
-
payload.
|
|
2970
|
-
collection: slugs.tickets,
|
|
3310
|
+
dbCount(payload, slugs.tickets, {
|
|
2971
3311
|
where: { createdAt: { greater_than_equal: sevenDaysAgo.toISOString() } },
|
|
2972
3312
|
overrideAccess: true
|
|
2973
3313
|
}),
|
|
2974
|
-
payload.
|
|
2975
|
-
collection: slugs.tickets,
|
|
3314
|
+
dbCount(payload, slugs.tickets, {
|
|
2976
3315
|
where: { createdAt: { greater_than_equal: thirtyDaysAgo.toISOString() } },
|
|
2977
3316
|
overrideAccess: true
|
|
2978
3317
|
})
|
|
2979
3318
|
]);
|
|
3319
|
+
const volumeByDay = await Promise.all(
|
|
3320
|
+
Array.from({ length: 7 }, (_, i) => i).map(async (offset) => {
|
|
3321
|
+
const dayStart = new Date(now);
|
|
3322
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
3323
|
+
dayStart.setDate(dayStart.getDate() - (6 - offset));
|
|
3324
|
+
const dayEnd = new Date(dayStart);
|
|
3325
|
+
dayEnd.setDate(dayEnd.getDate() + 1);
|
|
3326
|
+
const r = await dbCount(payload, slugs.tickets, {
|
|
3327
|
+
where: {
|
|
3328
|
+
and: [
|
|
3329
|
+
{ createdAt: { greater_than_equal: dayStart.toISOString() } },
|
|
3330
|
+
{ createdAt: { less_than: dayEnd.toISOString() } }
|
|
3331
|
+
]
|
|
3332
|
+
},
|
|
3333
|
+
overrideAccess: true
|
|
3334
|
+
});
|
|
3335
|
+
return { date: dayStart.toISOString(), count: r.totalDocs };
|
|
3336
|
+
})
|
|
3337
|
+
);
|
|
2980
3338
|
const PAGE_SIZE2 = 100;
|
|
2981
3339
|
const MAX_PAGES2 = 50;
|
|
2982
3340
|
let totalResponseTimeMs = 0;
|
|
@@ -2987,8 +3345,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
2987
3345
|
let page = 1;
|
|
2988
3346
|
let hasMore = true;
|
|
2989
3347
|
while (hasMore && page <= MAX_PAGES2) {
|
|
2990
|
-
const batch = await payload.
|
|
2991
|
-
collection: slugs.tickets,
|
|
3348
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
2992
3349
|
limit: PAGE_SIZE2,
|
|
2993
3350
|
page,
|
|
2994
3351
|
depth: 0,
|
|
@@ -3021,43 +3378,62 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3021
3378
|
hasMore = batch.hasNextPage ?? false;
|
|
3022
3379
|
page++;
|
|
3023
3380
|
}
|
|
3024
|
-
const surveyCount = await payload.
|
|
3025
|
-
collection: slugs.satisfactionSurveys,
|
|
3381
|
+
const surveyCount = await dbCount(payload, slugs.satisfactionSurveys, {
|
|
3026
3382
|
overrideAccess: true
|
|
3027
3383
|
});
|
|
3028
3384
|
let satisfactionAvg = 0;
|
|
3385
|
+
let satisfactionCount = 0;
|
|
3386
|
+
let npsScore = null;
|
|
3387
|
+
let npsCount = 0;
|
|
3029
3388
|
if (surveyCount.totalDocs > 0) {
|
|
3030
|
-
let
|
|
3389
|
+
let ratingSum = 0;
|
|
3390
|
+
let ratingN = 0;
|
|
3391
|
+
let promoters = 0;
|
|
3392
|
+
let detractors = 0;
|
|
3393
|
+
let npsN = 0;
|
|
3031
3394
|
let surveyPage = 1;
|
|
3032
3395
|
let surveyHasMore = true;
|
|
3033
3396
|
while (surveyHasMore && surveyPage <= MAX_PAGES2) {
|
|
3034
|
-
const batch = await payload.
|
|
3035
|
-
collection: slugs.satisfactionSurveys,
|
|
3397
|
+
const batch = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
3036
3398
|
limit: PAGE_SIZE2,
|
|
3037
3399
|
page: surveyPage,
|
|
3038
3400
|
depth: 0,
|
|
3039
3401
|
overrideAccess: true,
|
|
3040
|
-
select: { rating: true }
|
|
3402
|
+
select: { rating: true, nps: true }
|
|
3041
3403
|
});
|
|
3042
3404
|
for (const s of batch.docs) {
|
|
3043
|
-
|
|
3405
|
+
const r = s.rating;
|
|
3406
|
+
if (typeof r === "number" && r > 0) {
|
|
3407
|
+
ratingSum += r;
|
|
3408
|
+
ratingN++;
|
|
3409
|
+
}
|
|
3410
|
+
const n = s.nps;
|
|
3411
|
+
if (typeof n === "number") {
|
|
3412
|
+
npsN++;
|
|
3413
|
+
if (n >= 9) promoters++;
|
|
3414
|
+
else if (n <= 6) detractors++;
|
|
3415
|
+
}
|
|
3044
3416
|
}
|
|
3045
3417
|
surveyHasMore = batch.hasNextPage ?? false;
|
|
3046
3418
|
surveyPage++;
|
|
3047
3419
|
}
|
|
3048
|
-
satisfactionAvg = Math.round(
|
|
3420
|
+
satisfactionAvg = ratingN > 0 ? Math.round(ratingSum / ratingN * 10) / 10 : 0;
|
|
3421
|
+
satisfactionCount = ratingN;
|
|
3422
|
+
if (npsN > 0) {
|
|
3423
|
+
npsScore = Math.round((promoters - detractors) / npsN * 100);
|
|
3424
|
+
npsCount = npsN;
|
|
3425
|
+
}
|
|
3049
3426
|
}
|
|
3050
|
-
const
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
}),
|
|
3055
|
-
payload.count({
|
|
3056
|
-
collection: slugs.pendingEmails,
|
|
3427
|
+
const clientCount = await dbCount(payload, slugs.supportClients, { overrideAccess: true });
|
|
3428
|
+
let pendingEmailsTotal = 0;
|
|
3429
|
+
try {
|
|
3430
|
+
const pe = await dbCount(payload, slugs.pendingEmails, {
|
|
3057
3431
|
where: { status: { equals: "pending" } },
|
|
3058
3432
|
overrideAccess: true
|
|
3059
|
-
})
|
|
3060
|
-
|
|
3433
|
+
});
|
|
3434
|
+
pendingEmailsTotal = pe.totalDocs;
|
|
3435
|
+
} catch {
|
|
3436
|
+
}
|
|
3061
3437
|
return new Response(JSON.stringify({
|
|
3062
3438
|
total,
|
|
3063
3439
|
byStatus,
|
|
@@ -3065,13 +3441,16 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3065
3441
|
byCategory,
|
|
3066
3442
|
createdLast7Days: created7.totalDocs,
|
|
3067
3443
|
createdLast30Days: created30.totalDocs,
|
|
3444
|
+
volumeByDay,
|
|
3068
3445
|
avgResponseTimeHours: responseTimeCount > 0 ? Math.round(totalResponseTimeMs / responseTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3069
3446
|
avgResolutionTimeHours: resolutionTimeCount > 0 ? Math.round(totalResolutionTimeMs / resolutionTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3070
3447
|
totalTimeMinutes,
|
|
3071
3448
|
satisfactionAvg,
|
|
3072
|
-
satisfactionCount
|
|
3449
|
+
satisfactionCount,
|
|
3450
|
+
npsScore,
|
|
3451
|
+
npsCount,
|
|
3073
3452
|
clientCount: clientCount.totalDocs,
|
|
3074
|
-
pendingEmailsCount:
|
|
3453
|
+
pendingEmailsCount: pendingEmailsTotal
|
|
3075
3454
|
}), {
|
|
3076
3455
|
headers: {
|
|
3077
3456
|
"Content-Type": "application/json",
|
|
@@ -3141,13 +3520,26 @@ function createBillingEndpoint(slugs) {
|
|
|
3141
3520
|
let ticketPage = 1;
|
|
3142
3521
|
let ticketHasMore = true;
|
|
3143
3522
|
while (ticketHasMore && ticketPage <= MAX_PAGES) {
|
|
3144
|
-
const batch = await payload.
|
|
3145
|
-
collection: slugs.tickets,
|
|
3523
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3146
3524
|
where: ticketWhere,
|
|
3147
3525
|
limit: PAGE_SIZE,
|
|
3148
3526
|
page: ticketPage,
|
|
3527
|
+
// depth 2 is required: we read ticket.project.client.company below.
|
|
3149
3528
|
depth: 2,
|
|
3150
|
-
overrideAccess: true
|
|
3529
|
+
overrideAccess: true,
|
|
3530
|
+
// Bound the payload to only the fields the report consumes (skips body,
|
|
3531
|
+
// richText, SLA fields, etc.) while keeping project/client populated.
|
|
3532
|
+
select: {
|
|
3533
|
+
ticketNumber: true,
|
|
3534
|
+
subject: true,
|
|
3535
|
+
status: true,
|
|
3536
|
+
billedAmount: true,
|
|
3537
|
+
aiSummary: true,
|
|
3538
|
+
aiSummaryGeneratedAt: true,
|
|
3539
|
+
aiSummaryStatus: true,
|
|
3540
|
+
project: true,
|
|
3541
|
+
client: true
|
|
3542
|
+
}
|
|
3151
3543
|
});
|
|
3152
3544
|
allTickets.push(...batch.docs);
|
|
3153
3545
|
ticketHasMore = batch.hasNextPage ?? false;
|
|
@@ -3157,8 +3549,7 @@ function createBillingEndpoint(slugs) {
|
|
|
3157
3549
|
let entryPage = 1;
|
|
3158
3550
|
let entryHasMore = true;
|
|
3159
3551
|
while (entryHasMore && entryPage <= MAX_PAGES) {
|
|
3160
|
-
const batch = await payload.
|
|
3161
|
-
collection: slugs.timeEntries,
|
|
3552
|
+
const batch = await dbFind(payload, slugs.timeEntries, {
|
|
3162
3553
|
where: {
|
|
3163
3554
|
and: [
|
|
3164
3555
|
{ date: { greater_than_equal: from } },
|
|
@@ -3256,9 +3647,174 @@ function createBillingEndpoint(slugs) {
|
|
|
3256
3647
|
}
|
|
3257
3648
|
};
|
|
3258
3649
|
}
|
|
3650
|
+
var fmtAmount = (n) => `${n.toFixed(2)} EUR`;
|
|
3651
|
+
var fmtTime = (min) => `${Math.floor(min / 60)}h${String(min % 60).padStart(2, "0")}`;
|
|
3652
|
+
function generateInvoicePdf(data) {
|
|
3653
|
+
return new Promise((resolve, reject) => {
|
|
3654
|
+
try {
|
|
3655
|
+
const doc = new PDFDocument({ size: "A4", margin: 50 });
|
|
3656
|
+
const chunks = [];
|
|
3657
|
+
doc.on("data", (c) => chunks.push(c));
|
|
3658
|
+
doc.on("end", () => resolve(Buffer.concat(chunks)));
|
|
3659
|
+
doc.on("error", reject);
|
|
3660
|
+
doc.fillColor("#0f172a").fontSize(22).text("Invoice / Pre-billing");
|
|
3661
|
+
doc.moveDown(0.2);
|
|
3662
|
+
doc.fillColor("#64748b").fontSize(10).text(`Period: ${data.from} -> ${data.to}${data.projectId ? ` \xB7 Project #${data.projectId}` : ""}`);
|
|
3663
|
+
doc.moveDown(1);
|
|
3664
|
+
const X = { ticket: 50, subject: 130, client: 320, time: 430, amount: 500 };
|
|
3665
|
+
const drawRow = (r, opts) => {
|
|
3666
|
+
const y = doc.y;
|
|
3667
|
+
doc.fontSize(opts?.header ? 9 : 10).fillColor(opts?.header ? "#475569" : "#0f172a");
|
|
3668
|
+
if (opts?.total) doc.font("Helvetica-Bold");
|
|
3669
|
+
doc.text(r.ticket, X.ticket, y, { width: X.subject - X.ticket - 6 });
|
|
3670
|
+
doc.text(r.subject, X.subject, y, { width: X.client - X.subject - 6 });
|
|
3671
|
+
doc.text(r.client, X.client, y, { width: X.time - X.client - 6 });
|
|
3672
|
+
doc.text(r.time, X.time, y, { width: X.amount - X.time - 6, align: "right" });
|
|
3673
|
+
doc.text(r.amount, X.amount, y, { width: 545 - X.amount, align: "right" });
|
|
3674
|
+
doc.font("Helvetica");
|
|
3675
|
+
doc.moveDown(0.4);
|
|
3676
|
+
};
|
|
3677
|
+
drawRow({ ticket: "Ticket", subject: "Subject", client: "Client", time: "Time", amount: "Amount" }, { header: true });
|
|
3678
|
+
doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor("#e2e8f0").stroke();
|
|
3679
|
+
doc.moveDown(0.3);
|
|
3680
|
+
for (const l of data.lines) {
|
|
3681
|
+
if (doc.y > 760) doc.addPage();
|
|
3682
|
+
drawRow({
|
|
3683
|
+
ticket: l.ticketNumber || "",
|
|
3684
|
+
subject: l.subject || "",
|
|
3685
|
+
client: l.client || "",
|
|
3686
|
+
time: fmtTime(l.minutes),
|
|
3687
|
+
amount: fmtAmount(l.amount)
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor("#0f172a").stroke();
|
|
3691
|
+
doc.moveDown(0.3);
|
|
3692
|
+
drawRow({
|
|
3693
|
+
ticket: "",
|
|
3694
|
+
subject: `Total (${data.lines.length} ticket${data.lines.length > 1 ? "s" : ""})`,
|
|
3695
|
+
client: "",
|
|
3696
|
+
time: fmtTime(data.totalMinutes),
|
|
3697
|
+
amount: fmtAmount(data.totalAmount)
|
|
3698
|
+
}, { total: true });
|
|
3699
|
+
doc.end();
|
|
3700
|
+
} catch (err) {
|
|
3701
|
+
reject(err);
|
|
3702
|
+
}
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
// src/endpoints/invoice.ts
|
|
3707
|
+
function createInvoiceEndpoint(slugs) {
|
|
3708
|
+
return {
|
|
3709
|
+
path: "/support/billing/invoice",
|
|
3710
|
+
method: "get",
|
|
3711
|
+
handler: async (req) => {
|
|
3712
|
+
try {
|
|
3713
|
+
requireAdmin(req, slugs);
|
|
3714
|
+
const payload = req.payload;
|
|
3715
|
+
const url = new URL(req.url);
|
|
3716
|
+
const from = url.searchParams.get("from");
|
|
3717
|
+
const to = url.searchParams.get("to");
|
|
3718
|
+
const projectId = url.searchParams.get("projectId");
|
|
3719
|
+
const format = url.searchParams.get("format") === "pdf" ? "pdf" : "html";
|
|
3720
|
+
if (!from || !to) {
|
|
3721
|
+
return Response.json({ error: "Param\xE8tres from et to requis." }, { status: 400 });
|
|
3722
|
+
}
|
|
3723
|
+
const toExclusive = new Date(to);
|
|
3724
|
+
toExclusive.setDate(toExclusive.getDate() + 1);
|
|
3725
|
+
const toIso = toExclusive.toISOString();
|
|
3726
|
+
const where = {
|
|
3727
|
+
and: [
|
|
3728
|
+
{ billable: { equals: true } },
|
|
3729
|
+
...projectId ? [{ project: { equals: Number(projectId) } }] : [],
|
|
3730
|
+
{
|
|
3731
|
+
or: [
|
|
3732
|
+
{ and: [{ updatedAt: { greater_than_equal: from } }, { updatedAt: { less_than: toIso } }] },
|
|
3733
|
+
{ and: [{ createdAt: { greater_than_equal: from } }, { createdAt: { less_than: toIso } }] },
|
|
3734
|
+
{ and: [{ resolvedAt: { greater_than_equal: from } }, { resolvedAt: { less_than: toIso } }] }
|
|
3735
|
+
]
|
|
3736
|
+
}
|
|
3737
|
+
]
|
|
3738
|
+
};
|
|
3739
|
+
const tickets = [];
|
|
3740
|
+
let page = 1;
|
|
3741
|
+
let hasMore = true;
|
|
3742
|
+
while (hasMore && page <= 50) {
|
|
3743
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3744
|
+
where,
|
|
3745
|
+
limit: 500,
|
|
3746
|
+
page,
|
|
3747
|
+
depth: 2,
|
|
3748
|
+
overrideAccess: true,
|
|
3749
|
+
select: { ticketNumber: true, subject: true, billedAmount: true, totalTimeMinutes: true, project: true, client: true }
|
|
3750
|
+
});
|
|
3751
|
+
tickets.push(...batch.docs);
|
|
3752
|
+
hasMore = batch.hasNextPage ?? false;
|
|
3753
|
+
page++;
|
|
3754
|
+
}
|
|
3755
|
+
let totalAmount = 0;
|
|
3756
|
+
let totalMinutes = 0;
|
|
3757
|
+
const lines = tickets.map((t) => {
|
|
3758
|
+
const amount = typeof t.billedAmount === "number" ? t.billedAmount : 0;
|
|
3759
|
+
const minutes = typeof t.totalTimeMinutes === "number" ? t.totalTimeMinutes : 0;
|
|
3760
|
+
totalAmount += amount;
|
|
3761
|
+
totalMinutes += minutes;
|
|
3762
|
+
const client = typeof t.client === "object" && t.client ? t.client.company || "" : "";
|
|
3763
|
+
return { ticketNumber: String(t.ticketNumber || ""), subject: String(t.subject || ""), client, minutes, amount };
|
|
3764
|
+
});
|
|
3765
|
+
if (format === "pdf") {
|
|
3766
|
+
const pdf = await generateInvoicePdf({ from, to, projectId, lines, totalMinutes, totalAmount });
|
|
3767
|
+
return new Response(new Uint8Array(pdf), {
|
|
3768
|
+
status: 200,
|
|
3769
|
+
headers: {
|
|
3770
|
+
"Content-Type": "application/pdf",
|
|
3771
|
+
"Content-Disposition": `inline; filename="invoice-${from}_${to}.pdf"`
|
|
3772
|
+
}
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
const fmtAmount2 = (n) => `${n.toFixed(2)} \u20AC`;
|
|
3776
|
+
const fmtTime2 = (min) => `${Math.floor(min / 60)}h${String(min % 60).padStart(2, "0")}`;
|
|
3777
|
+
const rows = lines.map((l) => `<tr>
|
|
3778
|
+
<td>${escapeHtml(l.ticketNumber)}</td>
|
|
3779
|
+
<td>${escapeHtml(l.subject)}</td>
|
|
3780
|
+
<td>${escapeHtml(l.client)}</td>
|
|
3781
|
+
<td style="text-align:right;">${fmtTime2(l.minutes)}</td>
|
|
3782
|
+
<td style="text-align:right;">${fmtAmount2(l.amount)}</td>
|
|
3783
|
+
</tr>`).join("");
|
|
3784
|
+
const html = `<!doctype html>
|
|
3785
|
+
<html lang="fr"><head><meta charset="utf-8"><title>Facture ${escapeHtml(from)} \u2192 ${escapeHtml(to)}</title>
|
|
3786
|
+
<style>
|
|
3787
|
+
body { font-family: system-ui, -apple-system, sans-serif; color: #1e293b; max-width: 820px; margin: 32px auto; padding: 0 24px; }
|
|
3788
|
+
h1 { font-size: 22px; margin: 0 0 4px; }
|
|
3789
|
+
.period { color: #64748b; margin-bottom: 24px; }
|
|
3790
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
3791
|
+
th, td { padding: 8px 10px; border-bottom: 1px solid #e2e8f0; text-align: left; }
|
|
3792
|
+
th { background: #f8fafc; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: #475569; }
|
|
3793
|
+
tfoot td { font-weight: 700; border-top: 2px solid #0f172a; border-bottom: none; }
|
|
3794
|
+
@media print { body { margin: 0; } }
|
|
3795
|
+
</style></head>
|
|
3796
|
+
<body>
|
|
3797
|
+
<h1>Facture / Pr\xE9-facture</h1>
|
|
3798
|
+
<div class="period">P\xE9riode : ${escapeHtml(from)} \u2192 ${escapeHtml(to)}${projectId ? ` \xB7 Projet #${escapeHtml(projectId)}` : ""}</div>
|
|
3799
|
+
<table>
|
|
3800
|
+
<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>
|
|
3801
|
+
<tbody>${rows || '<tr><td colspan="5" style="color:#94a3b8;">Aucun ticket facturable sur la p\xE9riode.</td></tr>'}</tbody>
|
|
3802
|
+
<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>
|
|
3803
|
+
</table>
|
|
3804
|
+
</body></html>`;
|
|
3805
|
+
return new Response(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
3806
|
+
} catch (error) {
|
|
3807
|
+
const authResponse = handleAuthError(error);
|
|
3808
|
+
if (authResponse) return authResponse;
|
|
3809
|
+
console.error("[invoice] Error:", error);
|
|
3810
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3259
3815
|
|
|
3260
3816
|
// src/utils/generateTicketSynthesis.ts
|
|
3261
|
-
function
|
|
3817
|
+
function getClient4(aiSettings) {
|
|
3262
3818
|
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
3263
3819
|
if (aiSettings.provider === "ollama") {
|
|
3264
3820
|
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
@@ -3275,8 +3831,7 @@ async function generateTicketSynthesis(args) {
|
|
|
3275
3831
|
if (settings.ai.enableSynthesis === false) {
|
|
3276
3832
|
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "skipped", reason: "synthesis disabled" };
|
|
3277
3833
|
}
|
|
3278
|
-
const ticket = await payload.
|
|
3279
|
-
collection: slugs.tickets,
|
|
3834
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
3280
3835
|
id: ticketId,
|
|
3281
3836
|
depth: 1,
|
|
3282
3837
|
overrideAccess: true
|
|
@@ -3284,15 +3839,13 @@ async function generateTicketSynthesis(args) {
|
|
|
3284
3839
|
if (!ticket) {
|
|
3285
3840
|
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "error", reason: "ticket not found" };
|
|
3286
3841
|
}
|
|
3287
|
-
await payload.
|
|
3288
|
-
collection: slugs.tickets,
|
|
3842
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3289
3843
|
id: ticketId,
|
|
3290
3844
|
data: { aiSummaryStatus: "pending" },
|
|
3291
3845
|
overrideAccess: true
|
|
3292
3846
|
}).catch(() => {
|
|
3293
3847
|
});
|
|
3294
|
-
const messagesResult = await payload.
|
|
3295
|
-
collection: slugs.ticketMessages,
|
|
3848
|
+
const messagesResult = await dbFind(payload, slugs.ticketMessages, {
|
|
3296
3849
|
where: { ticket: { equals: ticketId } },
|
|
3297
3850
|
sort: "createdAt",
|
|
3298
3851
|
limit: 500,
|
|
@@ -3302,8 +3855,7 @@ async function generateTicketSynthesis(args) {
|
|
|
3302
3855
|
const messages = messagesResult.docs;
|
|
3303
3856
|
if (messages.length === 0) {
|
|
3304
3857
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3305
|
-
await payload.
|
|
3306
|
-
collection: slugs.tickets,
|
|
3858
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3307
3859
|
id: ticketId,
|
|
3308
3860
|
data: {
|
|
3309
3861
|
aiSummary: "(Aucun message dans ce ticket)",
|
|
@@ -3348,7 +3900,7 @@ Regles strictes :
|
|
|
3348
3900
|
- Si le ticket n'a pas abouti, decris quand meme le travail d'analyse realise
|
|
3349
3901
|
|
|
3350
3902
|
Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
3351
|
-
const anthropic =
|
|
3903
|
+
const anthropic = getClient4(settings.ai);
|
|
3352
3904
|
const model = getModel3(settings.ai);
|
|
3353
3905
|
try {
|
|
3354
3906
|
const res = await anthropic.messages.create({
|
|
@@ -3358,8 +3910,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3358
3910
|
});
|
|
3359
3911
|
const summary = res.content[0]?.type === "text" ? res.content[0].text.trim() : "";
|
|
3360
3912
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3361
|
-
await payload.
|
|
3362
|
-
collection: slugs.tickets,
|
|
3913
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3363
3914
|
id: ticketId,
|
|
3364
3915
|
data: {
|
|
3365
3916
|
aiSummary: summary,
|
|
@@ -3371,8 +3922,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3371
3922
|
return { summary, generatedAt, status: "done" };
|
|
3372
3923
|
} catch (err) {
|
|
3373
3924
|
const message = err instanceof Error ? err.message : String(err);
|
|
3374
|
-
await payload.
|
|
3375
|
-
collection: slugs.tickets,
|
|
3925
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3376
3926
|
id: ticketId,
|
|
3377
3927
|
data: { aiSummaryStatus: "error" },
|
|
3378
3928
|
overrideAccess: true
|
|
@@ -3402,8 +3952,7 @@ function createTicketSynthesisEndpoint(slugs) {
|
|
|
3402
3952
|
return Response.json({ error: "ticketId must be a number" }, { status: 400 });
|
|
3403
3953
|
}
|
|
3404
3954
|
if (!force) {
|
|
3405
|
-
const existing = await payload.
|
|
3406
|
-
collection: slugs.tickets,
|
|
3955
|
+
const existing = await dbFindByID(payload, slugs.tickets, {
|
|
3407
3956
|
id: ticketId,
|
|
3408
3957
|
depth: 0,
|
|
3409
3958
|
overrideAccess: true
|
|
@@ -3447,8 +3996,7 @@ function createEmailStatsEndpoint(slugs) {
|
|
|
3447
3996
|
let page = 1;
|
|
3448
3997
|
let hasMore = true;
|
|
3449
3998
|
while (hasMore && page <= MAX_PAGES2) {
|
|
3450
|
-
const result = await payload.
|
|
3451
|
-
collection: slugs.emailLogs,
|
|
3999
|
+
const result = await dbFind(payload, slugs.emailLogs, {
|
|
3452
4000
|
where: { createdAt: { greater_than: cutoff } },
|
|
3453
4001
|
sort: "-createdAt",
|
|
3454
4002
|
limit: 500,
|
|
@@ -3522,184 +4070,72 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3522
4070
|
const payload = req.payload;
|
|
3523
4071
|
requireClient(req, slugs);
|
|
3524
4072
|
const body = await req.json();
|
|
3525
|
-
const { ticketId, rating, comment } = body;
|
|
3526
|
-
if (!ticketId
|
|
3527
|
-
return Response.json(
|
|
3528
|
-
{ error: "ticketId et rating (entier 1-5) sont requis." },
|
|
3529
|
-
{ status: 400 }
|
|
3530
|
-
);
|
|
4073
|
+
const { ticketId, rating, nps, comment } = body;
|
|
4074
|
+
if (!ticketId) {
|
|
4075
|
+
return Response.json({ error: "ticketId est requis." }, { status: 400 });
|
|
3531
4076
|
}
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
);
|
|
4077
|
+
const hasRating = rating !== void 0 && rating !== null;
|
|
4078
|
+
const hasNps = nps !== void 0 && nps !== null;
|
|
4079
|
+
if (!hasRating && !hasNps) {
|
|
4080
|
+
return Response.json({ error: "Un rating (1-5) et/ou un nps (0-10) est requis." }, { status: 400 });
|
|
3537
4081
|
}
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
id: ticketId,
|
|
3541
|
-
depth: 0,
|
|
3542
|
-
overrideAccess: false,
|
|
3543
|
-
user: req.user
|
|
3544
|
-
});
|
|
3545
|
-
if (!ticket) {
|
|
3546
|
-
return Response.json({ error: "Ticket introuvable." }, { status: 404 });
|
|
3547
|
-
}
|
|
3548
|
-
if (ticket.status !== "resolved") {
|
|
3549
|
-
return Response.json(
|
|
3550
|
-
{ error: "Le ticket doit \xEAtre r\xE9solu pour laisser un avis." },
|
|
3551
|
-
{ status: 400 }
|
|
3552
|
-
);
|
|
4082
|
+
if (hasRating && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
4083
|
+
return Response.json({ error: "rating doit \xEAtre un entier 1-5." }, { status: 400 });
|
|
3553
4084
|
}
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
where: { ticket: { equals: ticketId } },
|
|
3557
|
-
limit: 1,
|
|
3558
|
-
depth: 0,
|
|
3559
|
-
overrideAccess: true
|
|
3560
|
-
});
|
|
3561
|
-
if (existing.docs.length > 0) {
|
|
3562
|
-
return Response.json(
|
|
3563
|
-
{ error: "Vous avez d\xE9j\xE0 \xE9valu\xE9 ce ticket." },
|
|
3564
|
-
{ status: 409 }
|
|
3565
|
-
);
|
|
4085
|
+
if (hasNps && (!Number.isInteger(nps) || nps < 0 || nps > 10)) {
|
|
4086
|
+
return Response.json({ error: "nps doit \xEAtre un entier 0-10." }, { status: 400 });
|
|
3566
4087
|
}
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
client: req.user.id,
|
|
3573
|
-
rating: Math.round(rating),
|
|
3574
|
-
...comment ? { comment: comment.trim() } : {}
|
|
3575
|
-
},
|
|
3576
|
-
overrideAccess: true
|
|
3577
|
-
});
|
|
3578
|
-
return Response.json({ success: true, survey });
|
|
3579
|
-
} catch (error) {
|
|
3580
|
-
const authResponse = handleAuthError(error);
|
|
3581
|
-
if (authResponse) return authResponse;
|
|
3582
|
-
console.error("[satisfaction] Error:", error);
|
|
3583
|
-
return Response.json({ error: "Erreur interne." }, { status: 500 });
|
|
3584
|
-
}
|
|
3585
|
-
}
|
|
3586
|
-
};
|
|
3587
|
-
}
|
|
3588
|
-
var TRANSPARENT_GIF = Buffer.from(
|
|
3589
|
-
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
3590
|
-
"base64"
|
|
3591
|
-
);
|
|
3592
|
-
function generateTrackingToken(ticketId, messageId, secret) {
|
|
3593
|
-
return createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex").substring(0, 16);
|
|
3594
|
-
}
|
|
3595
|
-
function createTrackOpenEndpoint(slugs) {
|
|
3596
|
-
return {
|
|
3597
|
-
path: "/support/track-open",
|
|
3598
|
-
method: "get",
|
|
3599
|
-
handler: async (req) => {
|
|
3600
|
-
const url = new URL(req.url);
|
|
3601
|
-
const ticketId = url.searchParams.get("t");
|
|
3602
|
-
const messageId = url.searchParams.get("m");
|
|
3603
|
-
const sig = url.searchParams.get("sig");
|
|
3604
|
-
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
3605
|
-
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
3606
|
-
const secret = process.env.PAYLOAD_SECRET || "";
|
|
3607
|
-
if (secret && ticketId && messageId && sig) {
|
|
3608
|
-
const expected = generateTrackingToken(ticketId, messageId, secret);
|
|
3609
|
-
if (sig !== expected) {
|
|
3610
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3611
|
-
status: 200,
|
|
3612
|
-
headers: {
|
|
3613
|
-
"Content-Type": "image/gif",
|
|
3614
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3615
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3616
|
-
}
|
|
3617
|
-
});
|
|
3618
|
-
}
|
|
3619
|
-
} else if (secret && (!sig || !ticketId || !messageId)) {
|
|
3620
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3621
|
-
status: 200,
|
|
3622
|
-
headers: {
|
|
3623
|
-
"Content-Type": "image/gif",
|
|
3624
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3625
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3626
|
-
}
|
|
3627
|
-
});
|
|
3628
|
-
}
|
|
3629
|
-
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
3630
|
-
try {
|
|
3631
|
-
const payload = req.payload;
|
|
3632
|
-
const ticket = await payload.findByID({
|
|
3633
|
-
collection: slugs.tickets,
|
|
3634
|
-
id: parsedId,
|
|
3635
|
-
depth: 0,
|
|
3636
|
-
overrideAccess: true,
|
|
3637
|
-
select: { lastClientReadAt: true }
|
|
3638
|
-
});
|
|
3639
|
-
if (ticket) {
|
|
3640
|
-
const lastRead = ticket.lastClientReadAt ? new Date(ticket.lastClientReadAt).getTime() : 0;
|
|
3641
|
-
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
|
|
3642
|
-
if (lastRead < fiveMinAgo) {
|
|
3643
|
-
await payload.update({
|
|
3644
|
-
collection: slugs.tickets,
|
|
3645
|
-
id: parsedId,
|
|
3646
|
-
data: { lastClientReadAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3647
|
-
overrideAccess: true
|
|
3648
|
-
});
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
3652
|
-
const msg = await payload.findByID({
|
|
3653
|
-
collection: slugs.ticketMessages,
|
|
3654
|
-
id: parsedMsgId,
|
|
3655
|
-
depth: 0,
|
|
3656
|
-
overrideAccess: true,
|
|
3657
|
-
select: { emailOpenedAt: true }
|
|
3658
|
-
});
|
|
3659
|
-
if (msg && !msg.emailOpenedAt) {
|
|
3660
|
-
await payload.update({
|
|
3661
|
-
collection: slugs.ticketMessages,
|
|
3662
|
-
id: parsedMsgId,
|
|
3663
|
-
data: { emailOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3664
|
-
overrideAccess: true
|
|
3665
|
-
});
|
|
3666
|
-
const ticketInfo = await payload.findByID({
|
|
3667
|
-
collection: slugs.tickets,
|
|
3668
|
-
id: parsedId,
|
|
3669
|
-
depth: 1,
|
|
3670
|
-
overrideAccess: true,
|
|
3671
|
-
select: { ticketNumber: true, subject: true, client: true }
|
|
3672
|
-
});
|
|
3673
|
-
const clientName = typeof ticketInfo?.client === "object" ? ticketInfo.client?.firstName || "Client" : "Client";
|
|
3674
|
-
try {
|
|
3675
|
-
await payload.create({
|
|
3676
|
-
collection: "admin-notifications",
|
|
3677
|
-
data: {
|
|
3678
|
-
title: `Email ouvert \u2014 ${ticketInfo?.ticketNumber || "TK-????"}`,
|
|
3679
|
-
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || "ticket"}"`,
|
|
3680
|
-
type: "email_opened",
|
|
3681
|
-
link: `/admin/ticket?id=${parsedId}`
|
|
3682
|
-
},
|
|
3683
|
-
overrideAccess: true
|
|
3684
|
-
});
|
|
3685
|
-
} catch {
|
|
3686
|
-
}
|
|
3687
|
-
}
|
|
3688
|
-
}
|
|
3689
|
-
} catch (err) {
|
|
3690
|
-
console.error("[track-open] Error:", err);
|
|
4088
|
+
if (comment && typeof comment === "string" && comment.length > 5e3) {
|
|
4089
|
+
return Response.json(
|
|
4090
|
+
{ error: "Le commentaire ne peut pas d\xE9passer 5000 caract\xE8res." },
|
|
4091
|
+
{ status: 400 }
|
|
4092
|
+
);
|
|
3691
4093
|
}
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
"Expires": "0"
|
|
4094
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
4095
|
+
id: ticketId,
|
|
4096
|
+
depth: 0,
|
|
4097
|
+
overrideAccess: false,
|
|
4098
|
+
user: req.user
|
|
4099
|
+
});
|
|
4100
|
+
if (!ticket) {
|
|
4101
|
+
return Response.json({ error: "Ticket introuvable." }, { status: 404 });
|
|
3701
4102
|
}
|
|
3702
|
-
|
|
4103
|
+
if (ticket.status !== "resolved") {
|
|
4104
|
+
return Response.json(
|
|
4105
|
+
{ error: "Le ticket doit \xEAtre r\xE9solu pour laisser un avis." },
|
|
4106
|
+
{ status: 400 }
|
|
4107
|
+
);
|
|
4108
|
+
}
|
|
4109
|
+
const existing = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
4110
|
+
where: { ticket: { equals: ticketId } },
|
|
4111
|
+
limit: 1,
|
|
4112
|
+
depth: 0,
|
|
4113
|
+
overrideAccess: true
|
|
4114
|
+
});
|
|
4115
|
+
if (existing.docs.length > 0) {
|
|
4116
|
+
return Response.json(
|
|
4117
|
+
{ error: "Vous avez d\xE9j\xE0 \xE9valu\xE9 ce ticket." },
|
|
4118
|
+
{ status: 409 }
|
|
4119
|
+
);
|
|
4120
|
+
}
|
|
4121
|
+
const survey = await dbCreate(payload, slugs.satisfactionSurveys, {
|
|
4122
|
+
data: {
|
|
4123
|
+
source: "ticket",
|
|
4124
|
+
ticket: ticketId,
|
|
4125
|
+
client: req.user.id,
|
|
4126
|
+
...hasRating ? { rating: Math.round(rating) } : {},
|
|
4127
|
+
...hasNps ? { nps: Math.round(nps) } : {},
|
|
4128
|
+
...comment ? { comment: comment.trim() } : {}
|
|
4129
|
+
},
|
|
4130
|
+
overrideAccess: true
|
|
4131
|
+
});
|
|
4132
|
+
return Response.json({ success: true, survey });
|
|
4133
|
+
} catch (error) {
|
|
4134
|
+
const authResponse = handleAuthError(error);
|
|
4135
|
+
if (authResponse) return authResponse;
|
|
4136
|
+
console.error("[satisfaction] Error:", error);
|
|
4137
|
+
return Response.json({ error: "Erreur interne." }, { status: 500 });
|
|
4138
|
+
}
|
|
3703
4139
|
}
|
|
3704
4140
|
};
|
|
3705
4141
|
}
|
|
@@ -3718,8 +4154,7 @@ function createExportCsvEndpoint(slugs) {
|
|
|
3718
4154
|
let hasMore = true;
|
|
3719
4155
|
const allDocs = [];
|
|
3720
4156
|
while (hasMore) {
|
|
3721
|
-
const batch = await payload.
|
|
3722
|
-
collection: slugs.tickets,
|
|
4157
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3723
4158
|
limit: PAGE_SIZE2,
|
|
3724
4159
|
page,
|
|
3725
4160
|
depth: 1,
|
|
@@ -3795,21 +4230,18 @@ function createExportDataEndpoint(slugs) {
|
|
|
3795
4230
|
const payload = req.payload;
|
|
3796
4231
|
requireClient(req, slugs);
|
|
3797
4232
|
const [clientData, ticketsResult, messagesResult, surveysResult] = await Promise.all([
|
|
3798
|
-
payload.
|
|
3799
|
-
collection: slugs.supportClients,
|
|
4233
|
+
dbFindByID(payload, slugs.supportClients, {
|
|
3800
4234
|
id: req.user.id,
|
|
3801
4235
|
depth: 0,
|
|
3802
4236
|
overrideAccess: true
|
|
3803
4237
|
}),
|
|
3804
|
-
payload.
|
|
3805
|
-
collection: slugs.tickets,
|
|
4238
|
+
dbFind(payload, slugs.tickets, {
|
|
3806
4239
|
where: { client: { equals: req.user.id } },
|
|
3807
4240
|
limit: 1e3,
|
|
3808
4241
|
depth: 0,
|
|
3809
4242
|
overrideAccess: true
|
|
3810
4243
|
}),
|
|
3811
|
-
payload.
|
|
3812
|
-
collection: slugs.ticketMessages,
|
|
4244
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
3813
4245
|
where: {
|
|
3814
4246
|
"ticket.client": { equals: req.user.id },
|
|
3815
4247
|
authorType: { equals: "client" }
|
|
@@ -3818,8 +4250,7 @@ function createExportDataEndpoint(slugs) {
|
|
|
3818
4250
|
depth: 0,
|
|
3819
4251
|
overrideAccess: true
|
|
3820
4252
|
}),
|
|
3821
|
-
payload.
|
|
3822
|
-
collection: slugs.satisfactionSurveys,
|
|
4253
|
+
dbFind(payload, slugs.satisfactionSurveys, {
|
|
3823
4254
|
where: { client: { equals: req.user.id } },
|
|
3824
4255
|
limit: 500,
|
|
3825
4256
|
depth: 0,
|
|
@@ -3893,8 +4324,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3893
4324
|
if (!["create_ticket", "add_to_ticket", "ignore"].includes(action)) {
|
|
3894
4325
|
return Response.json({ error: "Invalid action" }, { status: 400 });
|
|
3895
4326
|
}
|
|
3896
|
-
const pendingEmail = await payload.
|
|
3897
|
-
collection: slugs.pendingEmails,
|
|
4327
|
+
const pendingEmail = await dbFindByID(payload, slugs.pendingEmails, {
|
|
3898
4328
|
id: Number(id),
|
|
3899
4329
|
depth: 1,
|
|
3900
4330
|
overrideAccess: true
|
|
@@ -3908,14 +4338,12 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3908
4338
|
let clientId = overrideClientId || (typeof pendingEmail.client === "object" ? pendingEmail.client?.id : pendingEmail.client);
|
|
3909
4339
|
let clientDoc = typeof pendingEmail.client === "object" ? pendingEmail.client : null;
|
|
3910
4340
|
if (overrideClientId && overrideClientId !== clientDoc?.id) {
|
|
3911
|
-
clientDoc = await payload.
|
|
3912
|
-
collection: slugs.supportClients,
|
|
4341
|
+
clientDoc = await dbFindByID(payload, slugs.supportClients, {
|
|
3913
4342
|
id: overrideClientId,
|
|
3914
4343
|
depth: 0,
|
|
3915
4344
|
overrideAccess: true
|
|
3916
4345
|
});
|
|
3917
|
-
await payload.
|
|
3918
|
-
collection: slugs.pendingEmails,
|
|
4346
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
3919
4347
|
id: Number(id),
|
|
3920
4348
|
data: { client: overrideClientId },
|
|
3921
4349
|
overrideAccess: true
|
|
@@ -3932,8 +4360,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3932
4360
|
file: typeof a.file === "object" ? a.file.id : a.file
|
|
3933
4361
|
})) || [];
|
|
3934
4362
|
if (action === "ignore") {
|
|
3935
|
-
await payload.
|
|
3936
|
-
collection: slugs.pendingEmails,
|
|
4363
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
3937
4364
|
id: Number(id),
|
|
3938
4365
|
data: {
|
|
3939
4366
|
status: "ignored",
|
|
@@ -3945,8 +4372,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3945
4372
|
return Response.json({ action: "ignored", pendingEmailId: Number(id) });
|
|
3946
4373
|
}
|
|
3947
4374
|
if (action === "create_ticket") {
|
|
3948
|
-
const newTicket = await payload.
|
|
3949
|
-
collection: slugs.tickets,
|
|
4375
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
3950
4376
|
data: {
|
|
3951
4377
|
subject: pendingEmail.subject,
|
|
3952
4378
|
client: clientId,
|
|
@@ -3957,8 +4383,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3957
4383
|
},
|
|
3958
4384
|
overrideAccess: true
|
|
3959
4385
|
});
|
|
3960
|
-
await payload.
|
|
3961
|
-
collection: slugs.ticketMessages,
|
|
4386
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
3962
4387
|
data: {
|
|
3963
4388
|
ticket: newTicket.id,
|
|
3964
4389
|
body: pendingEmail.body,
|
|
@@ -3985,8 +4410,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
3985
4410
|
} catch (err) {
|
|
3986
4411
|
console.error("[pending-email-process] Failed to send notification:", err);
|
|
3987
4412
|
}
|
|
3988
|
-
await payload.
|
|
3989
|
-
collection: slugs.pendingEmails,
|
|
4413
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
3990
4414
|
id: Number(id),
|
|
3991
4415
|
data: {
|
|
3992
4416
|
status: "processed",
|
|
@@ -4006,8 +4430,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4006
4430
|
if (!ticketId) {
|
|
4007
4431
|
return Response.json({ error: "ticketId is required for add_to_ticket" }, { status: 400 });
|
|
4008
4432
|
}
|
|
4009
|
-
const targetTicket = await payload.
|
|
4010
|
-
collection: slugs.tickets,
|
|
4433
|
+
const targetTicket = await dbFindByID(payload, slugs.tickets, {
|
|
4011
4434
|
id: ticketId,
|
|
4012
4435
|
depth: 0,
|
|
4013
4436
|
overrideAccess: true
|
|
@@ -4015,8 +4438,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4015
4438
|
if (!targetTicket) {
|
|
4016
4439
|
return Response.json({ error: "Target ticket not found" }, { status: 404 });
|
|
4017
4440
|
}
|
|
4018
|
-
await payload.
|
|
4019
|
-
collection: slugs.ticketMessages,
|
|
4441
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
4020
4442
|
data: {
|
|
4021
4443
|
ticket: ticketId,
|
|
4022
4444
|
body: pendingEmail.body,
|
|
@@ -4028,15 +4450,13 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4028
4450
|
overrideAccess: true
|
|
4029
4451
|
});
|
|
4030
4452
|
if (targetTicket.status === "resolved") {
|
|
4031
|
-
await payload.
|
|
4032
|
-
collection: slugs.tickets,
|
|
4453
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
4033
4454
|
id: ticketId,
|
|
4034
4455
|
data: { status: "open" },
|
|
4035
4456
|
overrideAccess: true
|
|
4036
4457
|
});
|
|
4037
4458
|
}
|
|
4038
|
-
await payload.
|
|
4039
|
-
collection: slugs.pendingEmails,
|
|
4459
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4040
4460
|
id: Number(id),
|
|
4041
4461
|
data: {
|
|
4042
4462
|
status: "processed",
|
|
@@ -4089,8 +4509,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4089
4509
|
if (!messageId) {
|
|
4090
4510
|
return Response.json({ error: "messageId requis" }, { status: 400 });
|
|
4091
4511
|
}
|
|
4092
|
-
const message = await payload.
|
|
4093
|
-
collection: slugs.ticketMessages,
|
|
4512
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
4094
4513
|
id: messageId,
|
|
4095
4514
|
depth: 0,
|
|
4096
4515
|
overrideAccess: true
|
|
@@ -4099,8 +4518,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4099
4518
|
return Response.json({ error: "Message introuvable" }, { status: 404 });
|
|
4100
4519
|
}
|
|
4101
4520
|
const ticketId = typeof message.ticket === "object" ? message.ticket.id : message.ticket;
|
|
4102
|
-
const ticket = await payload.
|
|
4103
|
-
collection: slugs.tickets,
|
|
4521
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
4104
4522
|
id: ticketId,
|
|
4105
4523
|
depth: 1,
|
|
4106
4524
|
overrideAccess: true
|
|
@@ -4226,8 +4644,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4226
4644
|
let created = 0;
|
|
4227
4645
|
let skipped = 0;
|
|
4228
4646
|
for (const article of KB_ARTICLES) {
|
|
4229
|
-
const existing = await payload.
|
|
4230
|
-
collection: slugs.knowledgeBase,
|
|
4647
|
+
const existing = await dbFind(payload, slugs.knowledgeBase, {
|
|
4231
4648
|
where: { slug: { equals: article.slug } },
|
|
4232
4649
|
limit: 1,
|
|
4233
4650
|
depth: 0,
|
|
@@ -4254,8 +4671,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4254
4671
|
version: 1
|
|
4255
4672
|
}
|
|
4256
4673
|
};
|
|
4257
|
-
await payload.
|
|
4258
|
-
collection: slugs.knowledgeBase,
|
|
4674
|
+
await dbCreate(payload, slugs.knowledgeBase, {
|
|
4259
4675
|
data: {
|
|
4260
4676
|
title: article.title,
|
|
4261
4677
|
slug: article.slug,
|
|
@@ -4310,8 +4726,7 @@ function createLoginEndpoint(slugs) {
|
|
|
4310
4726
|
collection: slugs.supportClients,
|
|
4311
4727
|
data: { email, password }
|
|
4312
4728
|
});
|
|
4313
|
-
payload.
|
|
4314
|
-
collection: slugs.authLogs,
|
|
4729
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4315
4730
|
data: { email, success: true, action: "login", ipAddress: ip, userAgent },
|
|
4316
4731
|
overrideAccess: true
|
|
4317
4732
|
}).catch(() => {
|
|
@@ -4335,12 +4750,14 @@ function createLoginEndpoint(slugs) {
|
|
|
4335
4750
|
);
|
|
4336
4751
|
} catch (err) {
|
|
4337
4752
|
const errorMessage = err instanceof Error ? err.message : "Erreur inconnue";
|
|
4753
|
+
if (errorMessage.includes("2FA_REQUIRED")) {
|
|
4754
|
+
return Response.json({ requires2FA: true }, { status: 200 });
|
|
4755
|
+
}
|
|
4338
4756
|
let errorReason = "Identifiants incorrects";
|
|
4339
4757
|
if (errorMessage.includes("locked") || errorMessage.includes("verrouill\xE9") || errorMessage.includes("Too many")) {
|
|
4340
4758
|
errorReason = "Compte verrouill\xE9 (trop de tentatives)";
|
|
4341
4759
|
}
|
|
4342
|
-
payload.
|
|
4343
|
-
collection: slugs.authLogs,
|
|
4760
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4344
4761
|
data: { email, success: false, action: "login", errorReason, ipAddress: ip, userAgent },
|
|
4345
4762
|
overrideAccess: true
|
|
4346
4763
|
}).catch(() => {
|
|
@@ -4361,7 +4778,10 @@ function generateSecureCode() {
|
|
|
4361
4778
|
return String(num);
|
|
4362
4779
|
}
|
|
4363
4780
|
function hashCode(code) {
|
|
4364
|
-
const secret = process.env.PAYLOAD_SECRET
|
|
4781
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
4782
|
+
if (!secret) {
|
|
4783
|
+
throw new Error("[support][2fa] PAYLOAD_SECRET is not set \u2014 refusing to operate 2FA with an insecure fallback secret");
|
|
4784
|
+
}
|
|
4365
4785
|
return createHmac("sha256", secret).update(code).digest("hex");
|
|
4366
4786
|
}
|
|
4367
4787
|
function createAuth2faEndpoint(slugs) {
|
|
@@ -4386,8 +4806,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4386
4806
|
if (sendLimiter.check(email)) {
|
|
4387
4807
|
return Response.json(genericSendResponse);
|
|
4388
4808
|
}
|
|
4389
|
-
const clients = await payload.
|
|
4390
|
-
collection: slugs.supportClients,
|
|
4809
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4391
4810
|
where: { email: { equals: email } },
|
|
4392
4811
|
limit: 1,
|
|
4393
4812
|
depth: 0,
|
|
@@ -4400,8 +4819,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4400
4819
|
const plainCode = generateSecureCode();
|
|
4401
4820
|
const twoFactorCode = hashCode(plainCode);
|
|
4402
4821
|
const twoFactorExpiry = new Date(Date.now() + 10 * 60 * 1e3).toISOString();
|
|
4403
|
-
await payload.
|
|
4404
|
-
collection: slugs.supportClients,
|
|
4822
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4405
4823
|
id: client.id,
|
|
4406
4824
|
data: { twoFactorCode, twoFactorExpiry },
|
|
4407
4825
|
overrideAccess: true
|
|
@@ -4432,8 +4850,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4432
4850
|
{ status: 429 }
|
|
4433
4851
|
);
|
|
4434
4852
|
}
|
|
4435
|
-
const clients = await payload.
|
|
4436
|
-
collection: slugs.supportClients,
|
|
4853
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4437
4854
|
where: { email: { equals: email } },
|
|
4438
4855
|
limit: 1,
|
|
4439
4856
|
depth: 0,
|
|
@@ -4449,8 +4866,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4449
4866
|
return Response.json({ error: "Aucun code en attente" }, { status: 400 });
|
|
4450
4867
|
}
|
|
4451
4868
|
if (/* @__PURE__ */ new Date() > new Date(storedExpiry)) {
|
|
4452
|
-
await payload.
|
|
4453
|
-
collection: slugs.supportClients,
|
|
4869
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4454
4870
|
id: client.id,
|
|
4455
4871
|
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4456
4872
|
overrideAccess: true
|
|
@@ -4464,10 +4880,9 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4464
4880
|
if (submittedBuffer.length !== storedBuffer.length || !crypto3.timingSafeEqual(submittedBuffer, storedBuffer)) {
|
|
4465
4881
|
return Response.json({ error: "Code incorrect" }, { status: 400 });
|
|
4466
4882
|
}
|
|
4467
|
-
await payload.
|
|
4468
|
-
collection: slugs.supportClients,
|
|
4883
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4469
4884
|
id: client.id,
|
|
4470
|
-
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4885
|
+
data: { twoFactorCode: "", twoFactorExpiry: "", twoFactorVerifiedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4471
4886
|
overrideAccess: true
|
|
4472
4887
|
});
|
|
4473
4888
|
verifyLimiter.reset(email);
|
|
@@ -4541,15 +4956,33 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4541
4956
|
if (!profile.email) {
|
|
4542
4957
|
return Response.json({ error: "no_email" }, { status: 400 });
|
|
4543
4958
|
}
|
|
4959
|
+
if (profile.verified_email !== true) {
|
|
4960
|
+
return Response.json({ error: "email_not_verified" }, { status: 403 });
|
|
4961
|
+
}
|
|
4544
4962
|
const payload = req.payload;
|
|
4545
|
-
const
|
|
4546
|
-
|
|
4547
|
-
where: { email: { equals: profile.email } },
|
|
4963
|
+
const byGoogle = await dbFind(payload, slugs.supportClients, {
|
|
4964
|
+
where: { googleId: { equals: profile.id } },
|
|
4548
4965
|
limit: 1,
|
|
4549
4966
|
depth: 0,
|
|
4550
4967
|
overrideAccess: true
|
|
4551
4968
|
});
|
|
4552
|
-
let clientDoc =
|
|
4969
|
+
let clientDoc = byGoogle.docs[0];
|
|
4970
|
+
if (!clientDoc) {
|
|
4971
|
+
const byEmail = await dbFind(payload, slugs.supportClients, {
|
|
4972
|
+
where: { email: { equals: profile.email } },
|
|
4973
|
+
limit: 1,
|
|
4974
|
+
depth: 0,
|
|
4975
|
+
overrideAccess: true
|
|
4976
|
+
});
|
|
4977
|
+
clientDoc = byEmail.docs[0];
|
|
4978
|
+
if (clientDoc && profile.id) {
|
|
4979
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4980
|
+
id: clientDoc.id,
|
|
4981
|
+
data: { googleId: profile.id },
|
|
4982
|
+
overrideAccess: true
|
|
4983
|
+
});
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4553
4986
|
if (!clientDoc) {
|
|
4554
4987
|
const allowedDomains = options?.allowedEmailDomains;
|
|
4555
4988
|
if (allowedDomains && allowedDomains.length > 0) {
|
|
@@ -4567,43 +5000,64 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4567
5000
|
const autoPassword = crypto3.randomBytes(48).toString("base64url");
|
|
4568
5001
|
const fullName = profile.name || profile.email.split("@")[0];
|
|
4569
5002
|
const nameParts = fullName.split(" ");
|
|
4570
|
-
clientDoc = await payload.
|
|
4571
|
-
collection: slugs.supportClients,
|
|
5003
|
+
clientDoc = await dbCreate(payload, slugs.supportClients, {
|
|
4572
5004
|
data: {
|
|
4573
5005
|
email: profile.email,
|
|
4574
5006
|
firstName: nameParts[0] || fullName,
|
|
4575
5007
|
lastName: nameParts.slice(1).join(" ") || "-",
|
|
4576
5008
|
company: fullName,
|
|
5009
|
+
googleId: profile.id,
|
|
4577
5010
|
password: autoPassword
|
|
4578
5011
|
},
|
|
4579
5012
|
overrideAccess: true
|
|
4580
5013
|
});
|
|
4581
5014
|
}
|
|
4582
|
-
const
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
id: clientDoc.id,
|
|
4586
|
-
data: { password: tempPassword },
|
|
4587
|
-
overrideAccess: true
|
|
4588
|
-
});
|
|
4589
|
-
const loginResult = await payload.login({
|
|
4590
|
-
collection: slugs.supportClients,
|
|
4591
|
-
data: { email: profile.email, password: tempPassword }
|
|
4592
|
-
});
|
|
4593
|
-
const postLoginPassword = crypto3.randomBytes(48).toString("base64url");
|
|
4594
|
-
await payload.update({
|
|
4595
|
-
collection: slugs.supportClients,
|
|
4596
|
-
id: clientDoc.id,
|
|
4597
|
-
data: { password: postLoginPassword },
|
|
4598
|
-
overrideAccess: true
|
|
4599
|
-
});
|
|
4600
|
-
if (!loginResult.token) {
|
|
4601
|
-
return Response.json({ error: "login_failed" }, { status: 400 });
|
|
5015
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
5016
|
+
if (!secret) {
|
|
5017
|
+
return Response.json({ error: "server_misconfigured" }, { status: 500 });
|
|
4602
5018
|
}
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
5019
|
+
const collectionConfig = payload.collections[slugs.supportClients].config;
|
|
5020
|
+
const tokenExpiration = collectionConfig.auth?.tokenExpiration ?? 7200;
|
|
5021
|
+
let sid;
|
|
5022
|
+
if (collectionConfig.auth?.useSessions) {
|
|
5023
|
+
sid = crypto3.randomUUID();
|
|
5024
|
+
const now = /* @__PURE__ */ new Date();
|
|
5025
|
+
const expiresAt = new Date(now.getTime() + tokenExpiration * 1e3);
|
|
5026
|
+
const fresh = await dbFindByID(payload, slugs.supportClients, {
|
|
5027
|
+
id: clientDoc.id,
|
|
5028
|
+
depth: 0,
|
|
5029
|
+
overrideAccess: true,
|
|
5030
|
+
showHiddenFields: true
|
|
5031
|
+
});
|
|
5032
|
+
const kept = Array.isArray(fresh?.sessions) ? fresh.sessions.filter((s) => new Date(s.expiresAt) > now) : [];
|
|
5033
|
+
await payload.db.updateOne({
|
|
5034
|
+
collection: slugs.supportClients,
|
|
5035
|
+
id: clientDoc.id,
|
|
5036
|
+
data: {
|
|
5037
|
+
sessions: [
|
|
5038
|
+
...kept,
|
|
5039
|
+
{ id: sid, createdAt: now.toISOString(), expiresAt: expiresAt.toISOString() }
|
|
5040
|
+
]
|
|
5041
|
+
},
|
|
5042
|
+
returning: false
|
|
5043
|
+
});
|
|
5044
|
+
}
|
|
5045
|
+
const fieldsToSign = getFieldsToSign({
|
|
5046
|
+
collectionConfig,
|
|
5047
|
+
email: clientDoc.email,
|
|
5048
|
+
sid,
|
|
5049
|
+
user: { ...clientDoc, collection: slugs.supportClients }
|
|
5050
|
+
});
|
|
5051
|
+
const { token, exp } = await jwtSign({ fieldsToSign, secret, tokenExpiration });
|
|
5052
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
5053
|
+
const cookieSecure = process.env.NODE_ENV === "production";
|
|
5054
|
+
headers.append(
|
|
5055
|
+
"Set-Cookie",
|
|
5056
|
+
`payload-token=${token}; HttpOnly; ${cookieSecure ? "Secure; " : ""}SameSite=Lax; Path=/; Max-Age=${tokenExpiration}`
|
|
5057
|
+
);
|
|
5058
|
+
return new Response(JSON.stringify({ token, user: clientDoc, exp }), {
|
|
5059
|
+
status: 200,
|
|
5060
|
+
headers
|
|
4607
5061
|
});
|
|
4608
5062
|
}
|
|
4609
5063
|
return Response.json({ error: "Action invalide" }, { status: 400 });
|
|
@@ -4644,8 +5098,7 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4644
5098
|
);
|
|
4645
5099
|
}
|
|
4646
5100
|
const clientId = req.user.id;
|
|
4647
|
-
const tickets = await payload.
|
|
4648
|
-
collection: slugs.tickets,
|
|
5101
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4649
5102
|
where: { client: { equals: clientId } },
|
|
4650
5103
|
limit: 1e4,
|
|
4651
5104
|
depth: 0,
|
|
@@ -4654,39 +5107,32 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4654
5107
|
});
|
|
4655
5108
|
const ticketIds = tickets.docs.map((t) => t.id);
|
|
4656
5109
|
if (ticketIds.length > 0) {
|
|
4657
|
-
await payload.
|
|
4658
|
-
collection: slugs.ticketMessages,
|
|
5110
|
+
await dbDelete(payload, slugs.ticketMessages, {
|
|
4659
5111
|
where: { ticket: { in: ticketIds } },
|
|
4660
5112
|
overrideAccess: true
|
|
4661
5113
|
});
|
|
4662
|
-
await payload.
|
|
4663
|
-
collection: slugs.ticketActivityLog,
|
|
5114
|
+
await dbDelete(payload, slugs.ticketActivityLog, {
|
|
4664
5115
|
where: { ticket: { in: ticketIds } },
|
|
4665
5116
|
overrideAccess: true
|
|
4666
5117
|
});
|
|
4667
|
-
await payload.
|
|
4668
|
-
collection: slugs.timeEntries,
|
|
5118
|
+
await dbDelete(payload, slugs.timeEntries, {
|
|
4669
5119
|
where: { ticket: { in: ticketIds } },
|
|
4670
5120
|
overrideAccess: true
|
|
4671
5121
|
});
|
|
4672
|
-
await payload.
|
|
4673
|
-
collection: slugs.tickets,
|
|
5122
|
+
await dbDelete(payload, slugs.tickets, {
|
|
4674
5123
|
where: { client: { equals: clientId } },
|
|
4675
5124
|
overrideAccess: true
|
|
4676
5125
|
});
|
|
4677
5126
|
}
|
|
4678
|
-
await payload.
|
|
4679
|
-
collection: slugs.satisfactionSurveys,
|
|
5127
|
+
await dbDelete(payload, slugs.satisfactionSurveys, {
|
|
4680
5128
|
where: { client: { equals: clientId } },
|
|
4681
5129
|
overrideAccess: true
|
|
4682
5130
|
});
|
|
4683
|
-
await payload.
|
|
4684
|
-
collection: slugs.chatMessages,
|
|
5131
|
+
await dbDelete(payload, slugs.chatMessages, {
|
|
4685
5132
|
where: { client: { equals: clientId } },
|
|
4686
5133
|
overrideAccess: true
|
|
4687
5134
|
});
|
|
4688
|
-
await payload.
|
|
4689
|
-
collection: slugs.supportClients,
|
|
5135
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4690
5136
|
id: clientId,
|
|
4691
5137
|
overrideAccess: true
|
|
4692
5138
|
});
|
|
@@ -4727,8 +5173,8 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4727
5173
|
return Response.json({ error: "sourceId and targetId are required and must be different" }, { status: 400 });
|
|
4728
5174
|
}
|
|
4729
5175
|
const [source, target] = await Promise.all([
|
|
4730
|
-
payload
|
|
4731
|
-
payload
|
|
5176
|
+
dbFindByID(payload, slugs.supportClients, { id: sourceId, depth: 0, overrideAccess: true }),
|
|
5177
|
+
dbFindByID(payload, slugs.supportClients, { id: targetId, depth: 0, overrideAccess: true })
|
|
4732
5178
|
]);
|
|
4733
5179
|
if (!source || !target) {
|
|
4734
5180
|
return Response.json({ error: "Source or target client not found" }, { status: 404 });
|
|
@@ -4740,63 +5186,57 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4740
5186
|
pendingEmails: 0,
|
|
4741
5187
|
satisfactionSurveys: 0
|
|
4742
5188
|
};
|
|
4743
|
-
const tickets = await payload.
|
|
4744
|
-
collection: slugs.tickets,
|
|
5189
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4745
5190
|
where: { client: { equals: sourceId } },
|
|
4746
5191
|
limit: 500,
|
|
4747
5192
|
depth: 0,
|
|
4748
5193
|
overrideAccess: true
|
|
4749
5194
|
});
|
|
4750
5195
|
for (const ticket of tickets.docs) {
|
|
4751
|
-
await payload
|
|
5196
|
+
await dbUpdate(payload, slugs.tickets, { id: ticket.id, data: { client: targetId }, overrideAccess: true });
|
|
4752
5197
|
results.tickets++;
|
|
4753
5198
|
}
|
|
4754
|
-
const messages = await payload.
|
|
4755
|
-
collection: slugs.ticketMessages,
|
|
5199
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
4756
5200
|
where: { authorClient: { equals: sourceId } },
|
|
4757
5201
|
limit: 1e3,
|
|
4758
5202
|
depth: 0,
|
|
4759
5203
|
overrideAccess: true
|
|
4760
5204
|
});
|
|
4761
5205
|
for (const msg of messages.docs) {
|
|
4762
|
-
await payload
|
|
5206
|
+
await dbUpdate(payload, slugs.ticketMessages, { id: msg.id, data: { authorClient: targetId }, overrideAccess: true });
|
|
4763
5207
|
results.ticketMessages++;
|
|
4764
5208
|
}
|
|
4765
|
-
const chats = await payload.
|
|
4766
|
-
collection: slugs.chatMessages,
|
|
5209
|
+
const chats = await dbFind(payload, slugs.chatMessages, {
|
|
4767
5210
|
where: { client: { equals: sourceId } },
|
|
4768
5211
|
limit: 1e3,
|
|
4769
5212
|
depth: 0,
|
|
4770
5213
|
overrideAccess: true
|
|
4771
5214
|
});
|
|
4772
5215
|
for (const chat of chats.docs) {
|
|
4773
|
-
await payload
|
|
5216
|
+
await dbUpdate(payload, slugs.chatMessages, { id: chat.id, data: { client: targetId }, overrideAccess: true });
|
|
4774
5217
|
results.chatMessages++;
|
|
4775
5218
|
}
|
|
4776
|
-
const pendingEmails = await payload.
|
|
4777
|
-
collection: slugs.pendingEmails,
|
|
5219
|
+
const pendingEmails = await dbFind(payload, slugs.pendingEmails, {
|
|
4778
5220
|
where: { client: { equals: sourceId } },
|
|
4779
5221
|
limit: 500,
|
|
4780
5222
|
depth: 0,
|
|
4781
5223
|
overrideAccess: true
|
|
4782
5224
|
});
|
|
4783
5225
|
for (const pe of pendingEmails.docs) {
|
|
4784
|
-
await payload
|
|
5226
|
+
await dbUpdate(payload, slugs.pendingEmails, { id: pe.id, data: { client: targetId }, overrideAccess: true });
|
|
4785
5227
|
results.pendingEmails++;
|
|
4786
5228
|
}
|
|
4787
|
-
const surveys = await payload.
|
|
4788
|
-
collection: slugs.satisfactionSurveys,
|
|
5229
|
+
const surveys = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
4789
5230
|
where: { client: { equals: sourceId } },
|
|
4790
5231
|
limit: 500,
|
|
4791
5232
|
depth: 0,
|
|
4792
5233
|
overrideAccess: true
|
|
4793
5234
|
});
|
|
4794
5235
|
for (const survey of surveys.docs) {
|
|
4795
|
-
await payload
|
|
5236
|
+
await dbUpdate(payload, slugs.satisfactionSurveys, { id: survey.id, data: { client: targetId }, overrideAccess: true });
|
|
4796
5237
|
results.satisfactionSurveys++;
|
|
4797
5238
|
}
|
|
4798
|
-
await payload.
|
|
4799
|
-
collection: slugs.supportClients,
|
|
5239
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4800
5240
|
id: sourceId,
|
|
4801
5241
|
overrideAccess: true
|
|
4802
5242
|
});
|
|
@@ -4990,8 +5430,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
4990
5430
|
if (blockedEmails.includes(conversation.client.email)) {
|
|
4991
5431
|
return Response.json({ error: "Cannot create ticket from system email address" }, { status: 400 });
|
|
4992
5432
|
}
|
|
4993
|
-
const clientResult = await payload.
|
|
4994
|
-
collection: slugs.supportClients,
|
|
5433
|
+
const clientResult = await dbFind(payload, slugs.supportClients, {
|
|
4995
5434
|
where: { email: { equals: conversation.client.email } },
|
|
4996
5435
|
limit: 1,
|
|
4997
5436
|
depth: 0,
|
|
@@ -5002,8 +5441,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5002
5441
|
if (!client) {
|
|
5003
5442
|
const nameParts = conversation.client.name.split(" ");
|
|
5004
5443
|
const randomPassword = crypto3.randomBytes(48).toString("base64url");
|
|
5005
|
-
client = await payload.
|
|
5006
|
-
collection: slugs.supportClients,
|
|
5444
|
+
client = await dbCreate(payload, slugs.supportClients, {
|
|
5007
5445
|
data: {
|
|
5008
5446
|
email: conversation.client.email,
|
|
5009
5447
|
password: randomPassword,
|
|
@@ -5015,15 +5453,13 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5015
5453
|
});
|
|
5016
5454
|
isNewClient = true;
|
|
5017
5455
|
}
|
|
5018
|
-
const adminUsers = await payload.
|
|
5019
|
-
collection: slugs.users,
|
|
5456
|
+
const adminUsers = await dbFind(payload, slugs.users, {
|
|
5020
5457
|
limit: 1,
|
|
5021
5458
|
depth: 0,
|
|
5022
5459
|
overrideAccess: true
|
|
5023
5460
|
});
|
|
5024
5461
|
const adminUserId = adminUsers.docs[0]?.id;
|
|
5025
|
-
const ticket = await payload.
|
|
5026
|
-
collection: slugs.tickets,
|
|
5462
|
+
const ticket = await dbCreate(payload, slugs.tickets, {
|
|
5027
5463
|
data: {
|
|
5028
5464
|
subject: conversation.subject,
|
|
5029
5465
|
client: client.id,
|
|
@@ -5036,8 +5472,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5036
5472
|
let importedCount = 0;
|
|
5037
5473
|
for (const msg of conversation.messages) {
|
|
5038
5474
|
const isAdmin = msg.from === "admin";
|
|
5039
|
-
await payload.
|
|
5040
|
-
collection: slugs.ticketMessages,
|
|
5475
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
5041
5476
|
data: {
|
|
5042
5477
|
ticket: ticket.id,
|
|
5043
5478
|
body: msg.content,
|
|
@@ -5073,8 +5508,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5073
5508
|
// src/utils/fireWebhooks.ts
|
|
5074
5509
|
async function fireWebhooks(payload, slugs, event, data) {
|
|
5075
5510
|
try {
|
|
5076
|
-
const endpoints = await payload.
|
|
5077
|
-
collection: slugs.webhookEndpoints,
|
|
5511
|
+
const endpoints = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5078
5512
|
where: {
|
|
5079
5513
|
active: { equals: true },
|
|
5080
5514
|
events: { contains: event }
|
|
@@ -5098,8 +5532,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5098
5532
|
signal: AbortSignal.timeout(1e4)
|
|
5099
5533
|
});
|
|
5100
5534
|
try {
|
|
5101
|
-
await payload.
|
|
5102
|
-
collection: slugs.webhookEndpoints,
|
|
5535
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5103
5536
|
id: endpoint.id,
|
|
5104
5537
|
data: {
|
|
5105
5538
|
lastTriggeredAt: timestamp,
|
|
@@ -5112,8 +5545,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5112
5545
|
} catch (error) {
|
|
5113
5546
|
console.warn(`[support] Webhook delivery failed: ${endpoint.url}`, error);
|
|
5114
5547
|
try {
|
|
5115
|
-
await payload.
|
|
5116
|
-
collection: slugs.webhookEndpoints,
|
|
5548
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5117
5549
|
id: endpoint.id,
|
|
5118
5550
|
data: {
|
|
5119
5551
|
lastTriggeredAt: timestamp,
|
|
@@ -5148,8 +5580,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5148
5580
|
const settings = await readSupportSettings(payload);
|
|
5149
5581
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
|
|
5150
5582
|
const results = { processed: 0, errors: 0 };
|
|
5151
|
-
const scheduled = await payload.
|
|
5152
|
-
collection: slugs.ticketMessages,
|
|
5583
|
+
const scheduled = await dbFind(payload, slugs.ticketMessages, {
|
|
5153
5584
|
where: {
|
|
5154
5585
|
and: [
|
|
5155
5586
|
{ scheduledAt: { less_than_equal: now.toISOString() } },
|
|
@@ -5169,8 +5600,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5169
5600
|
try {
|
|
5170
5601
|
const msg = message;
|
|
5171
5602
|
const ticketId = typeof msg.ticket === "object" ? msg.ticket.id : msg.ticket;
|
|
5172
|
-
const ticket = await payload.
|
|
5173
|
-
collection: slugs.tickets,
|
|
5603
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5174
5604
|
id: ticketId,
|
|
5175
5605
|
depth: 1,
|
|
5176
5606
|
overrideAccess: true
|
|
@@ -5193,8 +5623,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5193
5623
|
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
5194
5624
|
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>`
|
|
5195
5625
|
});
|
|
5196
|
-
await payload.
|
|
5197
|
-
collection: slugs.ticketMessages,
|
|
5626
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5198
5627
|
id: msg.id,
|
|
5199
5628
|
data: {
|
|
5200
5629
|
scheduledSent: true,
|
|
@@ -5204,16 +5633,14 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5204
5633
|
overrideAccess: true
|
|
5205
5634
|
});
|
|
5206
5635
|
} else {
|
|
5207
|
-
await payload.
|
|
5208
|
-
collection: slugs.ticketMessages,
|
|
5636
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5209
5637
|
id: msg.id,
|
|
5210
5638
|
data: { scheduledSent: true },
|
|
5211
5639
|
overrideAccess: true
|
|
5212
5640
|
});
|
|
5213
5641
|
}
|
|
5214
5642
|
if (msg.authorType === "admin" && !msg.isInternal) {
|
|
5215
|
-
await payload.
|
|
5216
|
-
collection: slugs.tickets,
|
|
5643
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
5217
5644
|
id: ticketId,
|
|
5218
5645
|
data: { status: "waiting_client" },
|
|
5219
5646
|
overrideAccess: true
|
|
@@ -5245,6 +5672,208 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5245
5672
|
};
|
|
5246
5673
|
}
|
|
5247
5674
|
|
|
5675
|
+
// src/endpoints/process-snooze.ts
|
|
5676
|
+
function createProcessSnoozeEndpoint(slugs) {
|
|
5677
|
+
return {
|
|
5678
|
+
path: "/support/process-snooze",
|
|
5679
|
+
method: "post",
|
|
5680
|
+
handler: async (req) => {
|
|
5681
|
+
const secret = req.headers.get("x-cron-secret");
|
|
5682
|
+
const expectedSecret = process.env.CRON_SECRET;
|
|
5683
|
+
if (!expectedSecret || secret !== expectedSecret) {
|
|
5684
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5685
|
+
}
|
|
5686
|
+
try {
|
|
5687
|
+
const payload = req.payload;
|
|
5688
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
5689
|
+
const results = { processed: 0, errors: 0 };
|
|
5690
|
+
const due = await dbFind(payload, slugs.tickets, {
|
|
5691
|
+
where: {
|
|
5692
|
+
and: [
|
|
5693
|
+
{ snoozeUntil: { exists: true } },
|
|
5694
|
+
{ snoozeUntil: { less_than_equal: nowIso } }
|
|
5695
|
+
]
|
|
5696
|
+
},
|
|
5697
|
+
limit: 500,
|
|
5698
|
+
depth: 0,
|
|
5699
|
+
overrideAccess: true,
|
|
5700
|
+
select: { id: true, ticketNumber: true }
|
|
5701
|
+
});
|
|
5702
|
+
for (const ticket of due.docs) {
|
|
5703
|
+
try {
|
|
5704
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
5705
|
+
id: ticket.id,
|
|
5706
|
+
data: { snoozeUntil: null },
|
|
5707
|
+
overrideAccess: true
|
|
5708
|
+
});
|
|
5709
|
+
try {
|
|
5710
|
+
await dbCreate(payload, slugs.ticketActivityLog, {
|
|
5711
|
+
data: {
|
|
5712
|
+
ticket: ticket.id,
|
|
5713
|
+
action: "snooze_expired",
|
|
5714
|
+
detail: "Snooze \xE9chu \u2014 ticket r\xE9activ\xE9",
|
|
5715
|
+
actorType: "system"
|
|
5716
|
+
},
|
|
5717
|
+
overrideAccess: true
|
|
5718
|
+
});
|
|
5719
|
+
} catch {
|
|
5720
|
+
}
|
|
5721
|
+
results.processed++;
|
|
5722
|
+
} catch (err) {
|
|
5723
|
+
console.error("[support] Failed to wake snoozed ticket:", err);
|
|
5724
|
+
results.errors++;
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
return Response.json({ ok: true, ...results });
|
|
5728
|
+
} catch (err) {
|
|
5729
|
+
console.error("[process-snooze] Error:", err);
|
|
5730
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
};
|
|
5734
|
+
}
|
|
5735
|
+
|
|
5736
|
+
// src/endpoints/process-digests.ts
|
|
5737
|
+
function createProcessDigestsEndpoint(slugs) {
|
|
5738
|
+
return {
|
|
5739
|
+
path: "/support/process-digests",
|
|
5740
|
+
method: "post",
|
|
5741
|
+
handler: async (req) => {
|
|
5742
|
+
const secret = req.headers.get("x-cron-secret");
|
|
5743
|
+
if (!process.env.CRON_SECRET || secret !== process.env.CRON_SECRET) {
|
|
5744
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5745
|
+
}
|
|
5746
|
+
try {
|
|
5747
|
+
const payload = req.payload;
|
|
5748
|
+
let body = {};
|
|
5749
|
+
try {
|
|
5750
|
+
body = await req.json();
|
|
5751
|
+
} catch {
|
|
5752
|
+
}
|
|
5753
|
+
const frequency = body.frequency === "weekly" ? "weekly" : "daily";
|
|
5754
|
+
const settings = await readSupportSettings(payload);
|
|
5755
|
+
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
|
|
5756
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
5757
|
+
const itemsByClient = /* @__PURE__ */ new Map();
|
|
5758
|
+
let page = 1;
|
|
5759
|
+
let hasMore = true;
|
|
5760
|
+
while (hasMore && page <= 50) {
|
|
5761
|
+
const batch = await dbFind(payload, slugs.notificationQueue, { limit: 200, page, depth: 0, overrideAccess: true, sort: "createdAt" });
|
|
5762
|
+
for (const it of batch.docs) {
|
|
5763
|
+
const raw = it;
|
|
5764
|
+
const clientId = typeof raw.client === "object" ? raw.client?.id : raw.client;
|
|
5765
|
+
if (clientId === void 0 || clientId === null) continue;
|
|
5766
|
+
const key = String(clientId);
|
|
5767
|
+
if (!itemsByClient.has(key)) itemsByClient.set(key, []);
|
|
5768
|
+
itemsByClient.get(key).push({ id: raw.id, title: raw.title, message: raw.message });
|
|
5769
|
+
}
|
|
5770
|
+
hasMore = batch.hasNextPage ?? false;
|
|
5771
|
+
page++;
|
|
5772
|
+
}
|
|
5773
|
+
const results = { clients: 0, items: 0, errors: 0 };
|
|
5774
|
+
for (const [clientId, items] of itemsByClient) {
|
|
5775
|
+
try {
|
|
5776
|
+
const client = await dbFindByID(payload, slugs.supportClients, { id: clientId, depth: 0, overrideAccess: true });
|
|
5777
|
+
if (!client || client.notificationFrequency !== frequency) continue;
|
|
5778
|
+
if (client.email) {
|
|
5779
|
+
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("");
|
|
5780
|
+
const html = emailWrapper("Recapitulatif de vos tickets", [
|
|
5781
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
|
|
5782
|
+
emailParagraph(`Voici le recapitulatif de l'activite sur vos tickets (${items.length} notification${items.length > 1 ? "s" : ""}) :`),
|
|
5783
|
+
`<table cellpadding="0" cellspacing="0" border="0" width="100%">${rows}</table>`,
|
|
5784
|
+
...baseUrl ? [emailParagraph(`<a href="${baseUrl}/support/dashboard">Acceder a votre espace support</a>`)] : []
|
|
5785
|
+
].join(""), { kind: "system" });
|
|
5786
|
+
await payload.sendEmail({ to: client.email, ...replyTo ? { replyTo } : {}, subject: `Recapitulatif support (${items.length})`, html });
|
|
5787
|
+
}
|
|
5788
|
+
for (const i of items) {
|
|
5789
|
+
try {
|
|
5790
|
+
await dbDelete(payload, slugs.notificationQueue, { id: i.id, overrideAccess: true });
|
|
5791
|
+
} catch {
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
results.clients++;
|
|
5795
|
+
results.items += items.length;
|
|
5796
|
+
} catch (err) {
|
|
5797
|
+
console.error("[process-digests] client error:", err);
|
|
5798
|
+
results.errors++;
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5801
|
+
return Response.json({ ok: true, frequency, ...results });
|
|
5802
|
+
} catch (err) {
|
|
5803
|
+
console.error("[process-digests] Error:", err);
|
|
5804
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
};
|
|
5808
|
+
}
|
|
5809
|
+
var PROVIDERS = ["whatsapp", "messenger"];
|
|
5810
|
+
function createChannelsWebhookEndpoint(slugs) {
|
|
5811
|
+
return {
|
|
5812
|
+
path: "/support/channels/webhook",
|
|
5813
|
+
method: "post",
|
|
5814
|
+
handler: async (req) => {
|
|
5815
|
+
const secret = req.headers.get("x-channel-secret");
|
|
5816
|
+
if (!process.env.CHANNELS_WEBHOOK_SECRET || secret !== process.env.CHANNELS_WEBHOOK_SECRET) {
|
|
5817
|
+
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5818
|
+
}
|
|
5819
|
+
let body;
|
|
5820
|
+
try {
|
|
5821
|
+
body = await req.json();
|
|
5822
|
+
} catch {
|
|
5823
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5824
|
+
}
|
|
5825
|
+
const provider = body.provider;
|
|
5826
|
+
const from = (body.from || "").trim();
|
|
5827
|
+
const text = (body.text || "").trim();
|
|
5828
|
+
if (!PROVIDERS.includes(provider) || !from || !text) {
|
|
5829
|
+
return Response.json({ error: "provider, from et text requis." }, { status: 400 });
|
|
5830
|
+
}
|
|
5831
|
+
try {
|
|
5832
|
+
const payload = req.payload;
|
|
5833
|
+
const email = `${from}@${provider}.channel`;
|
|
5834
|
+
const existing = await dbFind(payload, slugs.supportClients, { where: { email: { equals: email } }, limit: 1, depth: 0, overrideAccess: true });
|
|
5835
|
+
let clientId;
|
|
5836
|
+
if (existing.docs.length > 0) {
|
|
5837
|
+
clientId = existing.docs[0].id;
|
|
5838
|
+
} else {
|
|
5839
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
5840
|
+
data: { email, firstName: body.name || provider, lastName: from.slice(0, 40), company: provider, password: randomBytes(16).toString("hex") },
|
|
5841
|
+
overrideAccess: true
|
|
5842
|
+
});
|
|
5843
|
+
clientId = created.id;
|
|
5844
|
+
}
|
|
5845
|
+
const open = await dbFind(payload, slugs.tickets, {
|
|
5846
|
+
where: { and: [{ client: { equals: clientId } }, { source: { equals: provider } }, { status: { in: ["open", "waiting_client"] } }] },
|
|
5847
|
+
sort: "-createdAt",
|
|
5848
|
+
limit: 1,
|
|
5849
|
+
depth: 0,
|
|
5850
|
+
overrideAccess: true
|
|
5851
|
+
});
|
|
5852
|
+
let ticketId;
|
|
5853
|
+
let createdTicket = false;
|
|
5854
|
+
if (open.docs.length > 0) {
|
|
5855
|
+
ticketId = open.docs[0].id;
|
|
5856
|
+
} else {
|
|
5857
|
+
const t = await dbCreate(payload, slugs.tickets, {
|
|
5858
|
+
data: { subject: text.slice(0, 80), client: clientId, status: "open", priority: "normal", source: provider },
|
|
5859
|
+
overrideAccess: true
|
|
5860
|
+
});
|
|
5861
|
+
ticketId = t.id;
|
|
5862
|
+
createdTicket = true;
|
|
5863
|
+
}
|
|
5864
|
+
const msg = await dbCreate(payload, slugs.ticketMessages, {
|
|
5865
|
+
data: { ticket: ticketId, body: text, authorType: "client", authorClient: clientId },
|
|
5866
|
+
overrideAccess: true
|
|
5867
|
+
});
|
|
5868
|
+
return Response.json({ ok: true, ticketId, createdTicket, messageId: msg.id });
|
|
5869
|
+
} catch (err) {
|
|
5870
|
+
console.error("[channels] Error:", err);
|
|
5871
|
+
return Response.json({ error: "Erreur interne" }, { status: 500 });
|
|
5872
|
+
}
|
|
5873
|
+
}
|
|
5874
|
+
};
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5248
5877
|
// src/endpoints/user-prefs.ts
|
|
5249
5878
|
var PREF_KEY_PREFIX = "support-user-prefs";
|
|
5250
5879
|
var DEFAULT_USER_PREFS2 = {
|
|
@@ -5260,8 +5889,7 @@ function createUserPrefsGetEndpoint(slugs) {
|
|
|
5260
5889
|
const payload = req.payload;
|
|
5261
5890
|
requireAdmin(req, slugs);
|
|
5262
5891
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5263
|
-
const prefs = await payload
|
|
5264
|
-
collection: "payload-preferences",
|
|
5892
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
5265
5893
|
where: { key: { equals: key } },
|
|
5266
5894
|
limit: 1,
|
|
5267
5895
|
depth: 0,
|
|
@@ -5295,8 +5923,7 @@ function createUserPrefsPostEndpoint(slugs) {
|
|
|
5295
5923
|
requireAdmin(req, slugs);
|
|
5296
5924
|
const body = await req.json();
|
|
5297
5925
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5298
|
-
const existing = await payload
|
|
5299
|
-
collection: "payload-preferences",
|
|
5926
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
5300
5927
|
where: { key: { equals: key } },
|
|
5301
5928
|
limit: 1,
|
|
5302
5929
|
depth: 0,
|
|
@@ -5357,8 +5984,7 @@ function createEscalateEndpoint(slugs) {
|
|
|
5357
5984
|
if (Number.isNaN(ticketId)) {
|
|
5358
5985
|
return Response.json({ error: "invalid-id" }, { status: 400 });
|
|
5359
5986
|
}
|
|
5360
|
-
const ticket = await req.payload.
|
|
5361
|
-
collection: slugs.tickets,
|
|
5987
|
+
const ticket = await dbUpdate(req.payload, slugs.tickets, {
|
|
5362
5988
|
id: ticketId,
|
|
5363
5989
|
data: { status: "escalated" },
|
|
5364
5990
|
overrideAccess: true
|
|
@@ -5406,8 +6032,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5406
6032
|
{ status: 400 }
|
|
5407
6033
|
);
|
|
5408
6034
|
}
|
|
5409
|
-
const ticket = await payload.
|
|
5410
|
-
collection: slugs.tickets,
|
|
6035
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5411
6036
|
id: ticketId,
|
|
5412
6037
|
depth: 0,
|
|
5413
6038
|
overrideAccess: true
|
|
@@ -5419,8 +6044,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5419
6044
|
if (ticketClientId !== req.user.id) {
|
|
5420
6045
|
return Response.json({ error: "forbidden" }, { status: 403 });
|
|
5421
6046
|
}
|
|
5422
|
-
const existing = await payload
|
|
5423
|
-
collection: "ticket-feedback",
|
|
6047
|
+
const existing = await dbFind(payload, "ticket-feedback", {
|
|
5424
6048
|
where: { ticket: { equals: ticketId } },
|
|
5425
6049
|
limit: 1,
|
|
5426
6050
|
depth: 0,
|
|
@@ -5432,8 +6056,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5432
6056
|
{ status: 409 }
|
|
5433
6057
|
);
|
|
5434
6058
|
}
|
|
5435
|
-
const feedback = await payload
|
|
5436
|
-
collection: "ticket-feedback",
|
|
6059
|
+
const feedback = await dbCreate(payload, "ticket-feedback", {
|
|
5437
6060
|
data: {
|
|
5438
6061
|
ticket: ticketId,
|
|
5439
6062
|
client: req.user.id,
|
|
@@ -5457,6 +6080,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5457
6080
|
// src/endpoints/transfer-ticket.ts
|
|
5458
6081
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5459
6082
|
var MAX_TRANSFERS_PER_DAY = 5;
|
|
6083
|
+
var transferLimiter = new RateLimiter(24 * 60 * 60 * 1e3, MAX_TRANSFERS_PER_DAY);
|
|
5460
6084
|
function createTransferTicketEndpoint(slugs) {
|
|
5461
6085
|
return {
|
|
5462
6086
|
path: "/support/tickets/:id/transfer",
|
|
@@ -5481,8 +6105,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5481
6105
|
const includeAttachments = body.includeAttachments !== false;
|
|
5482
6106
|
const customMessage = typeof body.message === "string" ? body.message.slice(0, 1e3) : "";
|
|
5483
6107
|
const payload = req.payload;
|
|
5484
|
-
const ticket = await payload.
|
|
5485
|
-
collection: slugs.tickets,
|
|
6108
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5486
6109
|
id: ticketId,
|
|
5487
6110
|
depth: 1,
|
|
5488
6111
|
overrideAccess: true
|
|
@@ -5493,10 +6116,15 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5493
6116
|
if (!isAdmin && !isOwner) {
|
|
5494
6117
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5495
6118
|
}
|
|
6119
|
+
if (transferLimiter.check(`${req.user.id}:${ticketId}`)) {
|
|
6120
|
+
return Response.json(
|
|
6121
|
+
{ error: `Limite atteinte (${MAX_TRANSFERS_PER_DAY} transferts par 24h sur ce ticket)` },
|
|
6122
|
+
{ status: 429 }
|
|
6123
|
+
);
|
|
6124
|
+
}
|
|
5496
6125
|
try {
|
|
5497
6126
|
const since = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
5498
|
-
const existing = await payload.
|
|
5499
|
-
collection: slugs.emailLogs,
|
|
6127
|
+
const existing = await dbCount(payload, slugs.emailLogs, {
|
|
5500
6128
|
where: {
|
|
5501
6129
|
and: [
|
|
5502
6130
|
{ action: { equals: "transfer" } },
|
|
@@ -5514,8 +6142,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5514
6142
|
}
|
|
5515
6143
|
} catch {
|
|
5516
6144
|
}
|
|
5517
|
-
const messages = await payload.
|
|
5518
|
-
collection: slugs.ticketMessages,
|
|
6145
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
5519
6146
|
where: {
|
|
5520
6147
|
and: [
|
|
5521
6148
|
{ ticket: { equals: ticketId } },
|
|
@@ -5595,8 +6222,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5595
6222
|
html
|
|
5596
6223
|
});
|
|
5597
6224
|
try {
|
|
5598
|
-
await payload.
|
|
5599
|
-
collection: slugs.emailLogs,
|
|
6225
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5600
6226
|
data: {
|
|
5601
6227
|
status: "success",
|
|
5602
6228
|
action: "transfer",
|
|
@@ -5636,6 +6262,8 @@ function statusToLabel(status) {
|
|
|
5636
6262
|
}
|
|
5637
6263
|
}
|
|
5638
6264
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6265
|
+
var MAX_COLLABORATORS_PER_TICKET = 20;
|
|
6266
|
+
var inviteLimiter = new RateLimiter(60 * 60 * 1e3, 10);
|
|
5639
6267
|
function createInviteCollaboratorEndpoint(slugs) {
|
|
5640
6268
|
return {
|
|
5641
6269
|
path: "/support/tickets/:id/invite",
|
|
@@ -5659,8 +6287,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5659
6287
|
}
|
|
5660
6288
|
const role = body.role === "collaborator" ? "collaborator" : "viewer";
|
|
5661
6289
|
const payload = req.payload;
|
|
5662
|
-
const ticket = await payload.
|
|
5663
|
-
collection: slugs.tickets,
|
|
6290
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5664
6291
|
id: ticketId,
|
|
5665
6292
|
depth: 1,
|
|
5666
6293
|
overrideAccess: true
|
|
@@ -5672,13 +6299,22 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5672
6299
|
if (!isAdmin && !isOwner) {
|
|
5673
6300
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5674
6301
|
}
|
|
6302
|
+
if (inviteLimiter.check(String(req.user.id))) {
|
|
6303
|
+
return Response.json({ error: "Trop d'invitations. R\xE9essayez plus tard." }, { status: 429 });
|
|
6304
|
+
}
|
|
6305
|
+
const collabCount = await dbCount(payload, "ticket-collaborators", { where: { ticket: { equals: ticketId } }, overrideAccess: true }).catch(() => ({ totalDocs: 0 }));
|
|
6306
|
+
if (collabCount.totalDocs >= MAX_COLLABORATORS_PER_TICKET) {
|
|
6307
|
+
return Response.json(
|
|
6308
|
+
{ error: `Limite de ${MAX_COLLABORATORS_PER_TICKET} collaborateurs atteinte sur ce ticket` },
|
|
6309
|
+
{ status: 429 }
|
|
6310
|
+
);
|
|
6311
|
+
}
|
|
5675
6312
|
const ownerEmail = typeof ticket.client === "object" ? ticket.client?.email : void 0;
|
|
5676
6313
|
if (ownerEmail && ownerEmail.toLowerCase() === email) {
|
|
5677
6314
|
return Response.json({ error: "Ce client est d\xE9j\xE0 propri\xE9taire du ticket" }, { status: 400 });
|
|
5678
6315
|
}
|
|
5679
6316
|
let inviteeId = null;
|
|
5680
|
-
const existing = await payload.
|
|
5681
|
-
collection: slugs.supportClients,
|
|
6317
|
+
const existing = await dbFind(payload, slugs.supportClients, {
|
|
5682
6318
|
where: { email: { equals: email } },
|
|
5683
6319
|
limit: 1,
|
|
5684
6320
|
depth: 0,
|
|
@@ -5688,8 +6324,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5688
6324
|
inviteeId = existing.docs[0].id;
|
|
5689
6325
|
} else {
|
|
5690
6326
|
const tempPassword = randomBytes(16).toString("hex");
|
|
5691
|
-
const created = await payload.
|
|
5692
|
-
collection: slugs.supportClients,
|
|
6327
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
5693
6328
|
data: {
|
|
5694
6329
|
email,
|
|
5695
6330
|
firstName: "Invit\xE9",
|
|
@@ -5706,8 +6341,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5706
6341
|
if (!inviteeId) {
|
|
5707
6342
|
return Response.json({ error: "Impossible de cr\xE9er le client invit\xE9" }, { status: 500 });
|
|
5708
6343
|
}
|
|
5709
|
-
const dupe = await payload
|
|
5710
|
-
collection: "ticket-collaborators",
|
|
6344
|
+
const dupe = await dbFind(payload, "ticket-collaborators", {
|
|
5711
6345
|
where: {
|
|
5712
6346
|
and: [
|
|
5713
6347
|
{ ticket: { equals: ticketId } },
|
|
@@ -5720,15 +6354,13 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5720
6354
|
});
|
|
5721
6355
|
const invitationToken = randomBytes(24).toString("hex");
|
|
5722
6356
|
if (dupe.docs.length > 0) {
|
|
5723
|
-
await payload
|
|
5724
|
-
collection: "ticket-collaborators",
|
|
6357
|
+
await dbUpdate(payload, "ticket-collaborators", {
|
|
5725
6358
|
id: dupe.docs[0].id,
|
|
5726
6359
|
data: { role, invitationToken },
|
|
5727
6360
|
overrideAccess: true
|
|
5728
6361
|
});
|
|
5729
6362
|
} else {
|
|
5730
|
-
await payload
|
|
5731
|
-
collection: "ticket-collaborators",
|
|
6363
|
+
await dbCreate(payload, "ticket-collaborators", {
|
|
5732
6364
|
data: {
|
|
5733
6365
|
ticket: ticketId,
|
|
5734
6366
|
client: inviteeId,
|
|
@@ -5771,8 +6403,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5771
6403
|
)
|
|
5772
6404
|
});
|
|
5773
6405
|
try {
|
|
5774
|
-
await payload.
|
|
5775
|
-
collection: slugs.emailLogs,
|
|
6406
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5776
6407
|
data: {
|
|
5777
6408
|
status: "success",
|
|
5778
6409
|
action: "invite",
|
|
@@ -5825,6 +6456,7 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5825
6456
|
];
|
|
5826
6457
|
if (!f || f.ai !== false) {
|
|
5827
6458
|
endpoints.push(createAiEndpoint(slugs));
|
|
6459
|
+
endpoints.push(createAiAgentEndpoint(slugs));
|
|
5828
6460
|
endpoints.push(...createClientIntelligenceEndpoint(slugs));
|
|
5829
6461
|
endpoints.push(createTicketSynthesisEndpoint(slugs));
|
|
5830
6462
|
}
|
|
@@ -5839,7 +6471,10 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5839
6471
|
endpoints.push(createSignatureGetEndpoint(slugs), createSignaturePostEndpoint(slugs));
|
|
5840
6472
|
}
|
|
5841
6473
|
if (!f || f.sla !== false) endpoints.push(createSlaCheckEndpoint(slugs));
|
|
5842
|
-
if (!f || f.autoClose !== false)
|
|
6474
|
+
if (!f || f.autoClose !== false) {
|
|
6475
|
+
endpoints.push(createAutoCloseEndpoint(slugs));
|
|
6476
|
+
endpoints.push(createSendReminderEndpoint(slugs));
|
|
6477
|
+
}
|
|
5843
6478
|
if (!f || f.customStatuses !== false) endpoints.push(createStatusesEndpoint(slugs));
|
|
5844
6479
|
if (!f || f.macros !== false) endpoints.push(createApplyMacroEndpoint(slugs));
|
|
5845
6480
|
if (!f || f.roundRobin !== false) {
|
|
@@ -5852,21 +6487,26 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5852
6487
|
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs));
|
|
5853
6488
|
endpoints.push(createAdminChatStreamEndpoint(slugs));
|
|
5854
6489
|
}
|
|
5855
|
-
if (!f || f.timeTracking !== false)
|
|
6490
|
+
if (!f || f.timeTracking !== false) {
|
|
6491
|
+
endpoints.push(createBillingEndpoint(slugs));
|
|
6492
|
+
endpoints.push(createInvoiceEndpoint(slugs));
|
|
6493
|
+
}
|
|
5856
6494
|
if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs));
|
|
5857
6495
|
if (!f || f.emailTracking !== false) {
|
|
5858
6496
|
endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs));
|
|
5859
6497
|
}
|
|
5860
6498
|
if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs));
|
|
5861
6499
|
if (!f || f.scheduledReplies !== false) endpoints.push(createProcessScheduledEndpoint(slugs));
|
|
6500
|
+
if (!f || f.snooze !== false) endpoints.push(createProcessSnoozeEndpoint(slugs));
|
|
6501
|
+
endpoints.push(createProcessDigestsEndpoint(slugs));
|
|
6502
|
+
endpoints.push(createChannelsWebhookEndpoint(slugs));
|
|
5862
6503
|
return endpoints;
|
|
5863
6504
|
}
|
|
5864
6505
|
|
|
5865
6506
|
// src/utils/adminNotification.ts
|
|
5866
6507
|
async function createAdminNotification(payload, data, collectionSlug = "admin-notifications") {
|
|
5867
6508
|
try {
|
|
5868
|
-
await payload
|
|
5869
|
-
collection: collectionSlug,
|
|
6509
|
+
await dbCreate(payload, collectionSlug, {
|
|
5870
6510
|
data: {
|
|
5871
6511
|
title: data.title,
|
|
5872
6512
|
message: data.message,
|
|
@@ -5886,8 +6526,7 @@ function dispatchWebhook(data, event, payload, slugs) {
|
|
|
5886
6526
|
}
|
|
5887
6527
|
async function _dispatch(data, event, payload, slugs) {
|
|
5888
6528
|
try {
|
|
5889
|
-
const { docs: endpoints } = await payload.
|
|
5890
|
-
collection: slugs.webhookEndpoints,
|
|
6529
|
+
const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5891
6530
|
where: {
|
|
5892
6531
|
and: [
|
|
5893
6532
|
{ active: { equals: true } },
|
|
@@ -5924,8 +6563,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
5924
6563
|
signal: AbortSignal.timeout(1e4)
|
|
5925
6564
|
// 10s timeout
|
|
5926
6565
|
});
|
|
5927
|
-
await payload.
|
|
5928
|
-
collection: slugs.webhookEndpoints,
|
|
6566
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5929
6567
|
id: endpoint.id,
|
|
5930
6568
|
data: {
|
|
5931
6569
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5939,8 +6577,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
5939
6577
|
} catch (err) {
|
|
5940
6578
|
console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
|
|
5941
6579
|
try {
|
|
5942
|
-
await payload.
|
|
5943
|
-
collection: slugs.webhookEndpoints,
|
|
6580
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5944
6581
|
id: endpoint.id,
|
|
5945
6582
|
data: {
|
|
5946
6583
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6174,8 +6811,7 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6174
6811
|
if (typeof policyRef === "object" && policyRef.firstResponseTime) {
|
|
6175
6812
|
return policyRef;
|
|
6176
6813
|
}
|
|
6177
|
-
return await payload.
|
|
6178
|
-
collection: slugs.slaPolicies,
|
|
6814
|
+
return await dbFindByID(payload, slugs.slaPolicies, {
|
|
6179
6815
|
id: policyId,
|
|
6180
6816
|
depth: 0,
|
|
6181
6817
|
overrideAccess: true
|
|
@@ -6186,8 +6822,7 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6186
6822
|
}
|
|
6187
6823
|
const priority = field(ticket, "priority") || "normal";
|
|
6188
6824
|
try {
|
|
6189
|
-
const defaults = await payload.
|
|
6190
|
-
collection: slugs.slaPolicies,
|
|
6825
|
+
const defaults = await dbFind(payload, slugs.slaPolicies, {
|
|
6191
6826
|
where: {
|
|
6192
6827
|
and: [
|
|
6193
6828
|
{ isDefault: { equals: true } },
|
|
@@ -6224,8 +6859,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6224
6859
|
if (typeof policyRef === "object" && policyRef.escalateOnBreach !== void 0) {
|
|
6225
6860
|
policy = policyRef;
|
|
6226
6861
|
} else {
|
|
6227
|
-
policy = await payload.
|
|
6228
|
-
collection: slugs.slaPolicies,
|
|
6862
|
+
policy = await dbFindByID(payload, slugs.slaPolicies, {
|
|
6229
6863
|
id: policyId,
|
|
6230
6864
|
depth: 0,
|
|
6231
6865
|
overrideAccess: true
|
|
@@ -6238,8 +6872,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6238
6872
|
try {
|
|
6239
6873
|
const escalateToId = typeof policy.escalateTo === "object" ? policy.escalateTo.id : policy.escalateTo;
|
|
6240
6874
|
if (!escalateToId) return;
|
|
6241
|
-
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await payload.
|
|
6242
|
-
collection: slugs.users,
|
|
6875
|
+
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await dbFindByID(payload, slugs.users, {
|
|
6243
6876
|
id: escalateToId,
|
|
6244
6877
|
depth: 0,
|
|
6245
6878
|
overrideAccess: true
|
|
@@ -6287,8 +6920,7 @@ function createAssignSlaDeadlines(slugs, notificationSlug = "admin-notifications
|
|
|
6287
6920
|
if (!field(doc, "slaPolicy") && policy.id) {
|
|
6288
6921
|
updateData.slaPolicy = policy.id;
|
|
6289
6922
|
}
|
|
6290
|
-
await payload.
|
|
6291
|
-
collection: slugs.tickets,
|
|
6923
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6292
6924
|
id: doc.id,
|
|
6293
6925
|
data: updateData,
|
|
6294
6926
|
overrideAccess: true
|
|
@@ -6311,8 +6943,7 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6311
6943
|
const now = /* @__PURE__ */ new Date();
|
|
6312
6944
|
const deadline = new Date(doc.slaResolutionDue);
|
|
6313
6945
|
const breached = now > deadline;
|
|
6314
|
-
await payload.
|
|
6315
|
-
collection: slugs.tickets,
|
|
6946
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6316
6947
|
id: doc.id,
|
|
6317
6948
|
data: { slaResolutionBreached: breached },
|
|
6318
6949
|
overrideAccess: true
|
|
@@ -6326,6 +6957,35 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6326
6957
|
return doc;
|
|
6327
6958
|
};
|
|
6328
6959
|
}
|
|
6960
|
+
function createPauseSlaOnHold(slugs) {
|
|
6961
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
6962
|
+
if (operation !== "update" || !previousDoc) return doc;
|
|
6963
|
+
if (previousDoc.status === doc.status) return doc;
|
|
6964
|
+
if (!doc.slaResolutionDue) return doc;
|
|
6965
|
+
const wasWaiting = previousDoc.status === "waiting_client";
|
|
6966
|
+
const isWaiting = doc.status === "waiting_client";
|
|
6967
|
+
try {
|
|
6968
|
+
if (!wasWaiting && isWaiting && !doc.slaPausedAt && doc.status !== "resolved") {
|
|
6969
|
+
await req.payload.update({
|
|
6970
|
+
collection: slugs.tickets,
|
|
6971
|
+
id: doc.id,
|
|
6972
|
+
data: { slaPausedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
6973
|
+
overrideAccess: true
|
|
6974
|
+
});
|
|
6975
|
+
} else if (wasWaiting && !isWaiting && doc.slaPausedAt) {
|
|
6976
|
+
const pausedMs = Date.now() - new Date(doc.slaPausedAt).getTime();
|
|
6977
|
+
const data = { slaPausedAt: null };
|
|
6978
|
+
if (pausedMs > 0) {
|
|
6979
|
+
data.slaResolutionDue = new Date(new Date(doc.slaResolutionDue).getTime() + pausedMs).toISOString();
|
|
6980
|
+
}
|
|
6981
|
+
await req.payload.update({ collection: slugs.tickets, id: doc.id, data, overrideAccess: true });
|
|
6982
|
+
}
|
|
6983
|
+
} catch (err) {
|
|
6984
|
+
console.error("[sla] Failed to pause/resume SLA on hold:", err);
|
|
6985
|
+
}
|
|
6986
|
+
return doc;
|
|
6987
|
+
};
|
|
6988
|
+
}
|
|
6329
6989
|
function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications") {
|
|
6330
6990
|
return async ({ doc, operation, req }) => {
|
|
6331
6991
|
if (operation !== "create") return doc;
|
|
@@ -6333,8 +6993,7 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6333
6993
|
try {
|
|
6334
6994
|
const { payload } = req;
|
|
6335
6995
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
6336
|
-
const ticket = await payload.
|
|
6337
|
-
collection: slugs.tickets,
|
|
6996
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
6338
6997
|
id: ticketId,
|
|
6339
6998
|
depth: 1,
|
|
6340
6999
|
overrideAccess: true
|
|
@@ -6345,8 +7004,7 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6345
7004
|
const now = /* @__PURE__ */ new Date();
|
|
6346
7005
|
const deadline = new Date(ticket.slaFirstResponseDue);
|
|
6347
7006
|
const breached = now > deadline;
|
|
6348
|
-
await payload.
|
|
6349
|
-
collection: slugs.tickets,
|
|
7007
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6350
7008
|
id: ticketId,
|
|
6351
7009
|
data: { slaFirstResponseBreached: breached },
|
|
6352
7010
|
overrideAccess: true
|
|
@@ -6361,6 +7019,108 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6361
7019
|
};
|
|
6362
7020
|
}
|
|
6363
7021
|
|
|
7022
|
+
// src/hooks/automationRules.ts
|
|
7023
|
+
function evaluateConditions(doc, rule) {
|
|
7024
|
+
const conds = Array.isArray(rule.conditions) ? rule.conditions : [];
|
|
7025
|
+
if (conds.length === 0) return true;
|
|
7026
|
+
const results = conds.map((c) => {
|
|
7027
|
+
const fieldVal = String(doc[c.field] ?? "");
|
|
7028
|
+
const v = String(c.value ?? "");
|
|
7029
|
+
switch (c.operator) {
|
|
7030
|
+
case "equals":
|
|
7031
|
+
return fieldVal === v;
|
|
7032
|
+
case "not_equals":
|
|
7033
|
+
return fieldVal !== v;
|
|
7034
|
+
case "contains":
|
|
7035
|
+
return fieldVal.toLowerCase().includes(v.toLowerCase());
|
|
7036
|
+
default:
|
|
7037
|
+
return false;
|
|
7038
|
+
}
|
|
7039
|
+
});
|
|
7040
|
+
return rule.matchType === "any" ? results.some(Boolean) : results.every(Boolean);
|
|
7041
|
+
}
|
|
7042
|
+
function createApplyAutomationRules(slugs) {
|
|
7043
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
7044
|
+
if (req.context?.skipAutomation) return doc;
|
|
7045
|
+
const events = [];
|
|
7046
|
+
if (operation === "create") events.push("ticket_created");
|
|
7047
|
+
if (operation === "update") {
|
|
7048
|
+
events.push("ticket_updated");
|
|
7049
|
+
if (previousDoc && previousDoc.status !== doc.status) events.push("ticket_status_changed");
|
|
7050
|
+
}
|
|
7051
|
+
if (events.length === 0) return doc;
|
|
7052
|
+
try {
|
|
7053
|
+
const rules = await dbFind(req.payload, slugs.automationRules, {
|
|
7054
|
+
where: { and: [{ enabled: { equals: true } }, { event: { in: events } }] },
|
|
7055
|
+
sort: "order",
|
|
7056
|
+
limit: 200,
|
|
7057
|
+
depth: 0,
|
|
7058
|
+
overrideAccess: true
|
|
7059
|
+
});
|
|
7060
|
+
const changes = {};
|
|
7061
|
+
const tags = Array.isArray(doc.tags) ? [...doc.tags] : [];
|
|
7062
|
+
let tagsTouched = false;
|
|
7063
|
+
for (const rule of rules.docs) {
|
|
7064
|
+
if (!evaluateConditions(doc, rule)) continue;
|
|
7065
|
+
for (const a of Array.isArray(rule.actions) ? rule.actions : []) {
|
|
7066
|
+
if (!a.value) continue;
|
|
7067
|
+
switch (a.type) {
|
|
7068
|
+
case "set_status":
|
|
7069
|
+
changes.status = a.value;
|
|
7070
|
+
break;
|
|
7071
|
+
case "set_priority":
|
|
7072
|
+
changes.priority = a.value;
|
|
7073
|
+
break;
|
|
7074
|
+
case "set_category":
|
|
7075
|
+
changes.category = a.value;
|
|
7076
|
+
break;
|
|
7077
|
+
case "assign":
|
|
7078
|
+
changes.assignedTo = a.value;
|
|
7079
|
+
break;
|
|
7080
|
+
case "add_tag":
|
|
7081
|
+
if (!tags.includes(a.value)) {
|
|
7082
|
+
tags.push(a.value);
|
|
7083
|
+
tagsTouched = true;
|
|
7084
|
+
}
|
|
7085
|
+
break;
|
|
7086
|
+
}
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
if (tagsTouched) changes.tags = tags;
|
|
7090
|
+
if (Object.keys(changes).length > 0) {
|
|
7091
|
+
await req.payload.update({
|
|
7092
|
+
collection: slugs.tickets,
|
|
7093
|
+
id: doc.id,
|
|
7094
|
+
data: changes,
|
|
7095
|
+
overrideAccess: true,
|
|
7096
|
+
context: { skipAutomation: true }
|
|
7097
|
+
});
|
|
7098
|
+
}
|
|
7099
|
+
} catch (err) {
|
|
7100
|
+
console.error("[support] Failed to apply automation rules:", err);
|
|
7101
|
+
}
|
|
7102
|
+
return doc;
|
|
7103
|
+
};
|
|
7104
|
+
}
|
|
7105
|
+
|
|
7106
|
+
// src/utils/teamAccess.ts
|
|
7107
|
+
function isTeamScopingEnabled() {
|
|
7108
|
+
return process.env.SUPPORT_TEAM_SCOPING === "1" || process.env.SUPPORT_TEAM_SCOPING === "true";
|
|
7109
|
+
}
|
|
7110
|
+
async function resolveAgentTeamIds(payload, slugs, userId) {
|
|
7111
|
+
try {
|
|
7112
|
+
const teams = await dbFind(payload, slugs.supportTeams, {
|
|
7113
|
+
where: { members: { in: [userId] } },
|
|
7114
|
+
limit: 200,
|
|
7115
|
+
depth: 0,
|
|
7116
|
+
overrideAccess: true
|
|
7117
|
+
});
|
|
7118
|
+
return teams.docs.map((t) => t.id);
|
|
7119
|
+
} catch {
|
|
7120
|
+
return [];
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
|
|
6364
7124
|
// src/collections/Tickets.ts
|
|
6365
7125
|
function createAssignTicketNumber(slugs) {
|
|
6366
7126
|
return async ({ data, operation, req }) => {
|
|
@@ -6623,56 +7383,6 @@ function createNotifyClientOnResolve(slugs) {
|
|
|
6623
7383
|
return doc;
|
|
6624
7384
|
};
|
|
6625
7385
|
}
|
|
6626
|
-
function createAutoCalculateSLA(slugs) {
|
|
6627
|
-
return async ({ doc, operation, req }) => {
|
|
6628
|
-
if (operation !== "create") return doc;
|
|
6629
|
-
try {
|
|
6630
|
-
const { payload } = req;
|
|
6631
|
-
const ticketPriority = doc.priority || "normal";
|
|
6632
|
-
let policy = null;
|
|
6633
|
-
const byPriority = await payload.find({
|
|
6634
|
-
collection: slugs.slaPolicies,
|
|
6635
|
-
where: { priority: { equals: ticketPriority } },
|
|
6636
|
-
limit: 1,
|
|
6637
|
-
depth: 0,
|
|
6638
|
-
overrideAccess: true
|
|
6639
|
-
});
|
|
6640
|
-
if (byPriority.docs.length > 0) {
|
|
6641
|
-
policy = byPriority.docs[0];
|
|
6642
|
-
} else {
|
|
6643
|
-
const defaults = await payload.find({
|
|
6644
|
-
collection: slugs.slaPolicies,
|
|
6645
|
-
where: { isDefault: { equals: true } },
|
|
6646
|
-
limit: 1,
|
|
6647
|
-
depth: 0,
|
|
6648
|
-
overrideAccess: true
|
|
6649
|
-
});
|
|
6650
|
-
if (defaults.docs.length > 0) {
|
|
6651
|
-
policy = defaults.docs[0];
|
|
6652
|
-
}
|
|
6653
|
-
}
|
|
6654
|
-
if (!policy) return doc;
|
|
6655
|
-
const now = /* @__PURE__ */ new Date();
|
|
6656
|
-
const firstResponseMinutes = policy.firstResponseTime || 240;
|
|
6657
|
-
const resolutionMinutes = policy.resolutionTime || 1440;
|
|
6658
|
-
const firstResponseDue = new Date(now.getTime() + firstResponseMinutes * 6e4);
|
|
6659
|
-
const resolutionDue = new Date(now.getTime() + resolutionMinutes * 6e4);
|
|
6660
|
-
await payload.update({
|
|
6661
|
-
collection: slugs.tickets,
|
|
6662
|
-
id: doc.id,
|
|
6663
|
-
data: {
|
|
6664
|
-
slaPolicy: policy.id,
|
|
6665
|
-
slaFirstResponseDue: firstResponseDue.toISOString(),
|
|
6666
|
-
slaResolutionDue: resolutionDue.toISOString()
|
|
6667
|
-
},
|
|
6668
|
-
overrideAccess: true
|
|
6669
|
-
});
|
|
6670
|
-
} catch (err) {
|
|
6671
|
-
console.error("[support] Failed to auto-calculate SLA:", err);
|
|
6672
|
-
}
|
|
6673
|
-
return doc;
|
|
6674
|
-
};
|
|
6675
|
-
}
|
|
6676
7386
|
function createFireTicketWebhooks(slugs) {
|
|
6677
7387
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
6678
7388
|
const { payload } = req;
|
|
@@ -6764,8 +7474,7 @@ function createCascadeDelete(slugs) {
|
|
|
6764
7474
|
return async ({ id, req }) => {
|
|
6765
7475
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys];
|
|
6766
7476
|
for (const slug of collections) {
|
|
6767
|
-
await req.payload
|
|
6768
|
-
collection: slug,
|
|
7477
|
+
await dbDelete(req.payload, slug, {
|
|
6769
7478
|
where: { ticket: { equals: id } },
|
|
6770
7479
|
overrideAccess: true
|
|
6771
7480
|
});
|
|
@@ -6890,6 +7599,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
6890
7599
|
{
|
|
6891
7600
|
name: "status",
|
|
6892
7601
|
type: "select",
|
|
7602
|
+
index: true,
|
|
6893
7603
|
defaultValue: "open",
|
|
6894
7604
|
label: "Statut",
|
|
6895
7605
|
options: [
|
|
@@ -6904,6 +7614,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
6904
7614
|
{
|
|
6905
7615
|
name: "priority",
|
|
6906
7616
|
type: "select",
|
|
7617
|
+
index: true,
|
|
6907
7618
|
defaultValue: "normal",
|
|
6908
7619
|
label: "Priorite",
|
|
6909
7620
|
options: [
|
|
@@ -6919,7 +7630,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
6919
7630
|
{
|
|
6920
7631
|
type: "row",
|
|
6921
7632
|
fields: [
|
|
6922
|
-
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, label: "Client", admin: { width: "50%" } }
|
|
7633
|
+
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, index: true, label: "Client", admin: { width: "50%" } }
|
|
6923
7634
|
]
|
|
6924
7635
|
},
|
|
6925
7636
|
// Billing collapsible
|
|
@@ -6988,15 +7699,16 @@ function createTicketsCollection(slugs, options) {
|
|
|
6988
7699
|
{
|
|
6989
7700
|
type: "row",
|
|
6990
7701
|
fields: [
|
|
6991
|
-
{ name: "slaFirstResponseDue", type: "date", label: "Echeance 1ere reponse (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
6992
|
-
{ name: "slaResolutionDue", type: "date", label: "Echeance resolution (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
7702
|
+
{ name: "slaFirstResponseDue", type: "date", index: true, label: "Echeance 1ere reponse (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7703
|
+
{ name: "slaResolutionDue", type: "date", index: true, label: "Echeance resolution (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
6993
7704
|
]
|
|
6994
7705
|
},
|
|
6995
7706
|
{
|
|
6996
7707
|
type: "row",
|
|
6997
7708
|
fields: [
|
|
6998
7709
|
{ name: "slaFirstResponseBreached", type: "checkbox", label: "1ere reponse depassee", admin: { readOnly: true, width: "50%" } },
|
|
6999
|
-
{ name: "slaResolutionBreached", type: "checkbox", label: "Resolution depassee", admin: { readOnly: true, width: "50%" } }
|
|
7710
|
+
{ name: "slaResolutionBreached", type: "checkbox", label: "Resolution depassee", admin: { readOnly: true, width: "50%" } },
|
|
7711
|
+
{ name: "slaPausedAt", type: "date", label: "SLA en pause depuis", admin: { hidden: true } }
|
|
7000
7712
|
]
|
|
7001
7713
|
}
|
|
7002
7714
|
]
|
|
@@ -7017,7 +7729,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7017
7729
|
]
|
|
7018
7730
|
},
|
|
7019
7731
|
{ name: "mergedInto", type: "relationship", relationTo: slugs.tickets, label: "Fusionne dans", admin: { readOnly: true } },
|
|
7020
|
-
{ name: "autoCloseRemindedAt", type: "date", label: "Rappel auto-close envoye", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
7732
|
+
{ name: "autoCloseRemindedAt", type: "date", label: "Rappel auto-close envoye", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7733
|
+
{ 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." } }
|
|
7021
7734
|
]
|
|
7022
7735
|
},
|
|
7023
7736
|
// Sidebar
|
|
@@ -7026,6 +7739,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7026
7739
|
{
|
|
7027
7740
|
name: "category",
|
|
7028
7741
|
type: "select",
|
|
7742
|
+
index: true,
|
|
7029
7743
|
label: "Categorie",
|
|
7030
7744
|
options: [
|
|
7031
7745
|
{ label: "Bug / Dysfonctionnement", value: "bug" },
|
|
@@ -7036,7 +7750,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7036
7750
|
],
|
|
7037
7751
|
admin: { position: "sidebar" }
|
|
7038
7752
|
},
|
|
7039
|
-
{ name: "assignedTo", type: "relationship", relationTo: slugs.users, label: "Assigne a", admin: { position: "sidebar" } },
|
|
7753
|
+
{ name: "assignedTo", type: "relationship", relationTo: slugs.users, index: true, label: "Assigne a", admin: { position: "sidebar" } },
|
|
7754
|
+
{ name: "team", type: "relationship", relationTo: slugs.supportTeams, index: true, label: "Equipe", admin: { position: "sidebar" } },
|
|
7040
7755
|
{
|
|
7041
7756
|
name: "source",
|
|
7042
7757
|
type: "select",
|
|
@@ -7046,7 +7761,9 @@ function createTicketsCollection(slugs, options) {
|
|
|
7046
7761
|
{ label: "Portail client", value: "portal" },
|
|
7047
7762
|
{ label: "Chat en direct", value: "live-chat" },
|
|
7048
7763
|
{ label: "Email", value: "email" },
|
|
7049
|
-
{ label: "Admin", value: "admin" }
|
|
7764
|
+
{ label: "Admin", value: "admin" },
|
|
7765
|
+
{ label: "WhatsApp", value: "whatsapp" },
|
|
7766
|
+
{ label: "Messenger", value: "messenger" }
|
|
7050
7767
|
],
|
|
7051
7768
|
admin: { position: "sidebar" }
|
|
7052
7769
|
},
|
|
@@ -7097,8 +7814,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7097
7814
|
afterChange: [
|
|
7098
7815
|
createTrackSLA(slugs),
|
|
7099
7816
|
createTrackAiSummaryOnResolve(slugs),
|
|
7100
|
-
createAutoCalculateSLA(slugs),
|
|
7101
7817
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
7818
|
+
createPauseSlaOnHold(slugs),
|
|
7102
7819
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
7103
7820
|
createLogTicketActivity(slugs),
|
|
7104
7821
|
createNotifyOnAssignment(slugs),
|
|
@@ -7106,7 +7823,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7106
7823
|
createTicketStatusEmail(slugs),
|
|
7107
7824
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
7108
7825
|
createFireTicketWebhooks(slugs),
|
|
7109
|
-
createDispatchWebhookOnTicket(slugs)
|
|
7826
|
+
createDispatchWebhookOnTicket(slugs),
|
|
7827
|
+
createApplyAutomationRules(slugs)
|
|
7110
7828
|
],
|
|
7111
7829
|
beforeDelete: [createCascadeDelete(slugs)]
|
|
7112
7830
|
},
|
|
@@ -7117,12 +7835,20 @@ function createTicketsCollection(slugs, options) {
|
|
|
7117
7835
|
return false;
|
|
7118
7836
|
},
|
|
7119
7837
|
read: async ({ req }) => {
|
|
7120
|
-
if (req.user?.collection === slugs.users)
|
|
7838
|
+
if (req.user?.collection === slugs.users) {
|
|
7839
|
+
if (!isTeamScopingEnabled()) return true;
|
|
7840
|
+
const teamIds = await resolveAgentTeamIds(req.payload, slugs, req.user.id);
|
|
7841
|
+
return {
|
|
7842
|
+
or: [
|
|
7843
|
+
{ team: { in: teamIds.length > 0 ? teamIds : [-1] } },
|
|
7844
|
+
{ team: { exists: false } }
|
|
7845
|
+
]
|
|
7846
|
+
};
|
|
7847
|
+
}
|
|
7121
7848
|
if (req.user?.collection === slugs.supportClients) {
|
|
7122
7849
|
let collabTicketIds = [];
|
|
7123
7850
|
try {
|
|
7124
|
-
const rows = await req.payload
|
|
7125
|
-
collection: "ticket-collaborators",
|
|
7851
|
+
const rows = await dbFind(req.payload, "ticket-collaborators", {
|
|
7126
7852
|
where: { client: { equals: req.user.id } },
|
|
7127
7853
|
limit: 1e3,
|
|
7128
7854
|
depth: 0,
|
|
@@ -7157,6 +7883,125 @@ function createTicketsCollection(slugs, options) {
|
|
|
7157
7883
|
};
|
|
7158
7884
|
}
|
|
7159
7885
|
|
|
7886
|
+
// src/utils/ticketAccess.ts
|
|
7887
|
+
async function resolveAccessibleTicketIds(payload, slugs, clientId) {
|
|
7888
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7889
|
+
try {
|
|
7890
|
+
const owned = await dbFind(payload, slugs.tickets, {
|
|
7891
|
+
where: { client: { equals: clientId } },
|
|
7892
|
+
limit: 5e3,
|
|
7893
|
+
depth: 0,
|
|
7894
|
+
overrideAccess: true
|
|
7895
|
+
});
|
|
7896
|
+
for (const t of owned.docs) ids.add(t.id);
|
|
7897
|
+
} catch {
|
|
7898
|
+
}
|
|
7899
|
+
try {
|
|
7900
|
+
const collab = await dbFind(payload, "ticket-collaborators", {
|
|
7901
|
+
where: { client: { equals: clientId } },
|
|
7902
|
+
limit: 1e3,
|
|
7903
|
+
depth: 0,
|
|
7904
|
+
overrideAccess: true
|
|
7905
|
+
});
|
|
7906
|
+
for (const r of collab.docs) {
|
|
7907
|
+
const row = r;
|
|
7908
|
+
const tid = typeof row.ticket === "object" ? row.ticket?.id : row.ticket;
|
|
7909
|
+
if (tid !== void 0 && tid !== null) ids.add(tid);
|
|
7910
|
+
}
|
|
7911
|
+
} catch {
|
|
7912
|
+
}
|
|
7913
|
+
return Array.from(ids);
|
|
7914
|
+
}
|
|
7915
|
+
var OPTIONS = {
|
|
7916
|
+
allowedTags: [
|
|
7917
|
+
"p",
|
|
7918
|
+
"br",
|
|
7919
|
+
"b",
|
|
7920
|
+
"strong",
|
|
7921
|
+
"i",
|
|
7922
|
+
"em",
|
|
7923
|
+
"u",
|
|
7924
|
+
"s",
|
|
7925
|
+
"strike",
|
|
7926
|
+
"del",
|
|
7927
|
+
"ins",
|
|
7928
|
+
"mark",
|
|
7929
|
+
"a",
|
|
7930
|
+
"ul",
|
|
7931
|
+
"ol",
|
|
7932
|
+
"li",
|
|
7933
|
+
"blockquote",
|
|
7934
|
+
"code",
|
|
7935
|
+
"pre",
|
|
7936
|
+
"kbd",
|
|
7937
|
+
"span",
|
|
7938
|
+
"div",
|
|
7939
|
+
"h1",
|
|
7940
|
+
"h2",
|
|
7941
|
+
"h3",
|
|
7942
|
+
"h4",
|
|
7943
|
+
"h5",
|
|
7944
|
+
"h6",
|
|
7945
|
+
"hr",
|
|
7946
|
+
"img",
|
|
7947
|
+
"table",
|
|
7948
|
+
"thead",
|
|
7949
|
+
"tbody",
|
|
7950
|
+
"tr",
|
|
7951
|
+
"th",
|
|
7952
|
+
"td"
|
|
7953
|
+
],
|
|
7954
|
+
allowedAttributes: {
|
|
7955
|
+
a: ["href", "title", "target", "rel"],
|
|
7956
|
+
img: ["src", "alt", "title", "width", "height"],
|
|
7957
|
+
code: ["class"],
|
|
7958
|
+
pre: ["class"],
|
|
7959
|
+
span: ["class"],
|
|
7960
|
+
"*": ["style"]
|
|
7961
|
+
},
|
|
7962
|
+
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
7963
|
+
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
|
7964
|
+
allowProtocolRelative: false,
|
|
7965
|
+
disallowedTagsMode: "discard",
|
|
7966
|
+
// Force safe rel on links opening a new tab (defense against tabnabbing).
|
|
7967
|
+
transformTags: {
|
|
7968
|
+
a: sanitizeHtml.simpleTransform("a", { rel: "noopener noreferrer nofollow" }, true)
|
|
7969
|
+
},
|
|
7970
|
+
// Only allow CSS that cannot carry a script payload.
|
|
7971
|
+
allowedStyles: {
|
|
7972
|
+
"*": {
|
|
7973
|
+
color: [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}\s*\)$/i, /^[a-z-]+$/i],
|
|
7974
|
+
"background-color": [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}\s*\)$/i, /^[a-z-]+$/i],
|
|
7975
|
+
"text-align": [/^(left|right|center|justify)$/],
|
|
7976
|
+
"font-weight": [/^(normal|bold|[1-9]00)$/],
|
|
7977
|
+
"font-style": [/^(normal|italic)$/],
|
|
7978
|
+
"text-decoration": [/^(underline|line-through|none)$/]
|
|
7979
|
+
}
|
|
7980
|
+
}
|
|
7981
|
+
};
|
|
7982
|
+
function sanitizeMessageHtml(html) {
|
|
7983
|
+
if (!html) return html;
|
|
7984
|
+
return sanitizeHtml(html, OPTIONS);
|
|
7985
|
+
}
|
|
7986
|
+
|
|
7987
|
+
// src/utils/notificationQueue.ts
|
|
7988
|
+
async function queueClientNotification(payload, slugs, item) {
|
|
7989
|
+
try {
|
|
7990
|
+
await dbCreate(payload, slugs.notificationQueue, {
|
|
7991
|
+
data: {
|
|
7992
|
+
client: item.client,
|
|
7993
|
+
type: item.type,
|
|
7994
|
+
title: item.title,
|
|
7995
|
+
message: item.message,
|
|
7996
|
+
...item.ticket ? { ticket: item.ticket } : {}
|
|
7997
|
+
},
|
|
7998
|
+
overrideAccess: true
|
|
7999
|
+
});
|
|
8000
|
+
} catch (err) {
|
|
8001
|
+
console.error("[support] Failed to queue client notification:", err);
|
|
8002
|
+
}
|
|
8003
|
+
}
|
|
8004
|
+
|
|
7160
8005
|
// src/collections/TicketMessages.ts
|
|
7161
8006
|
function createAssignAuthor(slugs) {
|
|
7162
8007
|
return async ({ data, operation, req }) => {
|
|
@@ -7180,8 +8025,12 @@ function createAutoUpdateStatus(slugs) {
|
|
|
7180
8025
|
if (!doc.isInternal) {
|
|
7181
8026
|
if (doc.authorType === "admin") {
|
|
7182
8027
|
updateData.status = "waiting_client";
|
|
8028
|
+
updateData.autoCloseScheduledAt = null;
|
|
8029
|
+
updateData.autoCloseRemindedAt = null;
|
|
7183
8030
|
} else if (doc.authorType === "client" || doc.authorType === "email") {
|
|
7184
8031
|
updateData.lastClientMessageAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8032
|
+
updateData.autoCloseScheduledAt = null;
|
|
8033
|
+
updateData.autoCloseRemindedAt = null;
|
|
7185
8034
|
if (ticket.status && ["waiting_client", "resolved"].includes(ticket.status)) {
|
|
7186
8035
|
updateData.status = "open";
|
|
7187
8036
|
}
|
|
@@ -7215,29 +8064,46 @@ function createNotifyClient(slugs) {
|
|
|
7215
8064
|
const internalBcc = process.env.SUPPORT_CLIENT_BCC || void 0;
|
|
7216
8065
|
const rawContent = doc.bodyHtml ? emailRichContent(doc.bodyHtml) : emailQuote(doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body);
|
|
7217
8066
|
const replyPreview = (doc.body || "").replace(/\s+/g, " ").trim().slice(0, 90);
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
8067
|
+
const freq = client.notificationFrequency;
|
|
8068
|
+
if (freq === "daily" || freq === "weekly") {
|
|
8069
|
+
await queueClientNotification(req.payload, slugs, {
|
|
8070
|
+
client: client.id,
|
|
8071
|
+
type: "reply",
|
|
8072
|
+
title: `Nouvelle reponse \u2014 ${ticketNumber}`,
|
|
8073
|
+
message: replyPreview || `Reponse a votre ticket ${ticketNumber}`,
|
|
8074
|
+
ticket: ticketId
|
|
8075
|
+
});
|
|
8076
|
+
return doc;
|
|
8077
|
+
}
|
|
8078
|
+
void (async () => {
|
|
8079
|
+
try {
|
|
8080
|
+
await req.payload.sendEmail({
|
|
8081
|
+
to: client.email,
|
|
8082
|
+
...supportEmail ? { replyTo: supportEmail } : {},
|
|
8083
|
+
...internalBcc ? { bcc: internalBcc } : {},
|
|
8084
|
+
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
8085
|
+
html: emailWrapper(`Nouvelle reponse \u2014 ${ticketNumber}`, [
|
|
8086
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
|
|
8087
|
+
emailParagraph(`Notre equipe a apporte une reponse a votre ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(subject)}</em>.`),
|
|
8088
|
+
rawContent,
|
|
8089
|
+
emailButton("Consulter le ticket", portalUrl),
|
|
8090
|
+
emailParagraph('<span style="font-size: 13px; color: #6b7280;">Vous pouvez egalement repondre directement a cet email. Votre message sera automatiquement ajoute au ticket.</span>'),
|
|
8091
|
+
emailTrackingPixel(ticketId, doc.id)
|
|
8092
|
+
].join(""), {
|
|
8093
|
+
kind: "reply",
|
|
8094
|
+
preheader: replyPreview ? `${replyPreview}\u2026` : `Nouvelle reponse a votre ticket ${ticketNumber}`
|
|
8095
|
+
})
|
|
8096
|
+
});
|
|
8097
|
+
await req.payload.update({
|
|
8098
|
+
collection: slugs.ticketMessages,
|
|
8099
|
+
id: doc.id,
|
|
8100
|
+
data: { emailSentAt: (/* @__PURE__ */ new Date()).toISOString(), emailSentTo: client.email },
|
|
8101
|
+
overrideAccess: true
|
|
8102
|
+
});
|
|
8103
|
+
} catch (err) {
|
|
8104
|
+
console.error("[support] Failed to notify client (async):", err);
|
|
8105
|
+
}
|
|
8106
|
+
})();
|
|
7241
8107
|
} catch (err) {
|
|
7242
8108
|
console.error("[support] Failed to notify client:", err);
|
|
7243
8109
|
}
|
|
@@ -7275,8 +8141,7 @@ function createSyncTicketReplyToChat(slugs) {
|
|
|
7275
8141
|
if (!ticket?.chatSession) return doc;
|
|
7276
8142
|
if (doc.skipNotification) return doc;
|
|
7277
8143
|
const clientId = typeof ticket.client === "object" ? ticket.client.id : ticket.client;
|
|
7278
|
-
await payload.
|
|
7279
|
-
collection: slugs.chatMessages,
|
|
8144
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
7280
8145
|
data: {
|
|
7281
8146
|
session: ticket.chatSession,
|
|
7282
8147
|
client: clientId,
|
|
@@ -7393,6 +8258,57 @@ function createDispatchWebhookOnReply(slugs) {
|
|
|
7393
8258
|
return doc;
|
|
7394
8259
|
};
|
|
7395
8260
|
}
|
|
8261
|
+
function createSanitizeMessageHtml() {
|
|
8262
|
+
return ({ data }) => {
|
|
8263
|
+
if (data && typeof data.bodyHtml === "string" && data.bodyHtml) {
|
|
8264
|
+
data.bodyHtml = sanitizeMessageHtml(data.bodyHtml);
|
|
8265
|
+
}
|
|
8266
|
+
return data;
|
|
8267
|
+
};
|
|
8268
|
+
}
|
|
8269
|
+
var MENTION_RE = /@([^\s@]+@[^\s@]+\.[^\s@]+)/g;
|
|
8270
|
+
function createResolveMentions(slugs) {
|
|
8271
|
+
return async ({ data, operation, req }) => {
|
|
8272
|
+
if (operation !== "create") return data;
|
|
8273
|
+
const body = typeof data?.body === "string" ? data.body : "";
|
|
8274
|
+
const emails = Array.from(new Set([...body.matchAll(MENTION_RE)].map((m) => m[1].toLowerCase())));
|
|
8275
|
+
if (emails.length === 0) return data;
|
|
8276
|
+
try {
|
|
8277
|
+
const users = await dbFind(req.payload, slugs.users, { where: { email: { in: emails } }, limit: 50, depth: 0, overrideAccess: true });
|
|
8278
|
+
const ids = users.docs.map((u) => u.id);
|
|
8279
|
+
if (ids.length > 0) data.mentions = ids;
|
|
8280
|
+
} catch {
|
|
8281
|
+
}
|
|
8282
|
+
return data;
|
|
8283
|
+
};
|
|
8284
|
+
}
|
|
8285
|
+
function createNotifyMentions(slugs) {
|
|
8286
|
+
return async ({ doc, operation, req }) => {
|
|
8287
|
+
if (operation !== "create") return doc;
|
|
8288
|
+
const mentions = Array.isArray(doc.mentions) ? doc.mentions : [];
|
|
8289
|
+
if (mentions.length === 0) return doc;
|
|
8290
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8291
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
8292
|
+
for (const m of mentions) {
|
|
8293
|
+
const userId = typeof m === "object" ? m.id : m;
|
|
8294
|
+
try {
|
|
8295
|
+
const user = await dbFindByID(req.payload, slugs.users, { id: userId, depth: 0, overrideAccess: true });
|
|
8296
|
+
if (!user?.email) continue;
|
|
8297
|
+
await req.payload.sendEmail({
|
|
8298
|
+
to: user.email,
|
|
8299
|
+
subject: `Vous avez ete mentionne \u2014 ticket #${ticketId}`,
|
|
8300
|
+
html: emailWrapper("Mention", [
|
|
8301
|
+
emailParagraph(`Vous avez ete mentionne dans un message du ticket <strong>#${escapeHtml(String(ticketId))}</strong>.`),
|
|
8302
|
+
...baseUrl ? [emailButton("Ouvrir le ticket", `${baseUrl}/admin/collections/${slugs.tickets}/${ticketId}`, "dark")] : []
|
|
8303
|
+
].join(""), { kind: "system" })
|
|
8304
|
+
});
|
|
8305
|
+
} catch (err) {
|
|
8306
|
+
console.error("[support] Failed to notify mention:", err);
|
|
8307
|
+
}
|
|
8308
|
+
}
|
|
8309
|
+
return doc;
|
|
8310
|
+
};
|
|
8311
|
+
}
|
|
7396
8312
|
function createTicketMessagesCollection(slugs, options) {
|
|
7397
8313
|
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
7398
8314
|
return {
|
|
@@ -7400,7 +8316,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7400
8316
|
labels: { singular: "Message", plural: "Messages" },
|
|
7401
8317
|
admin: { hidden: true, group: "Support", defaultColumns: ["ticket", "authorType", "createdAt"] },
|
|
7402
8318
|
fields: [
|
|
7403
|
-
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, required: true, label: "Ticket" },
|
|
8319
|
+
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, required: true, index: true, label: "Ticket" },
|
|
7404
8320
|
{ name: "body", type: "textarea", required: true, label: "Message" },
|
|
7405
8321
|
{ name: "bodyHtml", type: "textarea", label: "Message HTML", admin: { hidden: true } },
|
|
7406
8322
|
{
|
|
@@ -7409,6 +8325,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7409
8325
|
{
|
|
7410
8326
|
name: "authorType",
|
|
7411
8327
|
type: "select",
|
|
8328
|
+
index: true,
|
|
7412
8329
|
label: "Type d'auteur",
|
|
7413
8330
|
defaultValue: "admin",
|
|
7414
8331
|
options: [
|
|
@@ -7444,8 +8361,9 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7444
8361
|
},
|
|
7445
8362
|
{ name: "isInternal", type: "checkbox", defaultValue: false, label: "Note interne", admin: { position: "sidebar" } },
|
|
7446
8363
|
{ name: "isSolution", type: "checkbox", defaultValue: false, label: "Reponse solution", admin: { position: "sidebar" } },
|
|
8364
|
+
{ name: "mentions", type: "relationship", relationTo: slugs.users, hasMany: true, label: "Agents mentionnes", admin: { hidden: true } },
|
|
7447
8365
|
{ name: "skipNotification", type: "checkbox", defaultValue: false, label: "Sans notification", admin: { position: "sidebar", condition: (data) => data?.skipNotification === true } },
|
|
7448
|
-
{ name: "scheduledAt", type: "date", label: "Programme pour", admin: { date: { pickerAppearance: "dayAndTime" }, position: "sidebar", condition: (data) => !!data?.scheduledAt } },
|
|
8366
|
+
{ name: "scheduledAt", type: "date", index: true, label: "Programme pour", admin: { date: { pickerAppearance: "dayAndTime" }, position: "sidebar", condition: (data) => !!data?.scheduledAt } },
|
|
7449
8367
|
{ name: "scheduledSent", type: "checkbox", defaultValue: false, admin: { hidden: true } },
|
|
7450
8368
|
{ name: "editedAt", type: "date", label: "Modifie le", admin: { hidden: true } },
|
|
7451
8369
|
{ name: "deletedAt", type: "date", label: "Supprime le", admin: { hidden: true } },
|
|
@@ -7454,7 +8372,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7454
8372
|
{ name: "emailOpenedAt", type: "date", label: "Email ouvert le", admin: { hidden: true } }
|
|
7455
8373
|
],
|
|
7456
8374
|
hooks: {
|
|
7457
|
-
beforeChange: [createAssignAuthor(slugs)],
|
|
8375
|
+
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
7458
8376
|
afterChange: [
|
|
7459
8377
|
createAutoUpdateStatus(slugs),
|
|
7460
8378
|
createNotifyClient(slugs),
|
|
@@ -7463,17 +8381,19 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7463
8381
|
createSyncTicketReplyToChat(slugs),
|
|
7464
8382
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
7465
8383
|
createFireMessageWebhooks(slugs),
|
|
7466
|
-
createDispatchWebhookOnReply(slugs)
|
|
8384
|
+
createDispatchWebhookOnReply(slugs),
|
|
8385
|
+
createNotifyMentions(slugs)
|
|
7467
8386
|
]
|
|
7468
8387
|
},
|
|
7469
8388
|
access: {
|
|
7470
8389
|
create: ({ req }) => req.user?.collection === slugs.users || req.user?.collection === slugs.supportClients,
|
|
7471
|
-
read: ({ req }) => {
|
|
8390
|
+
read: async ({ req }) => {
|
|
7472
8391
|
if (req.user?.collection === slugs.users) return true;
|
|
7473
8392
|
if (req.user?.collection === slugs.supportClients) {
|
|
8393
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
7474
8394
|
return {
|
|
7475
8395
|
and: [
|
|
7476
|
-
{
|
|
8396
|
+
{ ticket: { in: ids.length > 0 ? ids : [-1] } },
|
|
7477
8397
|
{ isInternal: { equals: false } },
|
|
7478
8398
|
{ or: [{ scheduledAt: { exists: false } }, { scheduledSent: { equals: true } }] }
|
|
7479
8399
|
]
|
|
@@ -7493,8 +8413,6 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7493
8413
|
timestamps: true
|
|
7494
8414
|
};
|
|
7495
8415
|
}
|
|
7496
|
-
|
|
7497
|
-
// src/collections/SupportClients.ts
|
|
7498
8416
|
function createSendInvitationOnCreate(slugs) {
|
|
7499
8417
|
return async ({ doc, operation, req }) => {
|
|
7500
8418
|
if (operation !== "create") return doc;
|
|
@@ -7534,6 +8452,26 @@ function createSendInvitationOnCreate(slugs) {
|
|
|
7534
8452
|
return doc;
|
|
7535
8453
|
};
|
|
7536
8454
|
}
|
|
8455
|
+
var TWO_FA_WINDOW_MS = 5 * 60 * 1e3;
|
|
8456
|
+
function createEnforce2FA(slugs) {
|
|
8457
|
+
return async ({ req, user }) => {
|
|
8458
|
+
if (!user?.twoFactorEnabled) return user;
|
|
8459
|
+
const raw = user.twoFactorVerifiedAt;
|
|
8460
|
+
const verifiedAt = raw ? new Date(raw).getTime() : 0;
|
|
8461
|
+
if (!(verifiedAt > Date.now() - TWO_FA_WINDOW_MS)) {
|
|
8462
|
+
throw new APIError("2FA_REQUIRED", 401);
|
|
8463
|
+
}
|
|
8464
|
+
await req.payload.update({
|
|
8465
|
+
collection: slugs.supportClients,
|
|
8466
|
+
id: user.id,
|
|
8467
|
+
data: { twoFactorVerifiedAt: null },
|
|
8468
|
+
req,
|
|
8469
|
+
overrideAccess: true
|
|
8470
|
+
});
|
|
8471
|
+
user.twoFactorVerifiedAt = null;
|
|
8472
|
+
return user;
|
|
8473
|
+
};
|
|
8474
|
+
}
|
|
7537
8475
|
function createSupportClientsCollection(slugs) {
|
|
7538
8476
|
return {
|
|
7539
8477
|
slug: slugs.supportClients,
|
|
@@ -7662,6 +8600,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7662
8600
|
name: "opportunities",
|
|
7663
8601
|
type: "textarea",
|
|
7664
8602
|
label: "Opportunites commerciales",
|
|
8603
|
+
access: {
|
|
8604
|
+
// Admin-only: never serialize commercial notes onto the client's own doc.
|
|
8605
|
+
read: ({ req }) => req.user?.collection === slugs.users
|
|
8606
|
+
},
|
|
7665
8607
|
admin: {
|
|
7666
8608
|
description: "Notes commerciales (visible uniquement par les admins)"
|
|
7667
8609
|
}
|
|
@@ -7705,6 +8647,19 @@ function createSupportClientsCollection(slugs) {
|
|
|
7705
8647
|
type: "date",
|
|
7706
8648
|
admin: { hidden: true }
|
|
7707
8649
|
},
|
|
8650
|
+
{
|
|
8651
|
+
// Set by /support/2fa "verify"; gates login via the beforeLogin hook.
|
|
8652
|
+
name: "twoFactorVerifiedAt",
|
|
8653
|
+
type: "date",
|
|
8654
|
+
admin: { hidden: true }
|
|
8655
|
+
},
|
|
8656
|
+
{
|
|
8657
|
+
// Google OAuth subject id — accounts are linked by this, not email alone.
|
|
8658
|
+
name: "googleId",
|
|
8659
|
+
type: "text",
|
|
8660
|
+
index: true,
|
|
8661
|
+
admin: { hidden: true }
|
|
8662
|
+
},
|
|
7708
8663
|
{
|
|
7709
8664
|
name: "notifyOnReply",
|
|
7710
8665
|
type: "checkbox",
|
|
@@ -7771,6 +8726,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7771
8726
|
name: "notes",
|
|
7772
8727
|
type: "textarea",
|
|
7773
8728
|
label: "Notes internes",
|
|
8729
|
+
access: {
|
|
8730
|
+
// Admin-only: never serialize internal notes onto the client's own doc.
|
|
8731
|
+
read: ({ req }) => req.user?.collection === slugs.users
|
|
8732
|
+
},
|
|
7774
8733
|
admin: {
|
|
7775
8734
|
description: "Visible uniquement par les admins",
|
|
7776
8735
|
position: "sidebar"
|
|
@@ -7778,6 +8737,7 @@ function createSupportClientsCollection(slugs) {
|
|
|
7778
8737
|
}
|
|
7779
8738
|
],
|
|
7780
8739
|
hooks: {
|
|
8740
|
+
beforeLogin: [createEnforce2FA(slugs)],
|
|
7781
8741
|
afterChange: [createSendInvitationOnCreate(slugs)]
|
|
7782
8742
|
},
|
|
7783
8743
|
access: {
|
|
@@ -7991,6 +8951,7 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
7991
8951
|
type: "relationship",
|
|
7992
8952
|
relationTo: slugs.tickets,
|
|
7993
8953
|
required: true,
|
|
8954
|
+
index: true,
|
|
7994
8955
|
label: "Ticket"
|
|
7995
8956
|
},
|
|
7996
8957
|
{
|
|
@@ -8036,10 +8997,11 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
8036
8997
|
access: {
|
|
8037
8998
|
create: () => false,
|
|
8038
8999
|
// Created only by hooks via overrideAccess
|
|
8039
|
-
read: ({ req }) => {
|
|
9000
|
+
read: async ({ req }) => {
|
|
8040
9001
|
if (req.user?.collection === slugs.users) return true;
|
|
8041
9002
|
if (req.user?.collection === slugs.supportClients) {
|
|
8042
|
-
|
|
9003
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
9004
|
+
return { ticket: { in: ids.length > 0 ? ids : [-1] } };
|
|
8043
9005
|
}
|
|
8044
9006
|
return false;
|
|
8045
9007
|
},
|
|
@@ -8108,12 +9070,21 @@ function createSatisfactionSurveysCollection(slugs) {
|
|
|
8108
9070
|
{
|
|
8109
9071
|
name: "rating",
|
|
8110
9072
|
type: "number",
|
|
8111
|
-
required: true,
|
|
8112
9073
|
min: 1,
|
|
8113
9074
|
max: 5,
|
|
8114
9075
|
label: "Note (1-5)",
|
|
8115
9076
|
admin: {
|
|
8116
|
-
description: "1 = Tr\xE8s insatisfait, 5 = Tr\xE8s satisfait"
|
|
9077
|
+
description: "1 = Tr\xE8s insatisfait, 5 = Tr\xE8s satisfait (CSAT)"
|
|
9078
|
+
}
|
|
9079
|
+
},
|
|
9080
|
+
{
|
|
9081
|
+
name: "nps",
|
|
9082
|
+
type: "number",
|
|
9083
|
+
min: 0,
|
|
9084
|
+
max: 10,
|
|
9085
|
+
label: "NPS (0-10)",
|
|
9086
|
+
admin: {
|
|
9087
|
+
description: "\xAB Recommanderiez-vous ? \xBB \u2014 promoteur 9-10, passif 7-8, d\xE9tracteur 0-6"
|
|
8117
9088
|
}
|
|
8118
9089
|
},
|
|
8119
9090
|
{
|
|
@@ -9359,6 +10330,124 @@ function createTicketCollaboratorsCollection(slugs) {
|
|
|
9359
10330
|
};
|
|
9360
10331
|
}
|
|
9361
10332
|
|
|
10333
|
+
// src/collections/NotificationQueue.ts
|
|
10334
|
+
function createNotificationQueueCollection(slugs) {
|
|
10335
|
+
return {
|
|
10336
|
+
slug: slugs.notificationQueue,
|
|
10337
|
+
labels: { singular: "Notification en file", plural: "Notifications en file" },
|
|
10338
|
+
admin: { hidden: true, group: "Support", defaultColumns: ["client", "type", "title", "createdAt"] },
|
|
10339
|
+
access: {
|
|
10340
|
+
// Internal: written/drained only via overrideAccess (hooks + cron).
|
|
10341
|
+
create: () => false,
|
|
10342
|
+
update: () => false,
|
|
10343
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10344
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10345
|
+
},
|
|
10346
|
+
fields: [
|
|
10347
|
+
{ name: "client", type: "relationship", relationTo: slugs.supportClients, required: true, index: true, label: "Client" },
|
|
10348
|
+
{ name: "type", type: "text", label: "Type" },
|
|
10349
|
+
{ name: "title", type: "text", label: "Titre" },
|
|
10350
|
+
{ name: "message", type: "textarea", label: "Message" },
|
|
10351
|
+
{ name: "ticket", type: "relationship", relationTo: slugs.tickets, label: "Ticket" }
|
|
10352
|
+
],
|
|
10353
|
+
timestamps: true
|
|
10354
|
+
};
|
|
10355
|
+
}
|
|
10356
|
+
|
|
10357
|
+
// src/collections/AutomationRules.ts
|
|
10358
|
+
var CONDITION_FIELDS = ["status", "priority", "category", "source", "subject"];
|
|
10359
|
+
var ACTION_TYPES = [
|
|
10360
|
+
{ label: "D\xE9finir le statut", value: "set_status" },
|
|
10361
|
+
{ label: "D\xE9finir la priorit\xE9", value: "set_priority" },
|
|
10362
|
+
{ label: "D\xE9finir la cat\xE9gorie", value: "set_category" },
|
|
10363
|
+
{ label: "Assigner \xE0 (id agent)", value: "assign" },
|
|
10364
|
+
{ label: "Ajouter un tag", value: "add_tag" }
|
|
10365
|
+
];
|
|
10366
|
+
function createAutomationRulesCollection(slugs) {
|
|
10367
|
+
return {
|
|
10368
|
+
slug: slugs.automationRules,
|
|
10369
|
+
labels: { singular: "R\xE8gle d'automatisation", plural: "R\xE8gles d'automatisation" },
|
|
10370
|
+
admin: { useAsTitle: "name", group: "Support", defaultColumns: ["name", "event", "enabled", "order"] },
|
|
10371
|
+
access: {
|
|
10372
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10373
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
10374
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
10375
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10376
|
+
},
|
|
10377
|
+
fields: [
|
|
10378
|
+
{ name: "name", type: "text", required: true, label: "Nom" },
|
|
10379
|
+
{ name: "enabled", type: "checkbox", defaultValue: true, label: "Active", index: true },
|
|
10380
|
+
{
|
|
10381
|
+
name: "event",
|
|
10382
|
+
type: "select",
|
|
10383
|
+
defaultValue: "ticket_created",
|
|
10384
|
+
index: true,
|
|
10385
|
+
label: "D\xE9clencheur",
|
|
10386
|
+
options: [
|
|
10387
|
+
{ label: "Ticket cr\xE9\xE9", value: "ticket_created" },
|
|
10388
|
+
{ label: "Ticket mis \xE0 jour", value: "ticket_updated" },
|
|
10389
|
+
{ label: "Changement de statut", value: "ticket_status_changed" }
|
|
10390
|
+
]
|
|
10391
|
+
},
|
|
10392
|
+
{
|
|
10393
|
+
name: "matchType",
|
|
10394
|
+
type: "select",
|
|
10395
|
+
defaultValue: "all",
|
|
10396
|
+
label: "Correspondance",
|
|
10397
|
+
options: [{ label: "Toutes les conditions (ET)", value: "all" }, { label: "Au moins une (OU)", value: "any" }]
|
|
10398
|
+
},
|
|
10399
|
+
{ name: "order", type: "number", defaultValue: 0, label: "Ordre d'\xE9valuation" },
|
|
10400
|
+
{
|
|
10401
|
+
name: "conditions",
|
|
10402
|
+
type: "array",
|
|
10403
|
+
label: "Conditions",
|
|
10404
|
+
fields: [
|
|
10405
|
+
{ name: "field", type: "select", required: true, options: CONDITION_FIELDS.map((v) => ({ label: v, value: v })) },
|
|
10406
|
+
{
|
|
10407
|
+
name: "operator",
|
|
10408
|
+
type: "select",
|
|
10409
|
+
defaultValue: "equals",
|
|
10410
|
+
required: true,
|
|
10411
|
+
options: [{ label: "= \xE9gal", value: "equals" }, { label: "\u2260 diff\xE9rent", value: "not_equals" }, { label: "contient", value: "contains" }]
|
|
10412
|
+
},
|
|
10413
|
+
{ name: "value", type: "text", required: true }
|
|
10414
|
+
]
|
|
10415
|
+
},
|
|
10416
|
+
{
|
|
10417
|
+
name: "actions",
|
|
10418
|
+
type: "array",
|
|
10419
|
+
label: "Actions",
|
|
10420
|
+
fields: [
|
|
10421
|
+
{ name: "type", type: "select", required: true, options: ACTION_TYPES },
|
|
10422
|
+
{ name: "value", type: "text", required: true }
|
|
10423
|
+
]
|
|
10424
|
+
}
|
|
10425
|
+
],
|
|
10426
|
+
timestamps: true
|
|
10427
|
+
};
|
|
10428
|
+
}
|
|
10429
|
+
|
|
10430
|
+
// src/collections/SupportTeam.ts
|
|
10431
|
+
function createSupportTeamCollection(slugs) {
|
|
10432
|
+
return {
|
|
10433
|
+
slug: slugs.supportTeams,
|
|
10434
|
+
labels: { singular: "\xC9quipe support", plural: "\xC9quipes support" },
|
|
10435
|
+
admin: { useAsTitle: "name", group: "Support", defaultColumns: ["name", "members"] },
|
|
10436
|
+
access: {
|
|
10437
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
10438
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
10439
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
10440
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10441
|
+
},
|
|
10442
|
+
fields: [
|
|
10443
|
+
{ name: "name", type: "text", required: true, label: "Nom" },
|
|
10444
|
+
{ name: "description", type: "textarea", label: "Description" },
|
|
10445
|
+
{ name: "members", type: "relationship", relationTo: slugs.users, hasMany: true, index: true, label: "Membres (agents)" }
|
|
10446
|
+
],
|
|
10447
|
+
timestamps: true
|
|
10448
|
+
};
|
|
10449
|
+
}
|
|
10450
|
+
|
|
9362
10451
|
// src/plugin.ts
|
|
9363
10452
|
function viewConfig(component, path) {
|
|
9364
10453
|
return { Component: component, path };
|
|
@@ -9394,7 +10483,10 @@ function supportPlugin(config) {
|
|
|
9394
10483
|
createSatisfactionSurveysCollection(slugs),
|
|
9395
10484
|
createKnowledgeBaseCollection(slugs),
|
|
9396
10485
|
createTicketFeedbackCollection(slugs, { notificationSlug: config?.notificationSlug }),
|
|
9397
|
-
createTicketCollaboratorsCollection(slugs)
|
|
10486
|
+
createTicketCollaboratorsCollection(slugs),
|
|
10487
|
+
createNotificationQueueCollection(slugs),
|
|
10488
|
+
createAutomationRulesCollection(slugs),
|
|
10489
|
+
createSupportTeamCollection(slugs)
|
|
9398
10490
|
];
|
|
9399
10491
|
if (features.authLogs !== false) supportCollections.push(createAuthLogsCollection(slugs));
|
|
9400
10492
|
if (features.timeTracking !== false) supportCollections.push(createTimeEntriesCollection(slugs));
|