@consilioweb/payload-support 0.16.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +347 -435
- package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
- package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
- package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
- package/dist/components/TicketConversation/SkeletonText.js +20 -0
- package/dist/components/TicketConversation/constants.cjs +27 -0
- package/dist/components/TicketConversation/constants.js +26 -1
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
- package/dist/components/TicketConversation/index.cjs +10 -147
- package/dist/components/TicketConversation/index.js +4 -141
- package/dist/index.cjs +1604 -671
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1602 -671
- package/dist/views/BillingView/client.cjs +40 -54
- package/dist/views/BillingView/client.js +40 -54
- package/dist/views/ChatView/client.cjs +4 -3
- package/dist/views/ChatView/client.js +4 -3
- package/dist/views/CrmView/client.cjs +7 -7
- package/dist/views/CrmView/client.js +7 -7
- package/dist/views/EmailTrackingView/client.cjs +6 -5
- package/dist/views/EmailTrackingView/client.js +6 -5
- package/dist/views/ImportConversationView/client.cjs +4 -4
- package/dist/views/ImportConversationView/client.js +4 -4
- package/dist/views/LogsView/client.cjs +3 -2
- package/dist/views/LogsView/client.js +3 -2
- package/dist/views/NewTicketView/client.cjs +1 -1
- package/dist/views/NewTicketView/client.js +1 -1
- package/dist/views/PendingEmailsView/client.cjs +7 -6
- package/dist/views/PendingEmailsView/client.js +7 -6
- package/dist/views/SupportDashboardView/client.cjs +19 -22
- package/dist/views/SupportDashboardView/client.js +19 -22
- package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
- package/dist/views/TicketDetailView/NextActionItem.js +34 -0
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
- package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
- package/dist/views/TicketDetailView/client.cjs +59 -190
- package/dist/views/TicketDetailView/client.js +52 -183
- package/dist/views/TicketDetailView/constants.cjs +20 -0
- package/dist/views/TicketDetailView/constants.js +16 -0
- package/dist/views/TicketDetailView/helpers.cjs +23 -0
- package/dist/views/TicketDetailView/helpers.js +20 -0
- package/dist/views/TicketDetailView/types.cjs +2 -0
- package/dist/views/TicketDetailView/types.js +1 -0
- package/dist/views/TicketInboxView/client.cjs +14 -10
- package/dist/views/TicketInboxView/client.js +14 -10
- package/dist/views/TicketingSettingsView/client.cjs +101 -96
- package/dist/views/TicketingSettingsView/client.js +92 -88
- package/dist/views/TimeDashboardView/client.cjs +2 -2
- package/dist/views/TimeDashboardView/client.js +2 -2
- package/dist/views/shared/StatusPill.cjs +23 -12
- package/dist/views/shared/StatusPill.js +23 -12
- package/dist/views/shared/dateLocale.cjs +5 -0
- package/dist/views/shared/dateLocale.js +3 -0
- package/package.json +23 -15
- package/src/__tests__/generateTrackingToken.test.ts +53 -0
- package/src/__tests__/integration/ai-agent.test.ts +46 -0
- package/src/__tests__/integration/auth.test.ts +43 -0
- package/src/__tests__/integration/automation-rules.test.ts +68 -0
- package/src/__tests__/integration/buildTestPayload.ts +52 -0
- package/src/__tests__/integration/channels.test.ts +52 -0
- package/src/__tests__/integration/core-behaviors.test.ts +58 -0
- package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
- package/src/__tests__/integration/digest.test.ts +48 -0
- package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
- package/src/__tests__/integration/isolation.test.ts +81 -0
- package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
- package/src/__tests__/integration/nps.test.ts +37 -0
- package/src/__tests__/integration/sanitization.test.ts +35 -0
- package/src/__tests__/integration/sla-pause.test.ts +39 -0
- package/src/__tests__/integration/smoke.test.ts +26 -0
- package/src/__tests__/integration/snooze.test.ts +68 -0
- package/src/__tests__/integration/teams.test.ts +38 -0
- package/src/__tests__/rateLimiter.test.ts +89 -0
- package/src/__tests__/sanitizeHtml.test.ts +165 -0
- package/src/__tests__/sla.test.ts +258 -0
- package/src/collections/AutomationRules.ts +66 -0
- package/src/collections/NotificationQueue.ts +30 -0
- package/src/collections/SatisfactionSurveys.ts +11 -2
- package/src/collections/SupportClients.ts +58 -1
- package/src/collections/SupportTeam.ts +27 -0
- package/src/collections/TicketActivityLog.ts +9 -4
- package/src/collections/TicketMessages.ts +129 -31
- package/src/collections/Tickets.ts +31 -73
- package/src/collections/index.ts +3 -0
- package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
- package/src/components/TicketConversation/SkeletonText.tsx +23 -0
- package/src/components/TicketConversation/constants.ts +29 -0
- package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
- package/src/components/TicketConversation/index.tsx +4 -152
- package/src/endpoints/admin-chat-stream.ts +3 -4
- package/src/endpoints/admin-chat.ts +11 -20
- package/src/endpoints/admin-stats.ts +70 -32
- package/src/endpoints/ai-agent.ts +36 -0
- package/src/endpoints/apply-macro.ts +8 -14
- package/src/endpoints/auth-2fa.ts +12 -12
- package/src/endpoints/auto-close.ts +6 -10
- package/src/endpoints/billing.ts +17 -4
- package/src/endpoints/bulk-action.ts +7 -12
- package/src/endpoints/channels.ts +89 -0
- package/src/endpoints/chat-stream.ts +3 -4
- package/src/endpoints/chat.ts +7 -12
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/client-intelligence.ts +4 -4
- package/src/endpoints/delete-account.ts +9 -16
- package/src/endpoints/email-stats.ts +2 -2
- package/src/endpoints/escalate.ts +2 -2
- package/src/endpoints/export-csv.ts +2 -2
- package/src/endpoints/export-data.ts +5 -8
- package/src/endpoints/import-conversation.ts +6 -10
- package/src/endpoints/index.ts +18 -1
- package/src/endpoints/invite-collaborator.ts +30 -15
- package/src/endpoints/invoice.ts +127 -0
- package/src/endpoints/kb-search.ts +2 -2
- package/src/endpoints/login.ts +12 -4
- package/src/endpoints/merge-clients.ts +14 -19
- package/src/endpoints/merge-tickets.ts +11 -18
- package/src/endpoints/oauth-google.ts +86 -39
- package/src/endpoints/pending-emails-process.ts +15 -25
- package/src/endpoints/process-digests.ts +93 -0
- package/src/endpoints/process-scheduled.ts +6 -10
- package/src/endpoints/process-snooze.ts +78 -0
- package/src/endpoints/purge-logs.ts +2 -2
- package/src/endpoints/resend-notification.ts +3 -4
- package/src/endpoints/round-robin-config.ts +3 -4
- package/src/endpoints/satisfaction.ts +21 -14
- package/src/endpoints/search.ts +5 -8
- package/src/endpoints/seed-kb.ts +3 -4
- package/src/endpoints/send-reminder.ts +6 -9
- package/src/endpoints/settings.ts +5 -2
- package/src/endpoints/signature.ts +3 -4
- package/src/endpoints/sla-check.ts +2 -2
- package/src/endpoints/split-ticket.ts +7 -12
- package/src/endpoints/statuses.ts +2 -2
- package/src/endpoints/ticket-feedback.ts +4 -6
- package/src/endpoints/ticket-synthesis.ts +2 -2
- package/src/endpoints/track-open.ts +16 -28
- package/src/endpoints/transfer-ticket.ts +21 -9
- package/src/endpoints/user-prefs.ts +3 -4
- package/src/hooks/automationRules.ts +84 -0
- package/src/hooks/checkSLA.ts +48 -16
- package/src/plugin.ts +6 -0
- package/src/portal/LiveChat.tsx +2 -63
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
- package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
- package/src/portal/auth/tickets/detail/page.tsx +2 -2
- package/src/portal/helpers.ts +46 -0
- package/src/portal/login/page.tsx +3 -8
- package/src/portal/types.ts +15 -0
- package/src/types/lucide-react.d.ts +34 -0
- package/src/types/payload-config.d.ts +10 -0
- package/src/utils/adminNotification.ts +2 -2
- package/src/utils/aiAgent.ts +147 -0
- package/src/utils/auth.ts +5 -4
- package/src/utils/channels.ts +36 -0
- package/src/utils/db.ts +73 -0
- package/src/utils/emailTemplate.ts +8 -1
- package/src/utils/fireWebhooks.ts +4 -6
- package/src/utils/generateTicketSynthesis.ts +7 -12
- package/src/utils/invoicePdf.ts +88 -0
- package/src/utils/notificationQueue.ts +36 -0
- package/src/utils/readSettings.ts +22 -6
- package/src/utils/sanitizeHtml.ts +57 -0
- package/src/utils/slugs.ts +6 -0
- package/src/utils/teamAccess.ts +27 -0
- package/src/utils/ticketAccess.ts +53 -0
- package/src/utils/webhookDispatcher.ts +4 -6
- package/src/views/BillingView/client.tsx +49 -37
- package/src/views/ChatView/client.tsx +4 -3
- package/src/views/CrmView/client.tsx +7 -7
- package/src/views/EmailTrackingView/client.tsx +7 -6
- package/src/views/ImportConversationView/client.tsx +4 -4
- package/src/views/LogsView/client.tsx +3 -2
- package/src/views/NewTicketView/client.tsx +1 -1
- package/src/views/PendingEmailsView/client.tsx +7 -6
- package/src/views/SupportDashboardView/client.tsx +21 -21
- package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
- package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
- package/src/views/TicketDetailView/client.tsx +54 -179
- package/src/views/TicketDetailView/constants.ts +23 -0
- package/src/views/TicketDetailView/helpers.ts +17 -0
- package/src/views/TicketDetailView/types.ts +10 -0
- package/src/views/TicketInboxView/client.tsx +16 -10
- package/src/views/TicketingSettingsView/client.tsx +90 -87
- package/src/views/TimeDashboardView/client.tsx +2 -2
- package/src/views/shared/StatusPill.tsx +24 -12
- package/src/views/shared/dateLocale.ts +5 -0
- package/src/views/shared/locales/en.json +708 -0
- package/src/views/shared/locales/fr.json +708 -0
package/dist/index.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,8 +2091,7 @@ 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" } },
|
|
@@ -1899,8 +2126,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1899
2126
|
const closeTotalDays = REMIND_AFTER_DAYS + CLOSE_AFTER_REMIND_DAYS;
|
|
1900
2127
|
const viaSchedule = !!t.autoCloseScheduledAt && new Date(t.autoCloseScheduledAt).getTime() <= now.getTime();
|
|
1901
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`;
|
|
1902
|
-
await payload.
|
|
1903
|
-
collection: slugs.ticketMessages,
|
|
2129
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
1904
2130
|
data: {
|
|
1905
2131
|
ticket: t.id,
|
|
1906
2132
|
body: noteBody,
|
|
@@ -1910,8 +2136,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
1910
2136
|
},
|
|
1911
2137
|
overrideAccess: true
|
|
1912
2138
|
});
|
|
1913
|
-
await payload.
|
|
1914
|
-
collection: slugs.tickets,
|
|
2139
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
1915
2140
|
id: t.id,
|
|
1916
2141
|
// Clear the armed deadline so a later manual reopen can't be
|
|
1917
2142
|
// closed instantly by a stale timestamp.
|
|
@@ -2034,8 +2259,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2034
2259
|
}
|
|
2035
2260
|
const rawHours = Number(body.hours);
|
|
2036
2261
|
const hours = Number.isFinite(rawHours) ? Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.round(rawHours))) : 24;
|
|
2037
|
-
const ticket = await payload.
|
|
2038
|
-
collection: slugs.tickets,
|
|
2262
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2039
2263
|
id: ticketId,
|
|
2040
2264
|
depth: 1,
|
|
2041
2265
|
overrideAccess: true
|
|
@@ -2057,8 +2281,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2057
2281
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_REPLY_TO || "";
|
|
2058
2282
|
let waitingSince = ticket.firstResponseAt || ticket.createdAt;
|
|
2059
2283
|
try {
|
|
2060
|
-
const lastAdminMsg = await payload.
|
|
2061
|
-
collection: slugs.ticketMessages,
|
|
2284
|
+
const lastAdminMsg = await dbFind(payload, slugs.ticketMessages, {
|
|
2062
2285
|
where: {
|
|
2063
2286
|
and: [
|
|
2064
2287
|
{ ticket: { equals: ticketId } },
|
|
@@ -2099,8 +2322,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2099
2322
|
preheader: `Sans r\xE9ponse de votre part, votre ticket ${ticketNumber} sera cl\xF4tur\xE9 le ${deadlineLabel}.`
|
|
2100
2323
|
})
|
|
2101
2324
|
});
|
|
2102
|
-
await payload.
|
|
2103
|
-
collection: slugs.ticketMessages,
|
|
2325
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2104
2326
|
data: {
|
|
2105
2327
|
ticket: ticketId,
|
|
2106
2328
|
body: `Relance envoy\xE9e \xE0 ${client.email}. Fermeture automatique programm\xE9e le ${deadlineLabel} sans r\xE9ponse du client.`,
|
|
@@ -2110,8 +2332,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2110
2332
|
},
|
|
2111
2333
|
overrideAccess: true
|
|
2112
2334
|
});
|
|
2113
|
-
await payload.
|
|
2114
|
-
collection: slugs.tickets,
|
|
2335
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2115
2336
|
id: ticketId,
|
|
2116
2337
|
data: {
|
|
2117
2338
|
status: "waiting_client",
|
|
@@ -2146,8 +2367,7 @@ function createStatusesEndpoint(slugs) {
|
|
|
2146
2367
|
if (!req.user) {
|
|
2147
2368
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2148
2369
|
}
|
|
2149
|
-
const { docs } = await payload.
|
|
2150
|
-
collection: slugs.ticketStatuses,
|
|
2370
|
+
const { docs } = await dbFind(payload, slugs.ticketStatuses, {
|
|
2151
2371
|
sort: "sortOrder",
|
|
2152
2372
|
limit: 100,
|
|
2153
2373
|
depth: 0,
|
|
@@ -2186,16 +2406,14 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2186
2406
|
if (!macroId || !ticketId) {
|
|
2187
2407
|
return Response.json({ error: "macroId and ticketId are required" }, { status: 400 });
|
|
2188
2408
|
}
|
|
2189
|
-
const macro = await payload.
|
|
2190
|
-
collection: slugs.macros,
|
|
2409
|
+
const macro = await dbFindByID(payload, slugs.macros, {
|
|
2191
2410
|
id: macroId,
|
|
2192
2411
|
depth: 0,
|
|
2193
2412
|
overrideAccess: true
|
|
2194
2413
|
});
|
|
2195
2414
|
if (!macro) return Response.json({ error: "Macro not found" }, { status: 404 });
|
|
2196
2415
|
if (!macro.isActive) return Response.json({ error: "Macro is disabled" }, { status: 400 });
|
|
2197
|
-
const ticket = await payload.
|
|
2198
|
-
collection: slugs.tickets,
|
|
2416
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
2199
2417
|
id: ticketId,
|
|
2200
2418
|
depth: 0,
|
|
2201
2419
|
overrideAccess: true
|
|
@@ -2207,8 +2425,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2207
2425
|
try {
|
|
2208
2426
|
switch (action.type) {
|
|
2209
2427
|
case "set_status":
|
|
2210
|
-
await payload.
|
|
2211
|
-
collection: slugs.tickets,
|
|
2428
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2212
2429
|
id: ticketId,
|
|
2213
2430
|
data: { status: action.value },
|
|
2214
2431
|
overrideAccess: true
|
|
@@ -2216,8 +2433,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2216
2433
|
appliedActions.push({ type: action.type, value: action.value, success: true });
|
|
2217
2434
|
break;
|
|
2218
2435
|
case "set_priority":
|
|
2219
|
-
await payload.
|
|
2220
|
-
collection: slugs.tickets,
|
|
2436
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2221
2437
|
id: ticketId,
|
|
2222
2438
|
data: { priority: action.value },
|
|
2223
2439
|
overrideAccess: true
|
|
@@ -2229,8 +2445,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2229
2445
|
if (!currentTags.includes(action.value)) {
|
|
2230
2446
|
currentTags.push(action.value);
|
|
2231
2447
|
}
|
|
2232
|
-
await payload.
|
|
2233
|
-
collection: slugs.tickets,
|
|
2448
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2234
2449
|
id: ticketId,
|
|
2235
2450
|
data: { tags: currentTags },
|
|
2236
2451
|
overrideAccess: true
|
|
@@ -2239,8 +2454,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2239
2454
|
break;
|
|
2240
2455
|
}
|
|
2241
2456
|
case "send_reply":
|
|
2242
|
-
await payload.
|
|
2243
|
-
collection: slugs.ticketMessages,
|
|
2457
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2244
2458
|
data: {
|
|
2245
2459
|
ticket: ticketId,
|
|
2246
2460
|
body: action.value,
|
|
@@ -2257,8 +2471,7 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2257
2471
|
appliedActions.push({ type: action.type, value: action.value, success: false, error: "Invalid user ID" });
|
|
2258
2472
|
break;
|
|
2259
2473
|
}
|
|
2260
|
-
await payload.
|
|
2261
|
-
collection: slugs.tickets,
|
|
2474
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
2262
2475
|
id: ticketId,
|
|
2263
2476
|
data: { assignedTo: userId },
|
|
2264
2477
|
overrideAccess: true
|
|
@@ -2310,8 +2523,7 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2310
2523
|
return Response.json({ error: "Invalid collection. Use email-logs or auth-logs." }, { status: 400 });
|
|
2311
2524
|
}
|
|
2312
2525
|
const cutoff = days > 0 ? new Date(Date.now() - days * 864e5).toISOString() : null;
|
|
2313
|
-
const result = await payload
|
|
2314
|
-
collection: allowedCollections[collection],
|
|
2526
|
+
const result = await dbDelete(payload, allowedCollections[collection], {
|
|
2315
2527
|
where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
|
|
2316
2528
|
overrideAccess: true
|
|
2317
2529
|
});
|
|
@@ -2354,8 +2566,7 @@ function createChatbotEndpoint(slugs) {
|
|
|
2354
2566
|
return Response.json({ error: "Question too short" }, { status: 400 });
|
|
2355
2567
|
}
|
|
2356
2568
|
const payload = req.payload;
|
|
2357
|
-
const articles = await payload.
|
|
2358
|
-
collection: slugs.knowledgeBase,
|
|
2569
|
+
const articles = await dbFind(payload, slugs.knowledgeBase, {
|
|
2359
2570
|
where: { published: { equals: true } },
|
|
2360
2571
|
limit: 100,
|
|
2361
2572
|
depth: 0,
|
|
@@ -2445,8 +2656,7 @@ function createChatGetEndpoint(slugs) {
|
|
|
2445
2656
|
if (after) {
|
|
2446
2657
|
where.createdAt = { greater_than: after };
|
|
2447
2658
|
}
|
|
2448
|
-
const messages = await payload.
|
|
2449
|
-
collection: slugs.chatMessages,
|
|
2659
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2450
2660
|
where,
|
|
2451
2661
|
sort: "createdAt",
|
|
2452
2662
|
limit: 100,
|
|
@@ -2489,8 +2699,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2489
2699
|
return Response.json({ error: "Trop de sessions cr\xE9\xE9es. R\xE9essayez plus tard." }, { status: 429 });
|
|
2490
2700
|
}
|
|
2491
2701
|
const sessionId = `chat_${crypto3.randomBytes(16).toString("hex")}`;
|
|
2492
|
-
const systemMsg = await payload.
|
|
2493
|
-
collection: slugs.chatMessages,
|
|
2702
|
+
const systemMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2494
2703
|
data: {
|
|
2495
2704
|
session: sessionId,
|
|
2496
2705
|
client: req.user.id,
|
|
@@ -2510,8 +2719,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2510
2719
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2511
2720
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2512
2721
|
}
|
|
2513
|
-
const existing = await payload.
|
|
2514
|
-
collection: slugs.chatMessages,
|
|
2722
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2515
2723
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2516
2724
|
limit: 1,
|
|
2517
2725
|
overrideAccess: true
|
|
@@ -2519,8 +2727,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2519
2727
|
if (existing.docs.length === 0) {
|
|
2520
2728
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2521
2729
|
}
|
|
2522
|
-
const newMsg = await payload.
|
|
2523
|
-
collection: slugs.chatMessages,
|
|
2730
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2524
2731
|
data: {
|
|
2525
2732
|
session,
|
|
2526
2733
|
client: req.user.id,
|
|
@@ -2533,8 +2740,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2533
2740
|
return Response.json({ message: newMsg });
|
|
2534
2741
|
}
|
|
2535
2742
|
if (action === "close" && session) {
|
|
2536
|
-
const existing = await payload.
|
|
2537
|
-
collection: slugs.chatMessages,
|
|
2743
|
+
const existing = await dbFind(payload, slugs.chatMessages, {
|
|
2538
2744
|
where: { session: { equals: session }, client: { equals: req.user.id } },
|
|
2539
2745
|
limit: 1,
|
|
2540
2746
|
overrideAccess: true
|
|
@@ -2542,8 +2748,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2542
2748
|
if (existing.docs.length === 0) {
|
|
2543
2749
|
return Response.json({ error: "Session invalide" }, { status: 403 });
|
|
2544
2750
|
}
|
|
2545
|
-
await payload.
|
|
2546
|
-
collection: slugs.chatMessages,
|
|
2751
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2547
2752
|
data: {
|
|
2548
2753
|
session,
|
|
2549
2754
|
client: req.user.id,
|
|
@@ -2585,8 +2790,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2585
2790
|
if (!sessionId) {
|
|
2586
2791
|
return Response.json({ error: "Missing session parameter" }, { status: 400 });
|
|
2587
2792
|
}
|
|
2588
|
-
const existing = await req.payload.
|
|
2589
|
-
collection: slugs.chatMessages,
|
|
2793
|
+
const existing = await dbFind(req.payload, slugs.chatMessages, {
|
|
2590
2794
|
where: {
|
|
2591
2795
|
session: { equals: sessionId },
|
|
2592
2796
|
client: { equals: userId }
|
|
@@ -2608,8 +2812,7 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2608
2812
|
);
|
|
2609
2813
|
const interval = setInterval(async () => {
|
|
2610
2814
|
try {
|
|
2611
|
-
const messages = await req.payload.
|
|
2612
|
-
collection: slugs.chatMessages,
|
|
2815
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2613
2816
|
where: {
|
|
2614
2817
|
session: { equals: sessionId },
|
|
2615
2818
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2690,8 +2893,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2690
2893
|
if (session) {
|
|
2691
2894
|
const where = { session: { equals: session } };
|
|
2692
2895
|
if (after) where.createdAt = { greater_than: after };
|
|
2693
|
-
const messages = await payload.
|
|
2694
|
-
collection: slugs.chatMessages,
|
|
2896
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
2695
2897
|
where,
|
|
2696
2898
|
sort: "createdAt",
|
|
2697
2899
|
limit: 200,
|
|
@@ -2705,8 +2907,7 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2705
2907
|
}
|
|
2706
2908
|
});
|
|
2707
2909
|
}
|
|
2708
|
-
const recentMessages = await payload.
|
|
2709
|
-
collection: slugs.chatMessages,
|
|
2910
|
+
const recentMessages = await dbFind(payload, slugs.chatMessages, {
|
|
2710
2911
|
sort: "-createdAt",
|
|
2711
2912
|
limit: 50,
|
|
2712
2913
|
depth: 1,
|
|
@@ -2769,8 +2970,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2769
2970
|
if (!session) {
|
|
2770
2971
|
return Response.json({ error: "Session requise" }, { status: 400 });
|
|
2771
2972
|
}
|
|
2772
|
-
const sessionMsg = await payload.
|
|
2773
|
-
collection: slugs.chatMessages,
|
|
2973
|
+
const sessionMsg = await dbFind(payload, slugs.chatMessages, {
|
|
2774
2974
|
where: { session: { equals: session } },
|
|
2775
2975
|
limit: 1,
|
|
2776
2976
|
depth: 0,
|
|
@@ -2788,8 +2988,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2788
2988
|
if (!trimmedMessage || trimmedMessage.length > 2e3) {
|
|
2789
2989
|
return Response.json({ error: "Message invalide (1-2000 caract\xE8res)." }, { status: 400 });
|
|
2790
2990
|
}
|
|
2791
|
-
const newMsg = await payload.
|
|
2792
|
-
collection: slugs.chatMessages,
|
|
2991
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
2793
2992
|
data: {
|
|
2794
2993
|
session,
|
|
2795
2994
|
client: clientId,
|
|
@@ -2801,8 +3000,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2801
3000
|
overrideAccess: true
|
|
2802
3001
|
});
|
|
2803
3002
|
try {
|
|
2804
|
-
const linkedTicket = await payload.
|
|
2805
|
-
collection: slugs.tickets,
|
|
3003
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2806
3004
|
where: { chatSession: { equals: session } },
|
|
2807
3005
|
limit: 1,
|
|
2808
3006
|
depth: 0,
|
|
@@ -2810,8 +3008,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2810
3008
|
});
|
|
2811
3009
|
if (linkedTicket.docs.length > 0) {
|
|
2812
3010
|
const ticketId = linkedTicket.docs[0].id;
|
|
2813
|
-
await payload.
|
|
2814
|
-
collection: slugs.ticketMessages,
|
|
3011
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2815
3012
|
data: {
|
|
2816
3013
|
ticket: ticketId,
|
|
2817
3014
|
body: trimmedMessage,
|
|
@@ -2820,8 +3017,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2820
3017
|
},
|
|
2821
3018
|
overrideAccess: true
|
|
2822
3019
|
});
|
|
2823
|
-
await payload.
|
|
2824
|
-
collection: slugs.chatMessages,
|
|
3020
|
+
await dbUpdate(payload, slugs.chatMessages, {
|
|
2825
3021
|
id: newMsg.id,
|
|
2826
3022
|
data: { ticket: ticketId },
|
|
2827
3023
|
overrideAccess: true
|
|
@@ -2833,8 +3029,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2833
3029
|
return Response.json({ message: newMsg });
|
|
2834
3030
|
}
|
|
2835
3031
|
if (action === "close") {
|
|
2836
|
-
await payload.
|
|
2837
|
-
collection: slugs.chatMessages,
|
|
3032
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
2838
3033
|
data: {
|
|
2839
3034
|
session,
|
|
2840
3035
|
client: clientId,
|
|
@@ -2845,16 +3040,14 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2845
3040
|
overrideAccess: true
|
|
2846
3041
|
});
|
|
2847
3042
|
try {
|
|
2848
|
-
const linkedTicket = await payload.
|
|
2849
|
-
collection: slugs.tickets,
|
|
3043
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
2850
3044
|
where: { chatSession: { equals: session } },
|
|
2851
3045
|
limit: 1,
|
|
2852
3046
|
depth: 0,
|
|
2853
3047
|
overrideAccess: true
|
|
2854
3048
|
});
|
|
2855
3049
|
if (linkedTicket.docs.length > 0) {
|
|
2856
|
-
await payload.
|
|
2857
|
-
collection: slugs.ticketMessages,
|
|
3050
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
2858
3051
|
data: {
|
|
2859
3052
|
ticket: linkedTicket.docs[0].id,
|
|
2860
3053
|
body: "Session de chat termin\xE9e par un agent.",
|
|
@@ -2917,8 +3110,7 @@ function createSessionStream(req, slugs, sessionId) {
|
|
|
2917
3110
|
);
|
|
2918
3111
|
const interval = setInterval(async () => {
|
|
2919
3112
|
try {
|
|
2920
|
-
const messages = await req.payload.
|
|
2921
|
-
collection: slugs.chatMessages,
|
|
3113
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2922
3114
|
where: {
|
|
2923
3115
|
session: { equals: sessionId },
|
|
2924
3116
|
createdAt: { greater_than: lastCheck }
|
|
@@ -2991,8 +3183,7 @@ function createSessionListStream(req, slugs) {
|
|
|
2991
3183
|
);
|
|
2992
3184
|
const interval = setInterval(async () => {
|
|
2993
3185
|
try {
|
|
2994
|
-
const recentMessages = await req.payload.
|
|
2995
|
-
collection: slugs.chatMessages,
|
|
3186
|
+
const recentMessages = await dbFind(req.payload, slugs.chatMessages, {
|
|
2996
3187
|
sort: "-createdAt",
|
|
2997
3188
|
limit: 50,
|
|
2998
3189
|
depth: 1,
|
|
@@ -3079,8 +3270,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3079
3270
|
const statuses = ["open", "waiting_client", "resolved"];
|
|
3080
3271
|
const statusCounts = await Promise.all(
|
|
3081
3272
|
statuses.map(async (status) => {
|
|
3082
|
-
const result = await payload.
|
|
3083
|
-
collection: slugs.tickets,
|
|
3273
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3084
3274
|
where: { status: { equals: status } },
|
|
3085
3275
|
overrideAccess: true
|
|
3086
3276
|
});
|
|
@@ -3092,8 +3282,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3092
3282
|
const priorities = ["low", "normal", "high", "urgent"];
|
|
3093
3283
|
const priorityCounts = await Promise.all(
|
|
3094
3284
|
priorities.map(async (priority) => {
|
|
3095
|
-
const result = await payload.
|
|
3096
|
-
collection: slugs.tickets,
|
|
3285
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3097
3286
|
where: { priority: { equals: priority } },
|
|
3098
3287
|
overrideAccess: true
|
|
3099
3288
|
});
|
|
@@ -3104,8 +3293,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3104
3293
|
const categories = ["bug", "content", "feature", "question", "hosting"];
|
|
3105
3294
|
const categoryCounts = await Promise.all(
|
|
3106
3295
|
categories.map(async (category) => {
|
|
3107
|
-
const result = await payload.
|
|
3108
|
-
collection: slugs.tickets,
|
|
3296
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
3109
3297
|
where: { category: { equals: category } },
|
|
3110
3298
|
overrideAccess: true
|
|
3111
3299
|
});
|
|
@@ -3119,17 +3307,34 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3119
3307
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
|
|
3120
3308
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
3121
3309
|
const [created7, created30] = await Promise.all([
|
|
3122
|
-
payload.
|
|
3123
|
-
collection: slugs.tickets,
|
|
3310
|
+
dbCount(payload, slugs.tickets, {
|
|
3124
3311
|
where: { createdAt: { greater_than_equal: sevenDaysAgo.toISOString() } },
|
|
3125
3312
|
overrideAccess: true
|
|
3126
3313
|
}),
|
|
3127
|
-
payload.
|
|
3128
|
-
collection: slugs.tickets,
|
|
3314
|
+
dbCount(payload, slugs.tickets, {
|
|
3129
3315
|
where: { createdAt: { greater_than_equal: thirtyDaysAgo.toISOString() } },
|
|
3130
3316
|
overrideAccess: true
|
|
3131
3317
|
})
|
|
3132
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
|
+
);
|
|
3133
3338
|
const PAGE_SIZE2 = 100;
|
|
3134
3339
|
const MAX_PAGES2 = 50;
|
|
3135
3340
|
let totalResponseTimeMs = 0;
|
|
@@ -3140,8 +3345,7 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3140
3345
|
let page = 1;
|
|
3141
3346
|
let hasMore = true;
|
|
3142
3347
|
while (hasMore && page <= MAX_PAGES2) {
|
|
3143
|
-
const batch = await payload.
|
|
3144
|
-
collection: slugs.tickets,
|
|
3348
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3145
3349
|
limit: PAGE_SIZE2,
|
|
3146
3350
|
page,
|
|
3147
3351
|
depth: 0,
|
|
@@ -3174,43 +3378,62 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3174
3378
|
hasMore = batch.hasNextPage ?? false;
|
|
3175
3379
|
page++;
|
|
3176
3380
|
}
|
|
3177
|
-
const surveyCount = await payload.
|
|
3178
|
-
collection: slugs.satisfactionSurveys,
|
|
3381
|
+
const surveyCount = await dbCount(payload, slugs.satisfactionSurveys, {
|
|
3179
3382
|
overrideAccess: true
|
|
3180
3383
|
});
|
|
3181
3384
|
let satisfactionAvg = 0;
|
|
3385
|
+
let satisfactionCount = 0;
|
|
3386
|
+
let npsScore = null;
|
|
3387
|
+
let npsCount = 0;
|
|
3182
3388
|
if (surveyCount.totalDocs > 0) {
|
|
3183
|
-
let
|
|
3389
|
+
let ratingSum = 0;
|
|
3390
|
+
let ratingN = 0;
|
|
3391
|
+
let promoters = 0;
|
|
3392
|
+
let detractors = 0;
|
|
3393
|
+
let npsN = 0;
|
|
3184
3394
|
let surveyPage = 1;
|
|
3185
3395
|
let surveyHasMore = true;
|
|
3186
3396
|
while (surveyHasMore && surveyPage <= MAX_PAGES2) {
|
|
3187
|
-
const batch = await payload.
|
|
3188
|
-
collection: slugs.satisfactionSurveys,
|
|
3397
|
+
const batch = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
3189
3398
|
limit: PAGE_SIZE2,
|
|
3190
3399
|
page: surveyPage,
|
|
3191
3400
|
depth: 0,
|
|
3192
3401
|
overrideAccess: true,
|
|
3193
|
-
select: { rating: true }
|
|
3402
|
+
select: { rating: true, nps: true }
|
|
3194
3403
|
});
|
|
3195
3404
|
for (const s of batch.docs) {
|
|
3196
|
-
|
|
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
|
+
}
|
|
3197
3416
|
}
|
|
3198
3417
|
surveyHasMore = batch.hasNextPage ?? false;
|
|
3199
3418
|
surveyPage++;
|
|
3200
3419
|
}
|
|
3201
|
-
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
|
+
}
|
|
3202
3426
|
}
|
|
3203
|
-
const
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
}),
|
|
3208
|
-
payload.count({
|
|
3209
|
-
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, {
|
|
3210
3431
|
where: { status: { equals: "pending" } },
|
|
3211
3432
|
overrideAccess: true
|
|
3212
|
-
})
|
|
3213
|
-
|
|
3433
|
+
});
|
|
3434
|
+
pendingEmailsTotal = pe.totalDocs;
|
|
3435
|
+
} catch {
|
|
3436
|
+
}
|
|
3214
3437
|
return new Response(JSON.stringify({
|
|
3215
3438
|
total,
|
|
3216
3439
|
byStatus,
|
|
@@ -3218,13 +3441,16 @@ function createAdminStatsEndpoint(slugs) {
|
|
|
3218
3441
|
byCategory,
|
|
3219
3442
|
createdLast7Days: created7.totalDocs,
|
|
3220
3443
|
createdLast30Days: created30.totalDocs,
|
|
3444
|
+
volumeByDay,
|
|
3221
3445
|
avgResponseTimeHours: responseTimeCount > 0 ? Math.round(totalResponseTimeMs / responseTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3222
3446
|
avgResolutionTimeHours: resolutionTimeCount > 0 ? Math.round(totalResolutionTimeMs / resolutionTimeCount / (1e3 * 60 * 60) * 10) / 10 : null,
|
|
3223
3447
|
totalTimeMinutes,
|
|
3224
3448
|
satisfactionAvg,
|
|
3225
|
-
satisfactionCount
|
|
3449
|
+
satisfactionCount,
|
|
3450
|
+
npsScore,
|
|
3451
|
+
npsCount,
|
|
3226
3452
|
clientCount: clientCount.totalDocs,
|
|
3227
|
-
pendingEmailsCount:
|
|
3453
|
+
pendingEmailsCount: pendingEmailsTotal
|
|
3228
3454
|
}), {
|
|
3229
3455
|
headers: {
|
|
3230
3456
|
"Content-Type": "application/json",
|
|
@@ -3294,13 +3520,26 @@ function createBillingEndpoint(slugs) {
|
|
|
3294
3520
|
let ticketPage = 1;
|
|
3295
3521
|
let ticketHasMore = true;
|
|
3296
3522
|
while (ticketHasMore && ticketPage <= MAX_PAGES) {
|
|
3297
|
-
const batch = await payload.
|
|
3298
|
-
collection: slugs.tickets,
|
|
3523
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3299
3524
|
where: ticketWhere,
|
|
3300
3525
|
limit: PAGE_SIZE,
|
|
3301
3526
|
page: ticketPage,
|
|
3527
|
+
// depth 2 is required: we read ticket.project.client.company below.
|
|
3302
3528
|
depth: 2,
|
|
3303
|
-
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
|
+
}
|
|
3304
3543
|
});
|
|
3305
3544
|
allTickets.push(...batch.docs);
|
|
3306
3545
|
ticketHasMore = batch.hasNextPage ?? false;
|
|
@@ -3310,8 +3549,7 @@ function createBillingEndpoint(slugs) {
|
|
|
3310
3549
|
let entryPage = 1;
|
|
3311
3550
|
let entryHasMore = true;
|
|
3312
3551
|
while (entryHasMore && entryPage <= MAX_PAGES) {
|
|
3313
|
-
const batch = await payload.
|
|
3314
|
-
collection: slugs.timeEntries,
|
|
3552
|
+
const batch = await dbFind(payload, slugs.timeEntries, {
|
|
3315
3553
|
where: {
|
|
3316
3554
|
and: [
|
|
3317
3555
|
{ date: { greater_than_equal: from } },
|
|
@@ -3409,43 +3647,205 @@ function createBillingEndpoint(slugs) {
|
|
|
3409
3647
|
}
|
|
3410
3648
|
};
|
|
3411
3649
|
}
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
function
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
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
|
+
}
|
|
3436
3703
|
});
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
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
|
+
}
|
|
3815
|
+
|
|
3816
|
+
// src/utils/generateTicketSynthesis.ts
|
|
3817
|
+
function getClient4(aiSettings) {
|
|
3818
|
+
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
3819
|
+
if (aiSettings.provider === "ollama") {
|
|
3820
|
+
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
3821
|
+
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
3822
|
+
}
|
|
3823
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
3824
|
+
}
|
|
3825
|
+
function getModel3(aiSettings) {
|
|
3826
|
+
return aiSettings.model || "claude-haiku-4-5-20251001";
|
|
3827
|
+
}
|
|
3828
|
+
async function generateTicketSynthesis(args) {
|
|
3829
|
+
const { payload, slugs, ticketId } = args;
|
|
3830
|
+
const settings = await readSupportSettings(payload);
|
|
3831
|
+
if (settings.ai.enableSynthesis === false) {
|
|
3832
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "skipped", reason: "synthesis disabled" };
|
|
3833
|
+
}
|
|
3834
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
3835
|
+
id: ticketId,
|
|
3836
|
+
depth: 1,
|
|
3837
|
+
overrideAccess: true
|
|
3838
|
+
});
|
|
3839
|
+
if (!ticket) {
|
|
3840
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "error", reason: "ticket not found" };
|
|
3841
|
+
}
|
|
3842
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3843
|
+
id: ticketId,
|
|
3844
|
+
data: { aiSummaryStatus: "pending" },
|
|
3444
3845
|
overrideAccess: true
|
|
3445
3846
|
}).catch(() => {
|
|
3446
3847
|
});
|
|
3447
|
-
const messagesResult = await payload.
|
|
3448
|
-
collection: slugs.ticketMessages,
|
|
3848
|
+
const messagesResult = await dbFind(payload, slugs.ticketMessages, {
|
|
3449
3849
|
where: { ticket: { equals: ticketId } },
|
|
3450
3850
|
sort: "createdAt",
|
|
3451
3851
|
limit: 500,
|
|
@@ -3455,8 +3855,7 @@ async function generateTicketSynthesis(args) {
|
|
|
3455
3855
|
const messages = messagesResult.docs;
|
|
3456
3856
|
if (messages.length === 0) {
|
|
3457
3857
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3458
|
-
await payload.
|
|
3459
|
-
collection: slugs.tickets,
|
|
3858
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3460
3859
|
id: ticketId,
|
|
3461
3860
|
data: {
|
|
3462
3861
|
aiSummary: "(Aucun message dans ce ticket)",
|
|
@@ -3501,7 +3900,7 @@ Regles strictes :
|
|
|
3501
3900
|
- Si le ticket n'a pas abouti, decris quand meme le travail d'analyse realise
|
|
3502
3901
|
|
|
3503
3902
|
Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
3504
|
-
const anthropic =
|
|
3903
|
+
const anthropic = getClient4(settings.ai);
|
|
3505
3904
|
const model = getModel3(settings.ai);
|
|
3506
3905
|
try {
|
|
3507
3906
|
const res = await anthropic.messages.create({
|
|
@@ -3511,8 +3910,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3511
3910
|
});
|
|
3512
3911
|
const summary = res.content[0]?.type === "text" ? res.content[0].text.trim() : "";
|
|
3513
3912
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3514
|
-
await payload.
|
|
3515
|
-
collection: slugs.tickets,
|
|
3913
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3516
3914
|
id: ticketId,
|
|
3517
3915
|
data: {
|
|
3518
3916
|
aiSummary: summary,
|
|
@@ -3524,8 +3922,7 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3524
3922
|
return { summary, generatedAt, status: "done" };
|
|
3525
3923
|
} catch (err) {
|
|
3526
3924
|
const message = err instanceof Error ? err.message : String(err);
|
|
3527
|
-
await payload.
|
|
3528
|
-
collection: slugs.tickets,
|
|
3925
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
3529
3926
|
id: ticketId,
|
|
3530
3927
|
data: { aiSummaryStatus: "error" },
|
|
3531
3928
|
overrideAccess: true
|
|
@@ -3555,8 +3952,7 @@ function createTicketSynthesisEndpoint(slugs) {
|
|
|
3555
3952
|
return Response.json({ error: "ticketId must be a number" }, { status: 400 });
|
|
3556
3953
|
}
|
|
3557
3954
|
if (!force) {
|
|
3558
|
-
const existing = await payload.
|
|
3559
|
-
collection: slugs.tickets,
|
|
3955
|
+
const existing = await dbFindByID(payload, slugs.tickets, {
|
|
3560
3956
|
id: ticketId,
|
|
3561
3957
|
depth: 0,
|
|
3562
3958
|
overrideAccess: true
|
|
@@ -3600,8 +3996,7 @@ function createEmailStatsEndpoint(slugs) {
|
|
|
3600
3996
|
let page = 1;
|
|
3601
3997
|
let hasMore = true;
|
|
3602
3998
|
while (hasMore && page <= MAX_PAGES2) {
|
|
3603
|
-
const result = await payload.
|
|
3604
|
-
collection: slugs.emailLogs,
|
|
3999
|
+
const result = await dbFind(payload, slugs.emailLogs, {
|
|
3605
4000
|
where: { createdAt: { greater_than: cutoff } },
|
|
3606
4001
|
sort: "-createdAt",
|
|
3607
4002
|
limit: 500,
|
|
@@ -3675,12 +4070,20 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3675
4070
|
const payload = req.payload;
|
|
3676
4071
|
requireClient(req, slugs);
|
|
3677
4072
|
const body = await req.json();
|
|
3678
|
-
const { ticketId, rating, comment } = body;
|
|
3679
|
-
if (!ticketId
|
|
3680
|
-
return Response.json(
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
4073
|
+
const { ticketId, rating, nps, comment } = body;
|
|
4074
|
+
if (!ticketId) {
|
|
4075
|
+
return Response.json({ error: "ticketId est requis." }, { status: 400 });
|
|
4076
|
+
}
|
|
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 });
|
|
4081
|
+
}
|
|
4082
|
+
if (hasRating && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
4083
|
+
return Response.json({ error: "rating doit \xEAtre un entier 1-5." }, { status: 400 });
|
|
4084
|
+
}
|
|
4085
|
+
if (hasNps && (!Number.isInteger(nps) || nps < 0 || nps > 10)) {
|
|
4086
|
+
return Response.json({ error: "nps doit \xEAtre un entier 0-10." }, { status: 400 });
|
|
3684
4087
|
}
|
|
3685
4088
|
if (comment && typeof comment === "string" && comment.length > 5e3) {
|
|
3686
4089
|
return Response.json(
|
|
@@ -3688,8 +4091,7 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3688
4091
|
{ status: 400 }
|
|
3689
4092
|
);
|
|
3690
4093
|
}
|
|
3691
|
-
const ticket = await payload.
|
|
3692
|
-
collection: slugs.tickets,
|
|
4094
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
3693
4095
|
id: ticketId,
|
|
3694
4096
|
depth: 0,
|
|
3695
4097
|
overrideAccess: false,
|
|
@@ -3704,8 +4106,7 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3704
4106
|
{ status: 400 }
|
|
3705
4107
|
);
|
|
3706
4108
|
}
|
|
3707
|
-
const existing = await payload.
|
|
3708
|
-
collection: slugs.satisfactionSurveys,
|
|
4109
|
+
const existing = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
3709
4110
|
where: { ticket: { equals: ticketId } },
|
|
3710
4111
|
limit: 1,
|
|
3711
4112
|
depth: 0,
|
|
@@ -3717,13 +4118,13 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3717
4118
|
{ status: 409 }
|
|
3718
4119
|
);
|
|
3719
4120
|
}
|
|
3720
|
-
const survey = await payload.
|
|
3721
|
-
collection: slugs.satisfactionSurveys,
|
|
4121
|
+
const survey = await dbCreate(payload, slugs.satisfactionSurveys, {
|
|
3722
4122
|
data: {
|
|
3723
4123
|
source: "ticket",
|
|
3724
4124
|
ticket: ticketId,
|
|
3725
4125
|
client: req.user.id,
|
|
3726
|
-
rating: Math.round(rating),
|
|
4126
|
+
...hasRating ? { rating: Math.round(rating) } : {},
|
|
4127
|
+
...hasNps ? { nps: Math.round(nps) } : {},
|
|
3727
4128
|
...comment ? { comment: comment.trim() } : {}
|
|
3728
4129
|
},
|
|
3729
4130
|
overrideAccess: true
|
|
@@ -3738,124 +4139,6 @@ function createSatisfactionEndpoint(slugs) {
|
|
|
3738
4139
|
}
|
|
3739
4140
|
};
|
|
3740
4141
|
}
|
|
3741
|
-
var TRANSPARENT_GIF = Buffer.from(
|
|
3742
|
-
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
3743
|
-
"base64"
|
|
3744
|
-
);
|
|
3745
|
-
function generateTrackingToken(ticketId, messageId, secret) {
|
|
3746
|
-
return createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex").substring(0, 16);
|
|
3747
|
-
}
|
|
3748
|
-
function createTrackOpenEndpoint(slugs) {
|
|
3749
|
-
return {
|
|
3750
|
-
path: "/support/track-open",
|
|
3751
|
-
method: "get",
|
|
3752
|
-
handler: async (req) => {
|
|
3753
|
-
const url = new URL(req.url);
|
|
3754
|
-
const ticketId = url.searchParams.get("t");
|
|
3755
|
-
const messageId = url.searchParams.get("m");
|
|
3756
|
-
const sig = url.searchParams.get("sig");
|
|
3757
|
-
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
3758
|
-
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
3759
|
-
const secret = process.env.PAYLOAD_SECRET || "";
|
|
3760
|
-
if (secret && ticketId && messageId && sig) {
|
|
3761
|
-
const expected = generateTrackingToken(ticketId, messageId, secret);
|
|
3762
|
-
if (sig !== expected) {
|
|
3763
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3764
|
-
status: 200,
|
|
3765
|
-
headers: {
|
|
3766
|
-
"Content-Type": "image/gif",
|
|
3767
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3768
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3769
|
-
}
|
|
3770
|
-
});
|
|
3771
|
-
}
|
|
3772
|
-
} else if (secret && (!sig || !ticketId || !messageId)) {
|
|
3773
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3774
|
-
status: 200,
|
|
3775
|
-
headers: {
|
|
3776
|
-
"Content-Type": "image/gif",
|
|
3777
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3778
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
|
3779
|
-
}
|
|
3780
|
-
});
|
|
3781
|
-
}
|
|
3782
|
-
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
3783
|
-
try {
|
|
3784
|
-
const payload = req.payload;
|
|
3785
|
-
const ticket = await payload.findByID({
|
|
3786
|
-
collection: slugs.tickets,
|
|
3787
|
-
id: parsedId,
|
|
3788
|
-
depth: 0,
|
|
3789
|
-
overrideAccess: true,
|
|
3790
|
-
select: { lastClientReadAt: true }
|
|
3791
|
-
});
|
|
3792
|
-
if (ticket) {
|
|
3793
|
-
const lastRead = ticket.lastClientReadAt ? new Date(ticket.lastClientReadAt).getTime() : 0;
|
|
3794
|
-
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
|
|
3795
|
-
if (lastRead < fiveMinAgo) {
|
|
3796
|
-
await payload.update({
|
|
3797
|
-
collection: slugs.tickets,
|
|
3798
|
-
id: parsedId,
|
|
3799
|
-
data: { lastClientReadAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3800
|
-
overrideAccess: true
|
|
3801
|
-
});
|
|
3802
|
-
}
|
|
3803
|
-
}
|
|
3804
|
-
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
3805
|
-
const msg = await payload.findByID({
|
|
3806
|
-
collection: slugs.ticketMessages,
|
|
3807
|
-
id: parsedMsgId,
|
|
3808
|
-
depth: 0,
|
|
3809
|
-
overrideAccess: true,
|
|
3810
|
-
select: { emailOpenedAt: true }
|
|
3811
|
-
});
|
|
3812
|
-
if (msg && !msg.emailOpenedAt) {
|
|
3813
|
-
await payload.update({
|
|
3814
|
-
collection: slugs.ticketMessages,
|
|
3815
|
-
id: parsedMsgId,
|
|
3816
|
-
data: { emailOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3817
|
-
overrideAccess: true
|
|
3818
|
-
});
|
|
3819
|
-
const ticketInfo = await payload.findByID({
|
|
3820
|
-
collection: slugs.tickets,
|
|
3821
|
-
id: parsedId,
|
|
3822
|
-
depth: 1,
|
|
3823
|
-
overrideAccess: true,
|
|
3824
|
-
select: { ticketNumber: true, subject: true, client: true }
|
|
3825
|
-
});
|
|
3826
|
-
const clientName = typeof ticketInfo?.client === "object" ? ticketInfo.client?.firstName || "Client" : "Client";
|
|
3827
|
-
try {
|
|
3828
|
-
await payload.create({
|
|
3829
|
-
collection: "admin-notifications",
|
|
3830
|
-
data: {
|
|
3831
|
-
title: `Email ouvert \u2014 ${ticketInfo?.ticketNumber || "TK-????"}`,
|
|
3832
|
-
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || "ticket"}"`,
|
|
3833
|
-
type: "email_opened",
|
|
3834
|
-
link: `/admin/ticket?id=${parsedId}`
|
|
3835
|
-
},
|
|
3836
|
-
overrideAccess: true
|
|
3837
|
-
});
|
|
3838
|
-
} catch {
|
|
3839
|
-
}
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
} catch (err) {
|
|
3843
|
-
console.error("[track-open] Error:", err);
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
return new Response(TRANSPARENT_GIF, {
|
|
3847
|
-
status: 200,
|
|
3848
|
-
headers: {
|
|
3849
|
-
"Content-Type": "image/gif",
|
|
3850
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
3851
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
3852
|
-
"Pragma": "no-cache",
|
|
3853
|
-
"Expires": "0"
|
|
3854
|
-
}
|
|
3855
|
-
});
|
|
3856
|
-
}
|
|
3857
|
-
};
|
|
3858
|
-
}
|
|
3859
4142
|
|
|
3860
4143
|
// src/endpoints/export-csv.ts
|
|
3861
4144
|
function createExportCsvEndpoint(slugs) {
|
|
@@ -3871,8 +4154,7 @@ function createExportCsvEndpoint(slugs) {
|
|
|
3871
4154
|
let hasMore = true;
|
|
3872
4155
|
const allDocs = [];
|
|
3873
4156
|
while (hasMore) {
|
|
3874
|
-
const batch = await payload.
|
|
3875
|
-
collection: slugs.tickets,
|
|
4157
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
3876
4158
|
limit: PAGE_SIZE2,
|
|
3877
4159
|
page,
|
|
3878
4160
|
depth: 1,
|
|
@@ -3948,21 +4230,18 @@ function createExportDataEndpoint(slugs) {
|
|
|
3948
4230
|
const payload = req.payload;
|
|
3949
4231
|
requireClient(req, slugs);
|
|
3950
4232
|
const [clientData, ticketsResult, messagesResult, surveysResult] = await Promise.all([
|
|
3951
|
-
payload.
|
|
3952
|
-
collection: slugs.supportClients,
|
|
4233
|
+
dbFindByID(payload, slugs.supportClients, {
|
|
3953
4234
|
id: req.user.id,
|
|
3954
4235
|
depth: 0,
|
|
3955
4236
|
overrideAccess: true
|
|
3956
4237
|
}),
|
|
3957
|
-
payload.
|
|
3958
|
-
collection: slugs.tickets,
|
|
4238
|
+
dbFind(payload, slugs.tickets, {
|
|
3959
4239
|
where: { client: { equals: req.user.id } },
|
|
3960
4240
|
limit: 1e3,
|
|
3961
4241
|
depth: 0,
|
|
3962
4242
|
overrideAccess: true
|
|
3963
4243
|
}),
|
|
3964
|
-
payload.
|
|
3965
|
-
collection: slugs.ticketMessages,
|
|
4244
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
3966
4245
|
where: {
|
|
3967
4246
|
"ticket.client": { equals: req.user.id },
|
|
3968
4247
|
authorType: { equals: "client" }
|
|
@@ -3971,8 +4250,7 @@ function createExportDataEndpoint(slugs) {
|
|
|
3971
4250
|
depth: 0,
|
|
3972
4251
|
overrideAccess: true
|
|
3973
4252
|
}),
|
|
3974
|
-
payload.
|
|
3975
|
-
collection: slugs.satisfactionSurveys,
|
|
4253
|
+
dbFind(payload, slugs.satisfactionSurveys, {
|
|
3976
4254
|
where: { client: { equals: req.user.id } },
|
|
3977
4255
|
limit: 500,
|
|
3978
4256
|
depth: 0,
|
|
@@ -4046,8 +4324,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4046
4324
|
if (!["create_ticket", "add_to_ticket", "ignore"].includes(action)) {
|
|
4047
4325
|
return Response.json({ error: "Invalid action" }, { status: 400 });
|
|
4048
4326
|
}
|
|
4049
|
-
const pendingEmail = await payload.
|
|
4050
|
-
collection: slugs.pendingEmails,
|
|
4327
|
+
const pendingEmail = await dbFindByID(payload, slugs.pendingEmails, {
|
|
4051
4328
|
id: Number(id),
|
|
4052
4329
|
depth: 1,
|
|
4053
4330
|
overrideAccess: true
|
|
@@ -4061,14 +4338,12 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4061
4338
|
let clientId = overrideClientId || (typeof pendingEmail.client === "object" ? pendingEmail.client?.id : pendingEmail.client);
|
|
4062
4339
|
let clientDoc = typeof pendingEmail.client === "object" ? pendingEmail.client : null;
|
|
4063
4340
|
if (overrideClientId && overrideClientId !== clientDoc?.id) {
|
|
4064
|
-
clientDoc = await payload.
|
|
4065
|
-
collection: slugs.supportClients,
|
|
4341
|
+
clientDoc = await dbFindByID(payload, slugs.supportClients, {
|
|
4066
4342
|
id: overrideClientId,
|
|
4067
4343
|
depth: 0,
|
|
4068
4344
|
overrideAccess: true
|
|
4069
4345
|
});
|
|
4070
|
-
await payload.
|
|
4071
|
-
collection: slugs.pendingEmails,
|
|
4346
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4072
4347
|
id: Number(id),
|
|
4073
4348
|
data: { client: overrideClientId },
|
|
4074
4349
|
overrideAccess: true
|
|
@@ -4085,8 +4360,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4085
4360
|
file: typeof a.file === "object" ? a.file.id : a.file
|
|
4086
4361
|
})) || [];
|
|
4087
4362
|
if (action === "ignore") {
|
|
4088
|
-
await payload.
|
|
4089
|
-
collection: slugs.pendingEmails,
|
|
4363
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4090
4364
|
id: Number(id),
|
|
4091
4365
|
data: {
|
|
4092
4366
|
status: "ignored",
|
|
@@ -4098,8 +4372,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4098
4372
|
return Response.json({ action: "ignored", pendingEmailId: Number(id) });
|
|
4099
4373
|
}
|
|
4100
4374
|
if (action === "create_ticket") {
|
|
4101
|
-
const newTicket = await payload.
|
|
4102
|
-
collection: slugs.tickets,
|
|
4375
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
4103
4376
|
data: {
|
|
4104
4377
|
subject: pendingEmail.subject,
|
|
4105
4378
|
client: clientId,
|
|
@@ -4110,8 +4383,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4110
4383
|
},
|
|
4111
4384
|
overrideAccess: true
|
|
4112
4385
|
});
|
|
4113
|
-
await payload.
|
|
4114
|
-
collection: slugs.ticketMessages,
|
|
4386
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
4115
4387
|
data: {
|
|
4116
4388
|
ticket: newTicket.id,
|
|
4117
4389
|
body: pendingEmail.body,
|
|
@@ -4138,8 +4410,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4138
4410
|
} catch (err) {
|
|
4139
4411
|
console.error("[pending-email-process] Failed to send notification:", err);
|
|
4140
4412
|
}
|
|
4141
|
-
await payload.
|
|
4142
|
-
collection: slugs.pendingEmails,
|
|
4413
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4143
4414
|
id: Number(id),
|
|
4144
4415
|
data: {
|
|
4145
4416
|
status: "processed",
|
|
@@ -4159,8 +4430,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4159
4430
|
if (!ticketId) {
|
|
4160
4431
|
return Response.json({ error: "ticketId is required for add_to_ticket" }, { status: 400 });
|
|
4161
4432
|
}
|
|
4162
|
-
const targetTicket = await payload.
|
|
4163
|
-
collection: slugs.tickets,
|
|
4433
|
+
const targetTicket = await dbFindByID(payload, slugs.tickets, {
|
|
4164
4434
|
id: ticketId,
|
|
4165
4435
|
depth: 0,
|
|
4166
4436
|
overrideAccess: true
|
|
@@ -4168,8 +4438,7 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4168
4438
|
if (!targetTicket) {
|
|
4169
4439
|
return Response.json({ error: "Target ticket not found" }, { status: 404 });
|
|
4170
4440
|
}
|
|
4171
|
-
await payload.
|
|
4172
|
-
collection: slugs.ticketMessages,
|
|
4441
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
4173
4442
|
data: {
|
|
4174
4443
|
ticket: ticketId,
|
|
4175
4444
|
body: pendingEmail.body,
|
|
@@ -4181,15 +4450,13 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4181
4450
|
overrideAccess: true
|
|
4182
4451
|
});
|
|
4183
4452
|
if (targetTicket.status === "resolved") {
|
|
4184
|
-
await payload.
|
|
4185
|
-
collection: slugs.tickets,
|
|
4453
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
4186
4454
|
id: ticketId,
|
|
4187
4455
|
data: { status: "open" },
|
|
4188
4456
|
overrideAccess: true
|
|
4189
4457
|
});
|
|
4190
4458
|
}
|
|
4191
|
-
await payload.
|
|
4192
|
-
collection: slugs.pendingEmails,
|
|
4459
|
+
await dbUpdate(payload, slugs.pendingEmails, {
|
|
4193
4460
|
id: Number(id),
|
|
4194
4461
|
data: {
|
|
4195
4462
|
status: "processed",
|
|
@@ -4242,8 +4509,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4242
4509
|
if (!messageId) {
|
|
4243
4510
|
return Response.json({ error: "messageId requis" }, { status: 400 });
|
|
4244
4511
|
}
|
|
4245
|
-
const message = await payload.
|
|
4246
|
-
collection: slugs.ticketMessages,
|
|
4512
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
4247
4513
|
id: messageId,
|
|
4248
4514
|
depth: 0,
|
|
4249
4515
|
overrideAccess: true
|
|
@@ -4252,8 +4518,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4252
4518
|
return Response.json({ error: "Message introuvable" }, { status: 404 });
|
|
4253
4519
|
}
|
|
4254
4520
|
const ticketId = typeof message.ticket === "object" ? message.ticket.id : message.ticket;
|
|
4255
|
-
const ticket = await payload.
|
|
4256
|
-
collection: slugs.tickets,
|
|
4521
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
4257
4522
|
id: ticketId,
|
|
4258
4523
|
depth: 1,
|
|
4259
4524
|
overrideAccess: true
|
|
@@ -4379,8 +4644,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4379
4644
|
let created = 0;
|
|
4380
4645
|
let skipped = 0;
|
|
4381
4646
|
for (const article of KB_ARTICLES) {
|
|
4382
|
-
const existing = await payload.
|
|
4383
|
-
collection: slugs.knowledgeBase,
|
|
4647
|
+
const existing = await dbFind(payload, slugs.knowledgeBase, {
|
|
4384
4648
|
where: { slug: { equals: article.slug } },
|
|
4385
4649
|
limit: 1,
|
|
4386
4650
|
depth: 0,
|
|
@@ -4407,8 +4671,7 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4407
4671
|
version: 1
|
|
4408
4672
|
}
|
|
4409
4673
|
};
|
|
4410
|
-
await payload.
|
|
4411
|
-
collection: slugs.knowledgeBase,
|
|
4674
|
+
await dbCreate(payload, slugs.knowledgeBase, {
|
|
4412
4675
|
data: {
|
|
4413
4676
|
title: article.title,
|
|
4414
4677
|
slug: article.slug,
|
|
@@ -4463,8 +4726,7 @@ function createLoginEndpoint(slugs) {
|
|
|
4463
4726
|
collection: slugs.supportClients,
|
|
4464
4727
|
data: { email, password }
|
|
4465
4728
|
});
|
|
4466
|
-
payload.
|
|
4467
|
-
collection: slugs.authLogs,
|
|
4729
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4468
4730
|
data: { email, success: true, action: "login", ipAddress: ip, userAgent },
|
|
4469
4731
|
overrideAccess: true
|
|
4470
4732
|
}).catch(() => {
|
|
@@ -4488,12 +4750,14 @@ function createLoginEndpoint(slugs) {
|
|
|
4488
4750
|
);
|
|
4489
4751
|
} catch (err) {
|
|
4490
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
|
+
}
|
|
4491
4756
|
let errorReason = "Identifiants incorrects";
|
|
4492
4757
|
if (errorMessage.includes("locked") || errorMessage.includes("verrouill\xE9") || errorMessage.includes("Too many")) {
|
|
4493
4758
|
errorReason = "Compte verrouill\xE9 (trop de tentatives)";
|
|
4494
4759
|
}
|
|
4495
|
-
payload.
|
|
4496
|
-
collection: slugs.authLogs,
|
|
4760
|
+
dbCreate(payload, slugs.authLogs, {
|
|
4497
4761
|
data: { email, success: false, action: "login", errorReason, ipAddress: ip, userAgent },
|
|
4498
4762
|
overrideAccess: true
|
|
4499
4763
|
}).catch(() => {
|
|
@@ -4514,7 +4778,10 @@ function generateSecureCode() {
|
|
|
4514
4778
|
return String(num);
|
|
4515
4779
|
}
|
|
4516
4780
|
function hashCode(code) {
|
|
4517
|
-
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
|
+
}
|
|
4518
4785
|
return createHmac("sha256", secret).update(code).digest("hex");
|
|
4519
4786
|
}
|
|
4520
4787
|
function createAuth2faEndpoint(slugs) {
|
|
@@ -4539,8 +4806,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4539
4806
|
if (sendLimiter.check(email)) {
|
|
4540
4807
|
return Response.json(genericSendResponse);
|
|
4541
4808
|
}
|
|
4542
|
-
const clients = await payload.
|
|
4543
|
-
collection: slugs.supportClients,
|
|
4809
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4544
4810
|
where: { email: { equals: email } },
|
|
4545
4811
|
limit: 1,
|
|
4546
4812
|
depth: 0,
|
|
@@ -4553,8 +4819,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4553
4819
|
const plainCode = generateSecureCode();
|
|
4554
4820
|
const twoFactorCode = hashCode(plainCode);
|
|
4555
4821
|
const twoFactorExpiry = new Date(Date.now() + 10 * 60 * 1e3).toISOString();
|
|
4556
|
-
await payload.
|
|
4557
|
-
collection: slugs.supportClients,
|
|
4822
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4558
4823
|
id: client.id,
|
|
4559
4824
|
data: { twoFactorCode, twoFactorExpiry },
|
|
4560
4825
|
overrideAccess: true
|
|
@@ -4585,8 +4850,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4585
4850
|
{ status: 429 }
|
|
4586
4851
|
);
|
|
4587
4852
|
}
|
|
4588
|
-
const clients = await payload.
|
|
4589
|
-
collection: slugs.supportClients,
|
|
4853
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
4590
4854
|
where: { email: { equals: email } },
|
|
4591
4855
|
limit: 1,
|
|
4592
4856
|
depth: 0,
|
|
@@ -4602,8 +4866,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4602
4866
|
return Response.json({ error: "Aucun code en attente" }, { status: 400 });
|
|
4603
4867
|
}
|
|
4604
4868
|
if (/* @__PURE__ */ new Date() > new Date(storedExpiry)) {
|
|
4605
|
-
await payload.
|
|
4606
|
-
collection: slugs.supportClients,
|
|
4869
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4607
4870
|
id: client.id,
|
|
4608
4871
|
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4609
4872
|
overrideAccess: true
|
|
@@ -4617,10 +4880,9 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4617
4880
|
if (submittedBuffer.length !== storedBuffer.length || !crypto3.timingSafeEqual(submittedBuffer, storedBuffer)) {
|
|
4618
4881
|
return Response.json({ error: "Code incorrect" }, { status: 400 });
|
|
4619
4882
|
}
|
|
4620
|
-
await payload.
|
|
4621
|
-
collection: slugs.supportClients,
|
|
4883
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
4622
4884
|
id: client.id,
|
|
4623
|
-
data: { twoFactorCode: "", twoFactorExpiry: "" },
|
|
4885
|
+
data: { twoFactorCode: "", twoFactorExpiry: "", twoFactorVerifiedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4624
4886
|
overrideAccess: true
|
|
4625
4887
|
});
|
|
4626
4888
|
verifyLimiter.reset(email);
|
|
@@ -4694,15 +4956,33 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4694
4956
|
if (!profile.email) {
|
|
4695
4957
|
return Response.json({ error: "no_email" }, { status: 400 });
|
|
4696
4958
|
}
|
|
4959
|
+
if (profile.verified_email !== true) {
|
|
4960
|
+
return Response.json({ error: "email_not_verified" }, { status: 403 });
|
|
4961
|
+
}
|
|
4697
4962
|
const payload = req.payload;
|
|
4698
|
-
const
|
|
4699
|
-
|
|
4700
|
-
where: { email: { equals: profile.email } },
|
|
4963
|
+
const byGoogle = await dbFind(payload, slugs.supportClients, {
|
|
4964
|
+
where: { googleId: { equals: profile.id } },
|
|
4701
4965
|
limit: 1,
|
|
4702
4966
|
depth: 0,
|
|
4703
4967
|
overrideAccess: true
|
|
4704
4968
|
});
|
|
4705
|
-
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
|
+
}
|
|
4706
4986
|
if (!clientDoc) {
|
|
4707
4987
|
const allowedDomains = options?.allowedEmailDomains;
|
|
4708
4988
|
if (allowedDomains && allowedDomains.length > 0) {
|
|
@@ -4720,43 +5000,64 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
4720
5000
|
const autoPassword = crypto3.randomBytes(48).toString("base64url");
|
|
4721
5001
|
const fullName = profile.name || profile.email.split("@")[0];
|
|
4722
5002
|
const nameParts = fullName.split(" ");
|
|
4723
|
-
clientDoc = await payload.
|
|
4724
|
-
collection: slugs.supportClients,
|
|
5003
|
+
clientDoc = await dbCreate(payload, slugs.supportClients, {
|
|
4725
5004
|
data: {
|
|
4726
5005
|
email: profile.email,
|
|
4727
5006
|
firstName: nameParts[0] || fullName,
|
|
4728
5007
|
lastName: nameParts.slice(1).join(" ") || "-",
|
|
4729
5008
|
company: fullName,
|
|
5009
|
+
googleId: profile.id,
|
|
4730
5010
|
password: autoPassword
|
|
4731
5011
|
},
|
|
4732
5012
|
overrideAccess: true
|
|
4733
5013
|
});
|
|
4734
5014
|
}
|
|
4735
|
-
const
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
id: clientDoc.id,
|
|
4739
|
-
data: { password: tempPassword },
|
|
4740
|
-
overrideAccess: true
|
|
4741
|
-
});
|
|
4742
|
-
const loginResult = await payload.login({
|
|
4743
|
-
collection: slugs.supportClients,
|
|
4744
|
-
data: { email: profile.email, password: tempPassword }
|
|
4745
|
-
});
|
|
4746
|
-
const postLoginPassword = crypto3.randomBytes(48).toString("base64url");
|
|
4747
|
-
await payload.update({
|
|
4748
|
-
collection: slugs.supportClients,
|
|
4749
|
-
id: clientDoc.id,
|
|
4750
|
-
data: { password: postLoginPassword },
|
|
4751
|
-
overrideAccess: true
|
|
4752
|
-
});
|
|
4753
|
-
if (!loginResult.token) {
|
|
4754
|
-
return Response.json({ error: "login_failed" }, { status: 400 });
|
|
5015
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
5016
|
+
if (!secret) {
|
|
5017
|
+
return Response.json({ error: "server_misconfigured" }, { status: 500 });
|
|
4755
5018
|
}
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
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
|
|
4760
5061
|
});
|
|
4761
5062
|
}
|
|
4762
5063
|
return Response.json({ error: "Action invalide" }, { status: 400 });
|
|
@@ -4797,8 +5098,7 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4797
5098
|
);
|
|
4798
5099
|
}
|
|
4799
5100
|
const clientId = req.user.id;
|
|
4800
|
-
const tickets = await payload.
|
|
4801
|
-
collection: slugs.tickets,
|
|
5101
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4802
5102
|
where: { client: { equals: clientId } },
|
|
4803
5103
|
limit: 1e4,
|
|
4804
5104
|
depth: 0,
|
|
@@ -4807,39 +5107,32 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
4807
5107
|
});
|
|
4808
5108
|
const ticketIds = tickets.docs.map((t) => t.id);
|
|
4809
5109
|
if (ticketIds.length > 0) {
|
|
4810
|
-
await payload.
|
|
4811
|
-
collection: slugs.ticketMessages,
|
|
5110
|
+
await dbDelete(payload, slugs.ticketMessages, {
|
|
4812
5111
|
where: { ticket: { in: ticketIds } },
|
|
4813
5112
|
overrideAccess: true
|
|
4814
5113
|
});
|
|
4815
|
-
await payload.
|
|
4816
|
-
collection: slugs.ticketActivityLog,
|
|
5114
|
+
await dbDelete(payload, slugs.ticketActivityLog, {
|
|
4817
5115
|
where: { ticket: { in: ticketIds } },
|
|
4818
5116
|
overrideAccess: true
|
|
4819
5117
|
});
|
|
4820
|
-
await payload.
|
|
4821
|
-
collection: slugs.timeEntries,
|
|
5118
|
+
await dbDelete(payload, slugs.timeEntries, {
|
|
4822
5119
|
where: { ticket: { in: ticketIds } },
|
|
4823
5120
|
overrideAccess: true
|
|
4824
5121
|
});
|
|
4825
|
-
await payload.
|
|
4826
|
-
collection: slugs.tickets,
|
|
5122
|
+
await dbDelete(payload, slugs.tickets, {
|
|
4827
5123
|
where: { client: { equals: clientId } },
|
|
4828
5124
|
overrideAccess: true
|
|
4829
5125
|
});
|
|
4830
5126
|
}
|
|
4831
|
-
await payload.
|
|
4832
|
-
collection: slugs.satisfactionSurveys,
|
|
5127
|
+
await dbDelete(payload, slugs.satisfactionSurveys, {
|
|
4833
5128
|
where: { client: { equals: clientId } },
|
|
4834
5129
|
overrideAccess: true
|
|
4835
5130
|
});
|
|
4836
|
-
await payload.
|
|
4837
|
-
collection: slugs.chatMessages,
|
|
5131
|
+
await dbDelete(payload, slugs.chatMessages, {
|
|
4838
5132
|
where: { client: { equals: clientId } },
|
|
4839
5133
|
overrideAccess: true
|
|
4840
5134
|
});
|
|
4841
|
-
await payload.
|
|
4842
|
-
collection: slugs.supportClients,
|
|
5135
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4843
5136
|
id: clientId,
|
|
4844
5137
|
overrideAccess: true
|
|
4845
5138
|
});
|
|
@@ -4880,8 +5173,8 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4880
5173
|
return Response.json({ error: "sourceId and targetId are required and must be different" }, { status: 400 });
|
|
4881
5174
|
}
|
|
4882
5175
|
const [source, target] = await Promise.all([
|
|
4883
|
-
payload
|
|
4884
|
-
payload
|
|
5176
|
+
dbFindByID(payload, slugs.supportClients, { id: sourceId, depth: 0, overrideAccess: true }),
|
|
5177
|
+
dbFindByID(payload, slugs.supportClients, { id: targetId, depth: 0, overrideAccess: true })
|
|
4885
5178
|
]);
|
|
4886
5179
|
if (!source || !target) {
|
|
4887
5180
|
return Response.json({ error: "Source or target client not found" }, { status: 404 });
|
|
@@ -4893,63 +5186,57 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
4893
5186
|
pendingEmails: 0,
|
|
4894
5187
|
satisfactionSurveys: 0
|
|
4895
5188
|
};
|
|
4896
|
-
const tickets = await payload.
|
|
4897
|
-
collection: slugs.tickets,
|
|
5189
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
4898
5190
|
where: { client: { equals: sourceId } },
|
|
4899
5191
|
limit: 500,
|
|
4900
5192
|
depth: 0,
|
|
4901
5193
|
overrideAccess: true
|
|
4902
5194
|
});
|
|
4903
5195
|
for (const ticket of tickets.docs) {
|
|
4904
|
-
await payload
|
|
5196
|
+
await dbUpdate(payload, slugs.tickets, { id: ticket.id, data: { client: targetId }, overrideAccess: true });
|
|
4905
5197
|
results.tickets++;
|
|
4906
5198
|
}
|
|
4907
|
-
const messages = await payload.
|
|
4908
|
-
collection: slugs.ticketMessages,
|
|
5199
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
4909
5200
|
where: { authorClient: { equals: sourceId } },
|
|
4910
5201
|
limit: 1e3,
|
|
4911
5202
|
depth: 0,
|
|
4912
5203
|
overrideAccess: true
|
|
4913
5204
|
});
|
|
4914
5205
|
for (const msg of messages.docs) {
|
|
4915
|
-
await payload
|
|
5206
|
+
await dbUpdate(payload, slugs.ticketMessages, { id: msg.id, data: { authorClient: targetId }, overrideAccess: true });
|
|
4916
5207
|
results.ticketMessages++;
|
|
4917
5208
|
}
|
|
4918
|
-
const chats = await payload.
|
|
4919
|
-
collection: slugs.chatMessages,
|
|
5209
|
+
const chats = await dbFind(payload, slugs.chatMessages, {
|
|
4920
5210
|
where: { client: { equals: sourceId } },
|
|
4921
5211
|
limit: 1e3,
|
|
4922
5212
|
depth: 0,
|
|
4923
5213
|
overrideAccess: true
|
|
4924
5214
|
});
|
|
4925
5215
|
for (const chat of chats.docs) {
|
|
4926
|
-
await payload
|
|
5216
|
+
await dbUpdate(payload, slugs.chatMessages, { id: chat.id, data: { client: targetId }, overrideAccess: true });
|
|
4927
5217
|
results.chatMessages++;
|
|
4928
5218
|
}
|
|
4929
|
-
const pendingEmails = await payload.
|
|
4930
|
-
collection: slugs.pendingEmails,
|
|
5219
|
+
const pendingEmails = await dbFind(payload, slugs.pendingEmails, {
|
|
4931
5220
|
where: { client: { equals: sourceId } },
|
|
4932
5221
|
limit: 500,
|
|
4933
5222
|
depth: 0,
|
|
4934
5223
|
overrideAccess: true
|
|
4935
5224
|
});
|
|
4936
5225
|
for (const pe of pendingEmails.docs) {
|
|
4937
|
-
await payload
|
|
5226
|
+
await dbUpdate(payload, slugs.pendingEmails, { id: pe.id, data: { client: targetId }, overrideAccess: true });
|
|
4938
5227
|
results.pendingEmails++;
|
|
4939
5228
|
}
|
|
4940
|
-
const surveys = await payload.
|
|
4941
|
-
collection: slugs.satisfactionSurveys,
|
|
5229
|
+
const surveys = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
4942
5230
|
where: { client: { equals: sourceId } },
|
|
4943
5231
|
limit: 500,
|
|
4944
5232
|
depth: 0,
|
|
4945
5233
|
overrideAccess: true
|
|
4946
5234
|
});
|
|
4947
5235
|
for (const survey of surveys.docs) {
|
|
4948
|
-
await payload
|
|
5236
|
+
await dbUpdate(payload, slugs.satisfactionSurveys, { id: survey.id, data: { client: targetId }, overrideAccess: true });
|
|
4949
5237
|
results.satisfactionSurveys++;
|
|
4950
5238
|
}
|
|
4951
|
-
await payload.
|
|
4952
|
-
collection: slugs.supportClients,
|
|
5239
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
4953
5240
|
id: sourceId,
|
|
4954
5241
|
overrideAccess: true
|
|
4955
5242
|
});
|
|
@@ -5143,8 +5430,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5143
5430
|
if (blockedEmails.includes(conversation.client.email)) {
|
|
5144
5431
|
return Response.json({ error: "Cannot create ticket from system email address" }, { status: 400 });
|
|
5145
5432
|
}
|
|
5146
|
-
const clientResult = await payload.
|
|
5147
|
-
collection: slugs.supportClients,
|
|
5433
|
+
const clientResult = await dbFind(payload, slugs.supportClients, {
|
|
5148
5434
|
where: { email: { equals: conversation.client.email } },
|
|
5149
5435
|
limit: 1,
|
|
5150
5436
|
depth: 0,
|
|
@@ -5155,8 +5441,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5155
5441
|
if (!client) {
|
|
5156
5442
|
const nameParts = conversation.client.name.split(" ");
|
|
5157
5443
|
const randomPassword = crypto3.randomBytes(48).toString("base64url");
|
|
5158
|
-
client = await payload.
|
|
5159
|
-
collection: slugs.supportClients,
|
|
5444
|
+
client = await dbCreate(payload, slugs.supportClients, {
|
|
5160
5445
|
data: {
|
|
5161
5446
|
email: conversation.client.email,
|
|
5162
5447
|
password: randomPassword,
|
|
@@ -5168,15 +5453,13 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5168
5453
|
});
|
|
5169
5454
|
isNewClient = true;
|
|
5170
5455
|
}
|
|
5171
|
-
const adminUsers = await payload.
|
|
5172
|
-
collection: slugs.users,
|
|
5456
|
+
const adminUsers = await dbFind(payload, slugs.users, {
|
|
5173
5457
|
limit: 1,
|
|
5174
5458
|
depth: 0,
|
|
5175
5459
|
overrideAccess: true
|
|
5176
5460
|
});
|
|
5177
5461
|
const adminUserId = adminUsers.docs[0]?.id;
|
|
5178
|
-
const ticket = await payload.
|
|
5179
|
-
collection: slugs.tickets,
|
|
5462
|
+
const ticket = await dbCreate(payload, slugs.tickets, {
|
|
5180
5463
|
data: {
|
|
5181
5464
|
subject: conversation.subject,
|
|
5182
5465
|
client: client.id,
|
|
@@ -5189,8 +5472,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5189
5472
|
let importedCount = 0;
|
|
5190
5473
|
for (const msg of conversation.messages) {
|
|
5191
5474
|
const isAdmin = msg.from === "admin";
|
|
5192
|
-
await payload.
|
|
5193
|
-
collection: slugs.ticketMessages,
|
|
5475
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
5194
5476
|
data: {
|
|
5195
5477
|
ticket: ticket.id,
|
|
5196
5478
|
body: msg.content,
|
|
@@ -5226,8 +5508,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5226
5508
|
// src/utils/fireWebhooks.ts
|
|
5227
5509
|
async function fireWebhooks(payload, slugs, event, data) {
|
|
5228
5510
|
try {
|
|
5229
|
-
const endpoints = await payload.
|
|
5230
|
-
collection: slugs.webhookEndpoints,
|
|
5511
|
+
const endpoints = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5231
5512
|
where: {
|
|
5232
5513
|
active: { equals: true },
|
|
5233
5514
|
events: { contains: event }
|
|
@@ -5251,8 +5532,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5251
5532
|
signal: AbortSignal.timeout(1e4)
|
|
5252
5533
|
});
|
|
5253
5534
|
try {
|
|
5254
|
-
await payload.
|
|
5255
|
-
collection: slugs.webhookEndpoints,
|
|
5535
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5256
5536
|
id: endpoint.id,
|
|
5257
5537
|
data: {
|
|
5258
5538
|
lastTriggeredAt: timestamp,
|
|
@@ -5265,8 +5545,7 @@ async function fireWebhooks(payload, slugs, event, data) {
|
|
|
5265
5545
|
} catch (error) {
|
|
5266
5546
|
console.warn(`[support] Webhook delivery failed: ${endpoint.url}`, error);
|
|
5267
5547
|
try {
|
|
5268
|
-
await payload.
|
|
5269
|
-
collection: slugs.webhookEndpoints,
|
|
5548
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5270
5549
|
id: endpoint.id,
|
|
5271
5550
|
data: {
|
|
5272
5551
|
lastTriggeredAt: timestamp,
|
|
@@ -5301,8 +5580,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5301
5580
|
const settings = await readSupportSettings(payload);
|
|
5302
5581
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
|
|
5303
5582
|
const results = { processed: 0, errors: 0 };
|
|
5304
|
-
const scheduled = await payload.
|
|
5305
|
-
collection: slugs.ticketMessages,
|
|
5583
|
+
const scheduled = await dbFind(payload, slugs.ticketMessages, {
|
|
5306
5584
|
where: {
|
|
5307
5585
|
and: [
|
|
5308
5586
|
{ scheduledAt: { less_than_equal: now.toISOString() } },
|
|
@@ -5322,8 +5600,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5322
5600
|
try {
|
|
5323
5601
|
const msg = message;
|
|
5324
5602
|
const ticketId = typeof msg.ticket === "object" ? msg.ticket.id : msg.ticket;
|
|
5325
|
-
const ticket = await payload.
|
|
5326
|
-
collection: slugs.tickets,
|
|
5603
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5327
5604
|
id: ticketId,
|
|
5328
5605
|
depth: 1,
|
|
5329
5606
|
overrideAccess: true
|
|
@@ -5346,8 +5623,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5346
5623
|
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
5347
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>`
|
|
5348
5625
|
});
|
|
5349
|
-
await payload.
|
|
5350
|
-
collection: slugs.ticketMessages,
|
|
5626
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5351
5627
|
id: msg.id,
|
|
5352
5628
|
data: {
|
|
5353
5629
|
scheduledSent: true,
|
|
@@ -5357,16 +5633,14 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5357
5633
|
overrideAccess: true
|
|
5358
5634
|
});
|
|
5359
5635
|
} else {
|
|
5360
|
-
await payload.
|
|
5361
|
-
collection: slugs.ticketMessages,
|
|
5636
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
5362
5637
|
id: msg.id,
|
|
5363
5638
|
data: { scheduledSent: true },
|
|
5364
5639
|
overrideAccess: true
|
|
5365
5640
|
});
|
|
5366
5641
|
}
|
|
5367
5642
|
if (msg.authorType === "admin" && !msg.isInternal) {
|
|
5368
|
-
await payload.
|
|
5369
|
-
collection: slugs.tickets,
|
|
5643
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
5370
5644
|
id: ticketId,
|
|
5371
5645
|
data: { status: "waiting_client" },
|
|
5372
5646
|
overrideAccess: true
|
|
@@ -5398,6 +5672,208 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5398
5672
|
};
|
|
5399
5673
|
}
|
|
5400
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
|
+
|
|
5401
5877
|
// src/endpoints/user-prefs.ts
|
|
5402
5878
|
var PREF_KEY_PREFIX = "support-user-prefs";
|
|
5403
5879
|
var DEFAULT_USER_PREFS2 = {
|
|
@@ -5413,8 +5889,7 @@ function createUserPrefsGetEndpoint(slugs) {
|
|
|
5413
5889
|
const payload = req.payload;
|
|
5414
5890
|
requireAdmin(req, slugs);
|
|
5415
5891
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5416
|
-
const prefs = await payload
|
|
5417
|
-
collection: "payload-preferences",
|
|
5892
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
5418
5893
|
where: { key: { equals: key } },
|
|
5419
5894
|
limit: 1,
|
|
5420
5895
|
depth: 0,
|
|
@@ -5448,8 +5923,7 @@ function createUserPrefsPostEndpoint(slugs) {
|
|
|
5448
5923
|
requireAdmin(req, slugs);
|
|
5449
5924
|
const body = await req.json();
|
|
5450
5925
|
const key = `${PREF_KEY_PREFIX}-${req.user.id}`;
|
|
5451
|
-
const existing = await payload
|
|
5452
|
-
collection: "payload-preferences",
|
|
5926
|
+
const existing = await dbFind(payload, "payload-preferences", {
|
|
5453
5927
|
where: { key: { equals: key } },
|
|
5454
5928
|
limit: 1,
|
|
5455
5929
|
depth: 0,
|
|
@@ -5510,8 +5984,7 @@ function createEscalateEndpoint(slugs) {
|
|
|
5510
5984
|
if (Number.isNaN(ticketId)) {
|
|
5511
5985
|
return Response.json({ error: "invalid-id" }, { status: 400 });
|
|
5512
5986
|
}
|
|
5513
|
-
const ticket = await req.payload.
|
|
5514
|
-
collection: slugs.tickets,
|
|
5987
|
+
const ticket = await dbUpdate(req.payload, slugs.tickets, {
|
|
5515
5988
|
id: ticketId,
|
|
5516
5989
|
data: { status: "escalated" },
|
|
5517
5990
|
overrideAccess: true
|
|
@@ -5559,8 +6032,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5559
6032
|
{ status: 400 }
|
|
5560
6033
|
);
|
|
5561
6034
|
}
|
|
5562
|
-
const ticket = await payload.
|
|
5563
|
-
collection: slugs.tickets,
|
|
6035
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5564
6036
|
id: ticketId,
|
|
5565
6037
|
depth: 0,
|
|
5566
6038
|
overrideAccess: true
|
|
@@ -5572,8 +6044,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5572
6044
|
if (ticketClientId !== req.user.id) {
|
|
5573
6045
|
return Response.json({ error: "forbidden" }, { status: 403 });
|
|
5574
6046
|
}
|
|
5575
|
-
const existing = await payload
|
|
5576
|
-
collection: "ticket-feedback",
|
|
6047
|
+
const existing = await dbFind(payload, "ticket-feedback", {
|
|
5577
6048
|
where: { ticket: { equals: ticketId } },
|
|
5578
6049
|
limit: 1,
|
|
5579
6050
|
depth: 0,
|
|
@@ -5585,8 +6056,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5585
6056
|
{ status: 409 }
|
|
5586
6057
|
);
|
|
5587
6058
|
}
|
|
5588
|
-
const feedback = await payload
|
|
5589
|
-
collection: "ticket-feedback",
|
|
6059
|
+
const feedback = await dbCreate(payload, "ticket-feedback", {
|
|
5590
6060
|
data: {
|
|
5591
6061
|
ticket: ticketId,
|
|
5592
6062
|
client: req.user.id,
|
|
@@ -5610,6 +6080,7 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
5610
6080
|
// src/endpoints/transfer-ticket.ts
|
|
5611
6081
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5612
6082
|
var MAX_TRANSFERS_PER_DAY = 5;
|
|
6083
|
+
var transferLimiter = new RateLimiter(24 * 60 * 60 * 1e3, MAX_TRANSFERS_PER_DAY);
|
|
5613
6084
|
function createTransferTicketEndpoint(slugs) {
|
|
5614
6085
|
return {
|
|
5615
6086
|
path: "/support/tickets/:id/transfer",
|
|
@@ -5634,8 +6105,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5634
6105
|
const includeAttachments = body.includeAttachments !== false;
|
|
5635
6106
|
const customMessage = typeof body.message === "string" ? body.message.slice(0, 1e3) : "";
|
|
5636
6107
|
const payload = req.payload;
|
|
5637
|
-
const ticket = await payload.
|
|
5638
|
-
collection: slugs.tickets,
|
|
6108
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5639
6109
|
id: ticketId,
|
|
5640
6110
|
depth: 1,
|
|
5641
6111
|
overrideAccess: true
|
|
@@ -5646,10 +6116,15 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5646
6116
|
if (!isAdmin && !isOwner) {
|
|
5647
6117
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5648
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
|
+
}
|
|
5649
6125
|
try {
|
|
5650
6126
|
const since = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
5651
|
-
const existing = await payload.
|
|
5652
|
-
collection: slugs.emailLogs,
|
|
6127
|
+
const existing = await dbCount(payload, slugs.emailLogs, {
|
|
5653
6128
|
where: {
|
|
5654
6129
|
and: [
|
|
5655
6130
|
{ action: { equals: "transfer" } },
|
|
@@ -5667,8 +6142,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5667
6142
|
}
|
|
5668
6143
|
} catch {
|
|
5669
6144
|
}
|
|
5670
|
-
const messages = await payload.
|
|
5671
|
-
collection: slugs.ticketMessages,
|
|
6145
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
5672
6146
|
where: {
|
|
5673
6147
|
and: [
|
|
5674
6148
|
{ ticket: { equals: ticketId } },
|
|
@@ -5748,8 +6222,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
5748
6222
|
html
|
|
5749
6223
|
});
|
|
5750
6224
|
try {
|
|
5751
|
-
await payload.
|
|
5752
|
-
collection: slugs.emailLogs,
|
|
6225
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5753
6226
|
data: {
|
|
5754
6227
|
status: "success",
|
|
5755
6228
|
action: "transfer",
|
|
@@ -5789,6 +6262,8 @@ function statusToLabel(status) {
|
|
|
5789
6262
|
}
|
|
5790
6263
|
}
|
|
5791
6264
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6265
|
+
var MAX_COLLABORATORS_PER_TICKET = 20;
|
|
6266
|
+
var inviteLimiter = new RateLimiter(60 * 60 * 1e3, 10);
|
|
5792
6267
|
function createInviteCollaboratorEndpoint(slugs) {
|
|
5793
6268
|
return {
|
|
5794
6269
|
path: "/support/tickets/:id/invite",
|
|
@@ -5812,8 +6287,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5812
6287
|
}
|
|
5813
6288
|
const role = body.role === "collaborator" ? "collaborator" : "viewer";
|
|
5814
6289
|
const payload = req.payload;
|
|
5815
|
-
const ticket = await payload.
|
|
5816
|
-
collection: slugs.tickets,
|
|
6290
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
5817
6291
|
id: ticketId,
|
|
5818
6292
|
depth: 1,
|
|
5819
6293
|
overrideAccess: true
|
|
@@ -5825,13 +6299,22 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5825
6299
|
if (!isAdmin && !isOwner) {
|
|
5826
6300
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5827
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
|
+
}
|
|
5828
6312
|
const ownerEmail = typeof ticket.client === "object" ? ticket.client?.email : void 0;
|
|
5829
6313
|
if (ownerEmail && ownerEmail.toLowerCase() === email) {
|
|
5830
6314
|
return Response.json({ error: "Ce client est d\xE9j\xE0 propri\xE9taire du ticket" }, { status: 400 });
|
|
5831
6315
|
}
|
|
5832
6316
|
let inviteeId = null;
|
|
5833
|
-
const existing = await payload.
|
|
5834
|
-
collection: slugs.supportClients,
|
|
6317
|
+
const existing = await dbFind(payload, slugs.supportClients, {
|
|
5835
6318
|
where: { email: { equals: email } },
|
|
5836
6319
|
limit: 1,
|
|
5837
6320
|
depth: 0,
|
|
@@ -5841,8 +6324,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5841
6324
|
inviteeId = existing.docs[0].id;
|
|
5842
6325
|
} else {
|
|
5843
6326
|
const tempPassword = randomBytes(16).toString("hex");
|
|
5844
|
-
const created = await payload.
|
|
5845
|
-
collection: slugs.supportClients,
|
|
6327
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
5846
6328
|
data: {
|
|
5847
6329
|
email,
|
|
5848
6330
|
firstName: "Invit\xE9",
|
|
@@ -5859,8 +6341,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5859
6341
|
if (!inviteeId) {
|
|
5860
6342
|
return Response.json({ error: "Impossible de cr\xE9er le client invit\xE9" }, { status: 500 });
|
|
5861
6343
|
}
|
|
5862
|
-
const dupe = await payload
|
|
5863
|
-
collection: "ticket-collaborators",
|
|
6344
|
+
const dupe = await dbFind(payload, "ticket-collaborators", {
|
|
5864
6345
|
where: {
|
|
5865
6346
|
and: [
|
|
5866
6347
|
{ ticket: { equals: ticketId } },
|
|
@@ -5873,15 +6354,13 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5873
6354
|
});
|
|
5874
6355
|
const invitationToken = randomBytes(24).toString("hex");
|
|
5875
6356
|
if (dupe.docs.length > 0) {
|
|
5876
|
-
await payload
|
|
5877
|
-
collection: "ticket-collaborators",
|
|
6357
|
+
await dbUpdate(payload, "ticket-collaborators", {
|
|
5878
6358
|
id: dupe.docs[0].id,
|
|
5879
6359
|
data: { role, invitationToken },
|
|
5880
6360
|
overrideAccess: true
|
|
5881
6361
|
});
|
|
5882
6362
|
} else {
|
|
5883
|
-
await payload
|
|
5884
|
-
collection: "ticket-collaborators",
|
|
6363
|
+
await dbCreate(payload, "ticket-collaborators", {
|
|
5885
6364
|
data: {
|
|
5886
6365
|
ticket: ticketId,
|
|
5887
6366
|
client: inviteeId,
|
|
@@ -5924,8 +6403,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
5924
6403
|
)
|
|
5925
6404
|
});
|
|
5926
6405
|
try {
|
|
5927
|
-
await payload.
|
|
5928
|
-
collection: slugs.emailLogs,
|
|
6406
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
5929
6407
|
data: {
|
|
5930
6408
|
status: "success",
|
|
5931
6409
|
action: "invite",
|
|
@@ -5978,6 +6456,7 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5978
6456
|
];
|
|
5979
6457
|
if (!f || f.ai !== false) {
|
|
5980
6458
|
endpoints.push(createAiEndpoint(slugs));
|
|
6459
|
+
endpoints.push(createAiAgentEndpoint(slugs));
|
|
5981
6460
|
endpoints.push(...createClientIntelligenceEndpoint(slugs));
|
|
5982
6461
|
endpoints.push(createTicketSynthesisEndpoint(slugs));
|
|
5983
6462
|
}
|
|
@@ -6008,21 +6487,26 @@ function createSupportEndpoints(slugs, options) {
|
|
|
6008
6487
|
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs));
|
|
6009
6488
|
endpoints.push(createAdminChatStreamEndpoint(slugs));
|
|
6010
6489
|
}
|
|
6011
|
-
if (!f || f.timeTracking !== false)
|
|
6490
|
+
if (!f || f.timeTracking !== false) {
|
|
6491
|
+
endpoints.push(createBillingEndpoint(slugs));
|
|
6492
|
+
endpoints.push(createInvoiceEndpoint(slugs));
|
|
6493
|
+
}
|
|
6012
6494
|
if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs));
|
|
6013
6495
|
if (!f || f.emailTracking !== false) {
|
|
6014
6496
|
endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs));
|
|
6015
6497
|
}
|
|
6016
6498
|
if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs));
|
|
6017
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));
|
|
6018
6503
|
return endpoints;
|
|
6019
6504
|
}
|
|
6020
6505
|
|
|
6021
6506
|
// src/utils/adminNotification.ts
|
|
6022
6507
|
async function createAdminNotification(payload, data, collectionSlug = "admin-notifications") {
|
|
6023
6508
|
try {
|
|
6024
|
-
await payload
|
|
6025
|
-
collection: collectionSlug,
|
|
6509
|
+
await dbCreate(payload, collectionSlug, {
|
|
6026
6510
|
data: {
|
|
6027
6511
|
title: data.title,
|
|
6028
6512
|
message: data.message,
|
|
@@ -6042,8 +6526,7 @@ function dispatchWebhook(data, event, payload, slugs) {
|
|
|
6042
6526
|
}
|
|
6043
6527
|
async function _dispatch(data, event, payload, slugs) {
|
|
6044
6528
|
try {
|
|
6045
|
-
const { docs: endpoints } = await payload.
|
|
6046
|
-
collection: slugs.webhookEndpoints,
|
|
6529
|
+
const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
|
|
6047
6530
|
where: {
|
|
6048
6531
|
and: [
|
|
6049
6532
|
{ active: { equals: true } },
|
|
@@ -6080,8 +6563,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
6080
6563
|
signal: AbortSignal.timeout(1e4)
|
|
6081
6564
|
// 10s timeout
|
|
6082
6565
|
});
|
|
6083
|
-
await payload.
|
|
6084
|
-
collection: slugs.webhookEndpoints,
|
|
6566
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6085
6567
|
id: endpoint.id,
|
|
6086
6568
|
data: {
|
|
6087
6569
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6095,8 +6577,7 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
|
6095
6577
|
} catch (err) {
|
|
6096
6578
|
console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
|
|
6097
6579
|
try {
|
|
6098
|
-
await payload.
|
|
6099
|
-
collection: slugs.webhookEndpoints,
|
|
6580
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6100
6581
|
id: endpoint.id,
|
|
6101
6582
|
data: {
|
|
6102
6583
|
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6330,8 +6811,7 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6330
6811
|
if (typeof policyRef === "object" && policyRef.firstResponseTime) {
|
|
6331
6812
|
return policyRef;
|
|
6332
6813
|
}
|
|
6333
|
-
return await payload.
|
|
6334
|
-
collection: slugs.slaPolicies,
|
|
6814
|
+
return await dbFindByID(payload, slugs.slaPolicies, {
|
|
6335
6815
|
id: policyId,
|
|
6336
6816
|
depth: 0,
|
|
6337
6817
|
overrideAccess: true
|
|
@@ -6342,8 +6822,7 @@ async function resolveSlaPolicy(payload, ticket, slugs) {
|
|
|
6342
6822
|
}
|
|
6343
6823
|
const priority = field(ticket, "priority") || "normal";
|
|
6344
6824
|
try {
|
|
6345
|
-
const defaults = await payload.
|
|
6346
|
-
collection: slugs.slaPolicies,
|
|
6825
|
+
const defaults = await dbFind(payload, slugs.slaPolicies, {
|
|
6347
6826
|
where: {
|
|
6348
6827
|
and: [
|
|
6349
6828
|
{ isDefault: { equals: true } },
|
|
@@ -6380,8 +6859,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6380
6859
|
if (typeof policyRef === "object" && policyRef.escalateOnBreach !== void 0) {
|
|
6381
6860
|
policy = policyRef;
|
|
6382
6861
|
} else {
|
|
6383
|
-
policy = await payload.
|
|
6384
|
-
collection: slugs.slaPolicies,
|
|
6862
|
+
policy = await dbFindByID(payload, slugs.slaPolicies, {
|
|
6385
6863
|
id: policyId,
|
|
6386
6864
|
depth: 0,
|
|
6387
6865
|
overrideAccess: true
|
|
@@ -6394,8 +6872,7 @@ async function handleSlaBreach(payload, ticket, type, slugs, notificationSlug) {
|
|
|
6394
6872
|
try {
|
|
6395
6873
|
const escalateToId = typeof policy.escalateTo === "object" ? policy.escalateTo.id : policy.escalateTo;
|
|
6396
6874
|
if (!escalateToId) return;
|
|
6397
|
-
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await payload.
|
|
6398
|
-
collection: slugs.users,
|
|
6875
|
+
const escalateUser = typeof policy.escalateTo === "object" && policy.escalateTo.email ? policy.escalateTo : await dbFindByID(payload, slugs.users, {
|
|
6399
6876
|
id: escalateToId,
|
|
6400
6877
|
depth: 0,
|
|
6401
6878
|
overrideAccess: true
|
|
@@ -6443,8 +6920,7 @@ function createAssignSlaDeadlines(slugs, notificationSlug = "admin-notifications
|
|
|
6443
6920
|
if (!field(doc, "slaPolicy") && policy.id) {
|
|
6444
6921
|
updateData.slaPolicy = policy.id;
|
|
6445
6922
|
}
|
|
6446
|
-
await payload.
|
|
6447
|
-
collection: slugs.tickets,
|
|
6923
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6448
6924
|
id: doc.id,
|
|
6449
6925
|
data: updateData,
|
|
6450
6926
|
overrideAccess: true
|
|
@@ -6467,8 +6943,7 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6467
6943
|
const now = /* @__PURE__ */ new Date();
|
|
6468
6944
|
const deadline = new Date(doc.slaResolutionDue);
|
|
6469
6945
|
const breached = now > deadline;
|
|
6470
|
-
await payload.
|
|
6471
|
-
collection: slugs.tickets,
|
|
6946
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6472
6947
|
id: doc.id,
|
|
6473
6948
|
data: { slaResolutionBreached: breached },
|
|
6474
6949
|
overrideAccess: true
|
|
@@ -6482,6 +6957,35 @@ function createCheckSlaOnResolve(slugs, notificationSlug = "admin-notifications"
|
|
|
6482
6957
|
return doc;
|
|
6483
6958
|
};
|
|
6484
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
|
+
}
|
|
6485
6989
|
function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications") {
|
|
6486
6990
|
return async ({ doc, operation, req }) => {
|
|
6487
6991
|
if (operation !== "create") return doc;
|
|
@@ -6489,8 +6993,7 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6489
6993
|
try {
|
|
6490
6994
|
const { payload } = req;
|
|
6491
6995
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
6492
|
-
const ticket = await payload.
|
|
6493
|
-
collection: slugs.tickets,
|
|
6996
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
6494
6997
|
id: ticketId,
|
|
6495
6998
|
depth: 1,
|
|
6496
6999
|
overrideAccess: true
|
|
@@ -6501,8 +7004,7 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6501
7004
|
const now = /* @__PURE__ */ new Date();
|
|
6502
7005
|
const deadline = new Date(ticket.slaFirstResponseDue);
|
|
6503
7006
|
const breached = now > deadline;
|
|
6504
|
-
await payload.
|
|
6505
|
-
collection: slugs.tickets,
|
|
7007
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
6506
7008
|
id: ticketId,
|
|
6507
7009
|
data: { slaFirstResponseBreached: breached },
|
|
6508
7010
|
overrideAccess: true
|
|
@@ -6517,6 +7019,108 @@ function createCheckSlaOnReply(slugs, notificationSlug = "admin-notifications")
|
|
|
6517
7019
|
};
|
|
6518
7020
|
}
|
|
6519
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
|
+
|
|
6520
7124
|
// src/collections/Tickets.ts
|
|
6521
7125
|
function createAssignTicketNumber(slugs) {
|
|
6522
7126
|
return async ({ data, operation, req }) => {
|
|
@@ -6779,56 +7383,6 @@ function createNotifyClientOnResolve(slugs) {
|
|
|
6779
7383
|
return doc;
|
|
6780
7384
|
};
|
|
6781
7385
|
}
|
|
6782
|
-
function createAutoCalculateSLA(slugs) {
|
|
6783
|
-
return async ({ doc, operation, req }) => {
|
|
6784
|
-
if (operation !== "create") return doc;
|
|
6785
|
-
try {
|
|
6786
|
-
const { payload } = req;
|
|
6787
|
-
const ticketPriority = doc.priority || "normal";
|
|
6788
|
-
let policy = null;
|
|
6789
|
-
const byPriority = await payload.find({
|
|
6790
|
-
collection: slugs.slaPolicies,
|
|
6791
|
-
where: { priority: { equals: ticketPriority } },
|
|
6792
|
-
limit: 1,
|
|
6793
|
-
depth: 0,
|
|
6794
|
-
overrideAccess: true
|
|
6795
|
-
});
|
|
6796
|
-
if (byPriority.docs.length > 0) {
|
|
6797
|
-
policy = byPriority.docs[0];
|
|
6798
|
-
} else {
|
|
6799
|
-
const defaults = await payload.find({
|
|
6800
|
-
collection: slugs.slaPolicies,
|
|
6801
|
-
where: { isDefault: { equals: true } },
|
|
6802
|
-
limit: 1,
|
|
6803
|
-
depth: 0,
|
|
6804
|
-
overrideAccess: true
|
|
6805
|
-
});
|
|
6806
|
-
if (defaults.docs.length > 0) {
|
|
6807
|
-
policy = defaults.docs[0];
|
|
6808
|
-
}
|
|
6809
|
-
}
|
|
6810
|
-
if (!policy) return doc;
|
|
6811
|
-
const now = /* @__PURE__ */ new Date();
|
|
6812
|
-
const firstResponseMinutes = policy.firstResponseTime || 240;
|
|
6813
|
-
const resolutionMinutes = policy.resolutionTime || 1440;
|
|
6814
|
-
const firstResponseDue = new Date(now.getTime() + firstResponseMinutes * 6e4);
|
|
6815
|
-
const resolutionDue = new Date(now.getTime() + resolutionMinutes * 6e4);
|
|
6816
|
-
await payload.update({
|
|
6817
|
-
collection: slugs.tickets,
|
|
6818
|
-
id: doc.id,
|
|
6819
|
-
data: {
|
|
6820
|
-
slaPolicy: policy.id,
|
|
6821
|
-
slaFirstResponseDue: firstResponseDue.toISOString(),
|
|
6822
|
-
slaResolutionDue: resolutionDue.toISOString()
|
|
6823
|
-
},
|
|
6824
|
-
overrideAccess: true
|
|
6825
|
-
});
|
|
6826
|
-
} catch (err) {
|
|
6827
|
-
console.error("[support] Failed to auto-calculate SLA:", err);
|
|
6828
|
-
}
|
|
6829
|
-
return doc;
|
|
6830
|
-
};
|
|
6831
|
-
}
|
|
6832
7386
|
function createFireTicketWebhooks(slugs) {
|
|
6833
7387
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
6834
7388
|
const { payload } = req;
|
|
@@ -6920,8 +7474,7 @@ function createCascadeDelete(slugs) {
|
|
|
6920
7474
|
return async ({ id, req }) => {
|
|
6921
7475
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys];
|
|
6922
7476
|
for (const slug of collections) {
|
|
6923
|
-
await req.payload
|
|
6924
|
-
collection: slug,
|
|
7477
|
+
await dbDelete(req.payload, slug, {
|
|
6925
7478
|
where: { ticket: { equals: id } },
|
|
6926
7479
|
overrideAccess: true
|
|
6927
7480
|
});
|
|
@@ -7046,6 +7599,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7046
7599
|
{
|
|
7047
7600
|
name: "status",
|
|
7048
7601
|
type: "select",
|
|
7602
|
+
index: true,
|
|
7049
7603
|
defaultValue: "open",
|
|
7050
7604
|
label: "Statut",
|
|
7051
7605
|
options: [
|
|
@@ -7060,6 +7614,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7060
7614
|
{
|
|
7061
7615
|
name: "priority",
|
|
7062
7616
|
type: "select",
|
|
7617
|
+
index: true,
|
|
7063
7618
|
defaultValue: "normal",
|
|
7064
7619
|
label: "Priorite",
|
|
7065
7620
|
options: [
|
|
@@ -7075,7 +7630,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7075
7630
|
{
|
|
7076
7631
|
type: "row",
|
|
7077
7632
|
fields: [
|
|
7078
|
-
{ 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%" } }
|
|
7079
7634
|
]
|
|
7080
7635
|
},
|
|
7081
7636
|
// Billing collapsible
|
|
@@ -7144,15 +7699,16 @@ function createTicketsCollection(slugs, options) {
|
|
|
7144
7699
|
{
|
|
7145
7700
|
type: "row",
|
|
7146
7701
|
fields: [
|
|
7147
|
-
{ name: "slaFirstResponseDue", type: "date", label: "Echeance 1ere reponse (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7148
|
-
{ name: "slaResolutionDue", type: "date", label: "Echeance resolution (SLA)", admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } } }
|
|
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" } } }
|
|
7149
7704
|
]
|
|
7150
7705
|
},
|
|
7151
7706
|
{
|
|
7152
7707
|
type: "row",
|
|
7153
7708
|
fields: [
|
|
7154
7709
|
{ name: "slaFirstResponseBreached", type: "checkbox", label: "1ere reponse depassee", admin: { readOnly: true, width: "50%" } },
|
|
7155
|
-
{ name: "slaResolutionBreached", type: "checkbox", label: "Resolution depassee", admin: { readOnly: true, width: "50%" } }
|
|
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 } }
|
|
7156
7712
|
]
|
|
7157
7713
|
}
|
|
7158
7714
|
]
|
|
@@ -7174,7 +7730,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7174
7730
|
},
|
|
7175
7731
|
{ name: "mergedInto", type: "relationship", relationTo: slugs.tickets, label: "Fusionne dans", admin: { readOnly: true } },
|
|
7176
7732
|
{ name: "autoCloseRemindedAt", type: "date", label: "Rappel auto-close envoye", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7177
|
-
{ name: "autoCloseScheduledAt", type: "date", label: "Fermeture auto programmee", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" }, description: "Echeance ferme posee par une relance manuelle. Le ticket se ferme apres cette date sans reponse client." } }
|
|
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." } }
|
|
7178
7734
|
]
|
|
7179
7735
|
},
|
|
7180
7736
|
// Sidebar
|
|
@@ -7183,6 +7739,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
7183
7739
|
{
|
|
7184
7740
|
name: "category",
|
|
7185
7741
|
type: "select",
|
|
7742
|
+
index: true,
|
|
7186
7743
|
label: "Categorie",
|
|
7187
7744
|
options: [
|
|
7188
7745
|
{ label: "Bug / Dysfonctionnement", value: "bug" },
|
|
@@ -7193,7 +7750,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7193
7750
|
],
|
|
7194
7751
|
admin: { position: "sidebar" }
|
|
7195
7752
|
},
|
|
7196
|
-
{ 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" } },
|
|
7197
7755
|
{
|
|
7198
7756
|
name: "source",
|
|
7199
7757
|
type: "select",
|
|
@@ -7203,7 +7761,9 @@ function createTicketsCollection(slugs, options) {
|
|
|
7203
7761
|
{ label: "Portail client", value: "portal" },
|
|
7204
7762
|
{ label: "Chat en direct", value: "live-chat" },
|
|
7205
7763
|
{ label: "Email", value: "email" },
|
|
7206
|
-
{ label: "Admin", value: "admin" }
|
|
7764
|
+
{ label: "Admin", value: "admin" },
|
|
7765
|
+
{ label: "WhatsApp", value: "whatsapp" },
|
|
7766
|
+
{ label: "Messenger", value: "messenger" }
|
|
7207
7767
|
],
|
|
7208
7768
|
admin: { position: "sidebar" }
|
|
7209
7769
|
},
|
|
@@ -7254,8 +7814,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7254
7814
|
afterChange: [
|
|
7255
7815
|
createTrackSLA(slugs),
|
|
7256
7816
|
createTrackAiSummaryOnResolve(slugs),
|
|
7257
|
-
createAutoCalculateSLA(slugs),
|
|
7258
7817
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
7818
|
+
createPauseSlaOnHold(slugs),
|
|
7259
7819
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
7260
7820
|
createLogTicketActivity(slugs),
|
|
7261
7821
|
createNotifyOnAssignment(slugs),
|
|
@@ -7263,7 +7823,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
7263
7823
|
createTicketStatusEmail(slugs),
|
|
7264
7824
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
7265
7825
|
createFireTicketWebhooks(slugs),
|
|
7266
|
-
createDispatchWebhookOnTicket(slugs)
|
|
7826
|
+
createDispatchWebhookOnTicket(slugs),
|
|
7827
|
+
createApplyAutomationRules(slugs)
|
|
7267
7828
|
],
|
|
7268
7829
|
beforeDelete: [createCascadeDelete(slugs)]
|
|
7269
7830
|
},
|
|
@@ -7274,12 +7835,20 @@ function createTicketsCollection(slugs, options) {
|
|
|
7274
7835
|
return false;
|
|
7275
7836
|
},
|
|
7276
7837
|
read: async ({ req }) => {
|
|
7277
|
-
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
|
+
}
|
|
7278
7848
|
if (req.user?.collection === slugs.supportClients) {
|
|
7279
7849
|
let collabTicketIds = [];
|
|
7280
7850
|
try {
|
|
7281
|
-
const rows = await req.payload
|
|
7282
|
-
collection: "ticket-collaborators",
|
|
7851
|
+
const rows = await dbFind(req.payload, "ticket-collaborators", {
|
|
7283
7852
|
where: { client: { equals: req.user.id } },
|
|
7284
7853
|
limit: 1e3,
|
|
7285
7854
|
depth: 0,
|
|
@@ -7314,6 +7883,125 @@ function createTicketsCollection(slugs, options) {
|
|
|
7314
7883
|
};
|
|
7315
7884
|
}
|
|
7316
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
|
+
|
|
7317
8005
|
// src/collections/TicketMessages.ts
|
|
7318
8006
|
function createAssignAuthor(slugs) {
|
|
7319
8007
|
return async ({ data, operation, req }) => {
|
|
@@ -7376,29 +8064,46 @@ function createNotifyClient(slugs) {
|
|
|
7376
8064
|
const internalBcc = process.env.SUPPORT_CLIENT_BCC || void 0;
|
|
7377
8065
|
const rawContent = doc.bodyHtml ? emailRichContent(doc.bodyHtml) : emailQuote(doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body);
|
|
7378
8066
|
const replyPreview = (doc.body || "").replace(/\s+/g, " ").trim().slice(0, 90);
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
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
|
+
})();
|
|
7402
8107
|
} catch (err) {
|
|
7403
8108
|
console.error("[support] Failed to notify client:", err);
|
|
7404
8109
|
}
|
|
@@ -7436,8 +8141,7 @@ function createSyncTicketReplyToChat(slugs) {
|
|
|
7436
8141
|
if (!ticket?.chatSession) return doc;
|
|
7437
8142
|
if (doc.skipNotification) return doc;
|
|
7438
8143
|
const clientId = typeof ticket.client === "object" ? ticket.client.id : ticket.client;
|
|
7439
|
-
await payload.
|
|
7440
|
-
collection: slugs.chatMessages,
|
|
8144
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
7441
8145
|
data: {
|
|
7442
8146
|
session: ticket.chatSession,
|
|
7443
8147
|
client: clientId,
|
|
@@ -7554,6 +8258,57 @@ function createDispatchWebhookOnReply(slugs) {
|
|
|
7554
8258
|
return doc;
|
|
7555
8259
|
};
|
|
7556
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
|
+
}
|
|
7557
8312
|
function createTicketMessagesCollection(slugs, options) {
|
|
7558
8313
|
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
7559
8314
|
return {
|
|
@@ -7561,7 +8316,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7561
8316
|
labels: { singular: "Message", plural: "Messages" },
|
|
7562
8317
|
admin: { hidden: true, group: "Support", defaultColumns: ["ticket", "authorType", "createdAt"] },
|
|
7563
8318
|
fields: [
|
|
7564
|
-
{ 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" },
|
|
7565
8320
|
{ name: "body", type: "textarea", required: true, label: "Message" },
|
|
7566
8321
|
{ name: "bodyHtml", type: "textarea", label: "Message HTML", admin: { hidden: true } },
|
|
7567
8322
|
{
|
|
@@ -7570,6 +8325,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7570
8325
|
{
|
|
7571
8326
|
name: "authorType",
|
|
7572
8327
|
type: "select",
|
|
8328
|
+
index: true,
|
|
7573
8329
|
label: "Type d'auteur",
|
|
7574
8330
|
defaultValue: "admin",
|
|
7575
8331
|
options: [
|
|
@@ -7605,8 +8361,9 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7605
8361
|
},
|
|
7606
8362
|
{ name: "isInternal", type: "checkbox", defaultValue: false, label: "Note interne", admin: { position: "sidebar" } },
|
|
7607
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 } },
|
|
7608
8365
|
{ name: "skipNotification", type: "checkbox", defaultValue: false, label: "Sans notification", admin: { position: "sidebar", condition: (data) => data?.skipNotification === true } },
|
|
7609
|
-
{ name: "scheduledAt", type: "date", label: "Programme pour", admin: { date: { pickerAppearance: "dayAndTime" }, position: "sidebar", condition: (data) => !!data?.scheduledAt } },
|
|
8366
|
+
{ name: "scheduledAt", type: "date", index: true, label: "Programme pour", admin: { date: { pickerAppearance: "dayAndTime" }, position: "sidebar", condition: (data) => !!data?.scheduledAt } },
|
|
7610
8367
|
{ name: "scheduledSent", type: "checkbox", defaultValue: false, admin: { hidden: true } },
|
|
7611
8368
|
{ name: "editedAt", type: "date", label: "Modifie le", admin: { hidden: true } },
|
|
7612
8369
|
{ name: "deletedAt", type: "date", label: "Supprime le", admin: { hidden: true } },
|
|
@@ -7615,7 +8372,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7615
8372
|
{ name: "emailOpenedAt", type: "date", label: "Email ouvert le", admin: { hidden: true } }
|
|
7616
8373
|
],
|
|
7617
8374
|
hooks: {
|
|
7618
|
-
beforeChange: [createAssignAuthor(slugs)],
|
|
8375
|
+
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
7619
8376
|
afterChange: [
|
|
7620
8377
|
createAutoUpdateStatus(slugs),
|
|
7621
8378
|
createNotifyClient(slugs),
|
|
@@ -7624,17 +8381,19 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7624
8381
|
createSyncTicketReplyToChat(slugs),
|
|
7625
8382
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
7626
8383
|
createFireMessageWebhooks(slugs),
|
|
7627
|
-
createDispatchWebhookOnReply(slugs)
|
|
8384
|
+
createDispatchWebhookOnReply(slugs),
|
|
8385
|
+
createNotifyMentions(slugs)
|
|
7628
8386
|
]
|
|
7629
8387
|
},
|
|
7630
8388
|
access: {
|
|
7631
8389
|
create: ({ req }) => req.user?.collection === slugs.users || req.user?.collection === slugs.supportClients,
|
|
7632
|
-
read: ({ req }) => {
|
|
8390
|
+
read: async ({ req }) => {
|
|
7633
8391
|
if (req.user?.collection === slugs.users) return true;
|
|
7634
8392
|
if (req.user?.collection === slugs.supportClients) {
|
|
8393
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
7635
8394
|
return {
|
|
7636
8395
|
and: [
|
|
7637
|
-
{
|
|
8396
|
+
{ ticket: { in: ids.length > 0 ? ids : [-1] } },
|
|
7638
8397
|
{ isInternal: { equals: false } },
|
|
7639
8398
|
{ or: [{ scheduledAt: { exists: false } }, { scheduledSent: { equals: true } }] }
|
|
7640
8399
|
]
|
|
@@ -7654,8 +8413,6 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
7654
8413
|
timestamps: true
|
|
7655
8414
|
};
|
|
7656
8415
|
}
|
|
7657
|
-
|
|
7658
|
-
// src/collections/SupportClients.ts
|
|
7659
8416
|
function createSendInvitationOnCreate(slugs) {
|
|
7660
8417
|
return async ({ doc, operation, req }) => {
|
|
7661
8418
|
if (operation !== "create") return doc;
|
|
@@ -7695,6 +8452,26 @@ function createSendInvitationOnCreate(slugs) {
|
|
|
7695
8452
|
return doc;
|
|
7696
8453
|
};
|
|
7697
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
|
+
}
|
|
7698
8475
|
function createSupportClientsCollection(slugs) {
|
|
7699
8476
|
return {
|
|
7700
8477
|
slug: slugs.supportClients,
|
|
@@ -7823,6 +8600,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7823
8600
|
name: "opportunities",
|
|
7824
8601
|
type: "textarea",
|
|
7825
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
|
+
},
|
|
7826
8607
|
admin: {
|
|
7827
8608
|
description: "Notes commerciales (visible uniquement par les admins)"
|
|
7828
8609
|
}
|
|
@@ -7866,6 +8647,19 @@ function createSupportClientsCollection(slugs) {
|
|
|
7866
8647
|
type: "date",
|
|
7867
8648
|
admin: { hidden: true }
|
|
7868
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
|
+
},
|
|
7869
8663
|
{
|
|
7870
8664
|
name: "notifyOnReply",
|
|
7871
8665
|
type: "checkbox",
|
|
@@ -7932,6 +8726,10 @@ function createSupportClientsCollection(slugs) {
|
|
|
7932
8726
|
name: "notes",
|
|
7933
8727
|
type: "textarea",
|
|
7934
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
|
+
},
|
|
7935
8733
|
admin: {
|
|
7936
8734
|
description: "Visible uniquement par les admins",
|
|
7937
8735
|
position: "sidebar"
|
|
@@ -7939,6 +8737,7 @@ function createSupportClientsCollection(slugs) {
|
|
|
7939
8737
|
}
|
|
7940
8738
|
],
|
|
7941
8739
|
hooks: {
|
|
8740
|
+
beforeLogin: [createEnforce2FA(slugs)],
|
|
7942
8741
|
afterChange: [createSendInvitationOnCreate(slugs)]
|
|
7943
8742
|
},
|
|
7944
8743
|
access: {
|
|
@@ -8152,6 +8951,7 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
8152
8951
|
type: "relationship",
|
|
8153
8952
|
relationTo: slugs.tickets,
|
|
8154
8953
|
required: true,
|
|
8954
|
+
index: true,
|
|
8155
8955
|
label: "Ticket"
|
|
8156
8956
|
},
|
|
8157
8957
|
{
|
|
@@ -8197,10 +8997,11 @@ function createTicketActivityLogCollection(slugs) {
|
|
|
8197
8997
|
access: {
|
|
8198
8998
|
create: () => false,
|
|
8199
8999
|
// Created only by hooks via overrideAccess
|
|
8200
|
-
read: ({ req }) => {
|
|
9000
|
+
read: async ({ req }) => {
|
|
8201
9001
|
if (req.user?.collection === slugs.users) return true;
|
|
8202
9002
|
if (req.user?.collection === slugs.supportClients) {
|
|
8203
|
-
|
|
9003
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
9004
|
+
return { ticket: { in: ids.length > 0 ? ids : [-1] } };
|
|
8204
9005
|
}
|
|
8205
9006
|
return false;
|
|
8206
9007
|
},
|
|
@@ -8269,12 +9070,21 @@ function createSatisfactionSurveysCollection(slugs) {
|
|
|
8269
9070
|
{
|
|
8270
9071
|
name: "rating",
|
|
8271
9072
|
type: "number",
|
|
8272
|
-
required: true,
|
|
8273
9073
|
min: 1,
|
|
8274
9074
|
max: 5,
|
|
8275
9075
|
label: "Note (1-5)",
|
|
8276
9076
|
admin: {
|
|
8277
|
-
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"
|
|
8278
9088
|
}
|
|
8279
9089
|
},
|
|
8280
9090
|
{
|
|
@@ -9520,6 +10330,124 @@ function createTicketCollaboratorsCollection(slugs) {
|
|
|
9520
10330
|
};
|
|
9521
10331
|
}
|
|
9522
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
|
+
|
|
9523
10451
|
// src/plugin.ts
|
|
9524
10452
|
function viewConfig(component, path) {
|
|
9525
10453
|
return { Component: component, path };
|
|
@@ -9555,7 +10483,10 @@ function supportPlugin(config) {
|
|
|
9555
10483
|
createSatisfactionSurveysCollection(slugs),
|
|
9556
10484
|
createKnowledgeBaseCollection(slugs),
|
|
9557
10485
|
createTicketFeedbackCollection(slugs, { notificationSlug: config?.notificationSlug }),
|
|
9558
|
-
createTicketCollaboratorsCollection(slugs)
|
|
10486
|
+
createTicketCollaboratorsCollection(slugs),
|
|
10487
|
+
createNotificationQueueCollection(slugs),
|
|
10488
|
+
createAutomationRulesCollection(slugs),
|
|
10489
|
+
createSupportTeamCollection(slugs)
|
|
9559
10490
|
];
|
|
9560
10491
|
if (features.authLogs !== false) supportCollections.push(createAuthLogsCollection(slugs));
|
|
9561
10492
|
if (features.timeTracking !== false) supportCollections.push(createTimeEntriesCollection(slugs));
|