@consilioweb/payload-support 0.15.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +347 -435
- package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
- package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
- package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
- package/dist/components/TicketConversation/SkeletonText.js +20 -0
- package/dist/components/TicketConversation/components/ActionPanels.cjs +72 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +73 -2
- package/dist/components/TicketConversation/components/QuickActions.cjs +22 -1
- package/dist/components/TicketConversation/components/QuickActions.js +22 -1
- package/dist/components/TicketConversation/constants.cjs +27 -0
- package/dist/components/TicketConversation/constants.js +26 -1
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.cjs +59 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.js +59 -0
- package/dist/components/TicketConversation/index.cjs +42 -149
- package/dist/components/TicketConversation/index.js +37 -144
- package/dist/index.cjs +1835 -741
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1833 -741
- package/dist/views/BillingView/client.cjs +40 -54
- package/dist/views/BillingView/client.js +40 -54
- package/dist/views/ChatView/client.cjs +4 -3
- package/dist/views/ChatView/client.js +4 -3
- package/dist/views/CrmView/client.cjs +7 -7
- package/dist/views/CrmView/client.js +7 -7
- package/dist/views/EmailTrackingView/client.cjs +6 -5
- package/dist/views/EmailTrackingView/client.js +6 -5
- package/dist/views/ImportConversationView/client.cjs +4 -4
- package/dist/views/ImportConversationView/client.js +4 -4
- package/dist/views/LogsView/client.cjs +3 -2
- package/dist/views/LogsView/client.js +3 -2
- package/dist/views/NewTicketView/client.cjs +1 -1
- package/dist/views/NewTicketView/client.js +1 -1
- package/dist/views/PendingEmailsView/client.cjs +7 -6
- package/dist/views/PendingEmailsView/client.js +7 -6
- package/dist/views/SupportDashboardView/client.cjs +19 -22
- package/dist/views/SupportDashboardView/client.js +19 -22
- package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
- package/dist/views/TicketDetailView/NextActionItem.js +34 -0
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
- package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
- package/dist/views/TicketDetailView/client.cjs +59 -190
- package/dist/views/TicketDetailView/client.js +52 -183
- package/dist/views/TicketDetailView/constants.cjs +20 -0
- package/dist/views/TicketDetailView/constants.js +16 -0
- package/dist/views/TicketDetailView/helpers.cjs +23 -0
- package/dist/views/TicketDetailView/helpers.js +20 -0
- package/dist/views/TicketDetailView/types.cjs +2 -0
- package/dist/views/TicketDetailView/types.js +1 -0
- package/dist/views/TicketInboxView/client.cjs +14 -10
- package/dist/views/TicketInboxView/client.js +14 -10
- package/dist/views/TicketingSettingsView/client.cjs +101 -96
- package/dist/views/TicketingSettingsView/client.js +92 -88
- package/dist/views/TimeDashboardView/client.cjs +2 -2
- package/dist/views/TimeDashboardView/client.js +2 -2
- package/dist/views/shared/StatusPill.cjs +23 -12
- package/dist/views/shared/StatusPill.js +23 -12
- package/dist/views/shared/dateLocale.cjs +5 -0
- package/dist/views/shared/dateLocale.js +3 -0
- package/package.json +23 -15
- package/src/__tests__/generateTrackingToken.test.ts +53 -0
- package/src/__tests__/integration/ai-agent.test.ts +46 -0
- package/src/__tests__/integration/auth.test.ts +43 -0
- package/src/__tests__/integration/automation-rules.test.ts +68 -0
- package/src/__tests__/integration/buildTestPayload.ts +52 -0
- package/src/__tests__/integration/channels.test.ts +52 -0
- package/src/__tests__/integration/core-behaviors.test.ts +58 -0
- package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
- package/src/__tests__/integration/digest.test.ts +48 -0
- package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
- package/src/__tests__/integration/isolation.test.ts +81 -0
- package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
- package/src/__tests__/integration/nps.test.ts +37 -0
- package/src/__tests__/integration/sanitization.test.ts +35 -0
- package/src/__tests__/integration/sla-pause.test.ts +39 -0
- package/src/__tests__/integration/smoke.test.ts +26 -0
- package/src/__tests__/integration/snooze.test.ts +68 -0
- package/src/__tests__/integration/teams.test.ts +38 -0
- package/src/__tests__/rateLimiter.test.ts +89 -0
- package/src/__tests__/sanitizeHtml.test.ts +165 -0
- package/src/__tests__/sla.test.ts +258 -0
- package/src/collections/AutomationRules.ts +66 -0
- package/src/collections/NotificationQueue.ts +30 -0
- package/src/collections/SatisfactionSurveys.ts +11 -2
- package/src/collections/SupportClients.ts +58 -1
- package/src/collections/SupportTeam.ts +27 -0
- package/src/collections/TicketActivityLog.ts +9 -4
- package/src/collections/TicketMessages.ts +137 -31
- package/src/collections/Tickets.ts +31 -72
- package/src/collections/index.ts +3 -0
- package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
- package/src/components/TicketConversation/SkeletonText.tsx +23 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +82 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +25 -0
- package/src/components/TicketConversation/constants.ts +29 -0
- package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +51 -0
- package/src/components/TicketConversation/index.tsx +29 -157
- package/src/endpoints/admin-chat-stream.ts +3 -4
- package/src/endpoints/admin-chat.ts +11 -20
- package/src/endpoints/admin-stats.ts +70 -32
- package/src/endpoints/ai-agent.ts +36 -0
- package/src/endpoints/apply-macro.ts +8 -14
- package/src/endpoints/auth-2fa.ts +12 -12
- package/src/endpoints/auto-close.ts +38 -18
- package/src/endpoints/billing.ts +17 -4
- package/src/endpoints/bulk-action.ts +7 -12
- package/src/endpoints/channels.ts +89 -0
- package/src/endpoints/chat-stream.ts +3 -4
- package/src/endpoints/chat.ts +7 -12
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/client-intelligence.ts +4 -4
- package/src/endpoints/delete-account.ts +9 -16
- package/src/endpoints/email-stats.ts +2 -2
- package/src/endpoints/escalate.ts +2 -2
- package/src/endpoints/export-csv.ts +2 -2
- package/src/endpoints/export-data.ts +5 -8
- package/src/endpoints/import-conversation.ts +6 -10
- package/src/endpoints/index.ts +24 -2
- package/src/endpoints/invite-collaborator.ts +30 -15
- package/src/endpoints/invoice.ts +127 -0
- package/src/endpoints/kb-search.ts +2 -2
- package/src/endpoints/login.ts +12 -4
- package/src/endpoints/merge-clients.ts +14 -19
- package/src/endpoints/merge-tickets.ts +11 -18
- package/src/endpoints/oauth-google.ts +86 -39
- package/src/endpoints/pending-emails-process.ts +15 -25
- package/src/endpoints/process-digests.ts +93 -0
- package/src/endpoints/process-scheduled.ts +6 -10
- package/src/endpoints/process-snooze.ts +78 -0
- package/src/endpoints/purge-logs.ts +2 -2
- package/src/endpoints/resend-notification.ts +3 -4
- package/src/endpoints/round-robin-config.ts +3 -4
- package/src/endpoints/satisfaction.ts +21 -14
- package/src/endpoints/search.ts +5 -8
- package/src/endpoints/seed-kb.ts +3 -4
- package/src/endpoints/send-reminder.ts +186 -0
- package/src/endpoints/settings.ts +5 -2
- package/src/endpoints/signature.ts +3 -4
- package/src/endpoints/sla-check.ts +2 -2
- package/src/endpoints/split-ticket.ts +7 -12
- package/src/endpoints/statuses.ts +2 -2
- package/src/endpoints/ticket-feedback.ts +4 -6
- package/src/endpoints/ticket-synthesis.ts +2 -2
- package/src/endpoints/track-open.ts +16 -28
- package/src/endpoints/transfer-ticket.ts +21 -9
- package/src/endpoints/user-prefs.ts +3 -4
- package/src/hooks/automationRules.ts +84 -0
- package/src/hooks/checkSLA.ts +48 -16
- package/src/plugin.ts +6 -0
- package/src/portal/LiveChat.tsx +2 -63
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
- package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
- package/src/portal/auth/tickets/detail/page.tsx +2 -2
- package/src/portal/helpers.ts +46 -0
- package/src/portal/login/page.tsx +3 -8
- package/src/portal/types.ts +15 -0
- package/src/types/lucide-react.d.ts +34 -0
- package/src/types/payload-config.d.ts +10 -0
- package/src/utils/adminNotification.ts +2 -2
- package/src/utils/aiAgent.ts +147 -0
- package/src/utils/auth.ts +5 -4
- package/src/utils/channels.ts +36 -0
- package/src/utils/db.ts +73 -0
- package/src/utils/emailTemplate.ts +8 -1
- package/src/utils/fireWebhooks.ts +4 -6
- package/src/utils/generateTicketSynthesis.ts +7 -12
- package/src/utils/invoicePdf.ts +88 -0
- package/src/utils/notificationQueue.ts +36 -0
- package/src/utils/readSettings.ts +22 -6
- package/src/utils/sanitizeHtml.ts +57 -0
- package/src/utils/slugs.ts +6 -0
- package/src/utils/teamAccess.ts +27 -0
- package/src/utils/ticketAccess.ts +53 -0
- package/src/utils/webhookDispatcher.ts +4 -6
- package/src/views/BillingView/client.tsx +49 -37
- package/src/views/ChatView/client.tsx +4 -3
- package/src/views/CrmView/client.tsx +7 -7
- package/src/views/EmailTrackingView/client.tsx +7 -6
- package/src/views/ImportConversationView/client.tsx +4 -4
- package/src/views/LogsView/client.tsx +3 -2
- package/src/views/NewTicketView/client.tsx +1 -1
- package/src/views/PendingEmailsView/client.tsx +7 -6
- package/src/views/SupportDashboardView/client.tsx +21 -21
- package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
- package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
- package/src/views/TicketDetailView/client.tsx +54 -179
- package/src/views/TicketDetailView/constants.ts +23 -0
- package/src/views/TicketDetailView/helpers.ts +17 -0
- package/src/views/TicketDetailView/types.ts +10 -0
- package/src/views/TicketInboxView/client.tsx +16 -10
- package/src/views/TicketingSettingsView/client.tsx +90 -87
- package/src/views/TimeDashboardView/client.tsx +2 -2
- package/src/views/shared/StatusPill.tsx +24 -12
- package/src/views/shared/dateLocale.ts +5 -0
- package/src/views/shared/locales/en.json +708 -0
- package/src/views/shared/locales/fr.json +708 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireClient, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFindByID, dbFind, dbCreate } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* POST /api/support/satisfaction
|
|
@@ -17,13 +18,21 @@ export function createSatisfactionEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
17
18
|
requireClient(req, slugs)
|
|
18
19
|
|
|
19
20
|
const body = await req.json!()
|
|
20
|
-
const { ticketId, rating, comment } = body
|
|
21
|
+
const { ticketId, rating, nps, comment } = body
|
|
21
22
|
|
|
22
|
-
if (!ticketId
|
|
23
|
-
return Response.json(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
if (!ticketId) {
|
|
24
|
+
return Response.json({ error: 'ticketId est requis.' }, { status: 400 })
|
|
25
|
+
}
|
|
26
|
+
const hasRating = rating !== undefined && rating !== null
|
|
27
|
+
const hasNps = nps !== undefined && nps !== null
|
|
28
|
+
if (!hasRating && !hasNps) {
|
|
29
|
+
return Response.json({ error: 'Un rating (1-5) et/ou un nps (0-10) est requis.' }, { status: 400 })
|
|
30
|
+
}
|
|
31
|
+
if (hasRating && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
32
|
+
return Response.json({ error: 'rating doit être un entier 1-5.' }, { status: 400 })
|
|
33
|
+
}
|
|
34
|
+
if (hasNps && (!Number.isInteger(nps) || nps < 0 || nps > 10)) {
|
|
35
|
+
return Response.json({ error: 'nps doit être un entier 0-10.' }, { status: 400 })
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
if (comment && typeof comment === 'string' && comment.length > 5000) {
|
|
@@ -34,13 +43,12 @@ export function createSatisfactionEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
// Verify ticket belongs to client and is resolved
|
|
37
|
-
const ticket = await payload.
|
|
38
|
-
collection: slugs.tickets as any,
|
|
46
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
39
47
|
id: ticketId,
|
|
40
48
|
depth: 0,
|
|
41
49
|
overrideAccess: false,
|
|
42
50
|
user: req.user,
|
|
43
|
-
})
|
|
51
|
+
})
|
|
44
52
|
|
|
45
53
|
if (!ticket) {
|
|
46
54
|
return Response.json({ error: 'Ticket introuvable.' }, { status: 404 })
|
|
@@ -54,8 +62,7 @@ export function createSatisfactionEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
// Check if survey already exists
|
|
57
|
-
const existing = await payload.
|
|
58
|
-
collection: slugs.satisfactionSurveys as any,
|
|
65
|
+
const existing = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
59
66
|
where: { ticket: { equals: ticketId } },
|
|
60
67
|
limit: 1,
|
|
61
68
|
depth: 0,
|
|
@@ -69,13 +76,13 @@ export function createSatisfactionEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
69
76
|
)
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
const survey = await payload.
|
|
73
|
-
collection: slugs.satisfactionSurveys as any,
|
|
79
|
+
const survey = await dbCreate(payload, slugs.satisfactionSurveys, {
|
|
74
80
|
data: {
|
|
75
81
|
source: 'ticket',
|
|
76
82
|
ticket: ticketId,
|
|
77
83
|
client: req.user.id,
|
|
78
|
-
rating: Math.round(rating),
|
|
84
|
+
...(hasRating ? { rating: Math.round(rating) } : {}),
|
|
85
|
+
...(hasNps ? { nps: Math.round(nps) } : {}),
|
|
79
86
|
...(comment ? { comment: comment.trim() } : {}),
|
|
80
87
|
},
|
|
81
88
|
overrideAccess: true,
|
package/src/endpoints/search.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFind } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* GET /api/support/search?q=term
|
|
@@ -24,8 +25,7 @@ export function createSearchEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const [ticketsRes, clientsRes, messagesRes, articlesRes] = await Promise.all([
|
|
27
|
-
payload.
|
|
28
|
-
collection: slugs.tickets as any,
|
|
28
|
+
dbFind(payload, slugs.tickets, {
|
|
29
29
|
where: {
|
|
30
30
|
or: [
|
|
31
31
|
{ ticketNumber: { contains: q } },
|
|
@@ -37,8 +37,7 @@ export function createSearchEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
37
37
|
depth: 1,
|
|
38
38
|
overrideAccess: true,
|
|
39
39
|
}),
|
|
40
|
-
payload.
|
|
41
|
-
collection: slugs.supportClients as any,
|
|
40
|
+
dbFind(payload, slugs.supportClients, {
|
|
42
41
|
where: {
|
|
43
42
|
or: [
|
|
44
43
|
{ firstName: { contains: q } },
|
|
@@ -51,16 +50,14 @@ export function createSearchEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
51
50
|
depth: 0,
|
|
52
51
|
overrideAccess: true,
|
|
53
52
|
}),
|
|
54
|
-
payload.
|
|
55
|
-
collection: slugs.ticketMessages as any,
|
|
53
|
+
dbFind(payload, slugs.ticketMessages, {
|
|
56
54
|
where: { body: { contains: q } },
|
|
57
55
|
sort: '-createdAt',
|
|
58
56
|
limit: 5,
|
|
59
57
|
depth: 1,
|
|
60
58
|
overrideAccess: true,
|
|
61
59
|
}),
|
|
62
|
-
payload.
|
|
63
|
-
collection: slugs.knowledgeBase as any,
|
|
60
|
+
dbFind(payload, slugs.knowledgeBase, {
|
|
64
61
|
where: { title: { contains: q } },
|
|
65
62
|
limit: 5,
|
|
66
63
|
depth: 0,
|
package/src/endpoints/seed-kb.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFind, dbCreate } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
const KB_ARTICLES = [
|
|
6
7
|
{
|
|
@@ -95,8 +96,7 @@ export function createSeedKbEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
95
96
|
let skipped = 0
|
|
96
97
|
|
|
97
98
|
for (const article of KB_ARTICLES) {
|
|
98
|
-
const existing = await payload.
|
|
99
|
-
collection: slugs.knowledgeBase as any,
|
|
99
|
+
const existing = await dbFind(payload, slugs.knowledgeBase, {
|
|
100
100
|
where: { slug: { equals: article.slug } },
|
|
101
101
|
limit: 1,
|
|
102
102
|
depth: 0,
|
|
@@ -126,8 +126,7 @@ export function createSeedKbEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
126
126
|
},
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
await payload.
|
|
130
|
-
collection: slugs.knowledgeBase as any,
|
|
129
|
+
await dbCreate(payload, slugs.knowledgeBase, {
|
|
131
130
|
data: {
|
|
132
131
|
title: article.title,
|
|
133
132
|
slug: article.slug,
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload'
|
|
2
|
+
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { RateLimiter } from '../utils/rateLimiter'
|
|
5
|
+
import { escapeHtml, emailWrapper, emailParagraph, emailButton } from '../utils/emailTemplate'
|
|
6
|
+
import { readSupportSettings } from '../utils/readSettings'
|
|
7
|
+
import { dbFindByID, dbFind, dbCreate, dbUpdate } from '../utils/db'
|
|
8
|
+
|
|
9
|
+
// Manual reminders are a deliberate admin action, so the cap is generous —
|
|
10
|
+
// it only exists to stop a runaway script, not normal usage.
|
|
11
|
+
const reminderLimiter = new RateLimiter(60 * 60 * 1000, 30) // 30 per hour per admin
|
|
12
|
+
|
|
13
|
+
const MIN_HOURS = 1
|
|
14
|
+
const MAX_HOURS = 24 * 30 // 30 days
|
|
15
|
+
|
|
16
|
+
function formatFr(date: Date, withTime: boolean): string {
|
|
17
|
+
return date.toLocaleString('fr-FR', {
|
|
18
|
+
day: 'numeric',
|
|
19
|
+
month: 'long',
|
|
20
|
+
year: 'numeric',
|
|
21
|
+
...(withTime ? { hour: '2-digit', minute: '2-digit' } : {}),
|
|
22
|
+
timeZone: 'Europe/Paris',
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* POST /api/support/send-reminder
|
|
28
|
+
*
|
|
29
|
+
* Manually nudge a client whose ticket is awaiting their reply: sends a generic
|
|
30
|
+
* "your ticket will be closed if you don't respond" email, drops an internal
|
|
31
|
+
* note in the thread, and arms the ticket for automatic closure.
|
|
32
|
+
*
|
|
33
|
+
* Arming = set `status: waiting_client`, `autoCloseRemindedAt: now` (so the
|
|
34
|
+
* day-based cron reminder skips it) and `autoCloseScheduledAt: now + hours`.
|
|
35
|
+
* The existing `/api/support/auto-close` cron then resolves the ticket once the
|
|
36
|
+
* deadline passes — UNLESS the client replies first, which flips the status back
|
|
37
|
+
* to `open` and clears `autoCloseScheduledAt` (see TicketMessages auto-update hook).
|
|
38
|
+
*
|
|
39
|
+
* Admin-only, rate-limited.
|
|
40
|
+
*/
|
|
41
|
+
export function createSendReminderEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
42
|
+
return {
|
|
43
|
+
path: '/support/send-reminder',
|
|
44
|
+
method: 'post',
|
|
45
|
+
handler: async (req) => {
|
|
46
|
+
try {
|
|
47
|
+
const payload = req.payload
|
|
48
|
+
|
|
49
|
+
requireAdmin(req, slugs)
|
|
50
|
+
|
|
51
|
+
if (reminderLimiter.check(String(req.user.id))) {
|
|
52
|
+
return Response.json(
|
|
53
|
+
{ error: 'Trop de relances. Réessayez dans une heure.' },
|
|
54
|
+
{ status: 429 },
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let body: { ticketId?: string | number; hours?: number }
|
|
59
|
+
try {
|
|
60
|
+
body = await req.json!()
|
|
61
|
+
} catch {
|
|
62
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { ticketId } = body
|
|
66
|
+
if (!ticketId) {
|
|
67
|
+
return Response.json({ error: 'ticketId requis' }, { status: 400 })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rawHours = Number(body.hours)
|
|
71
|
+
const hours = Number.isFinite(rawHours)
|
|
72
|
+
? Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.round(rawHours)))
|
|
73
|
+
: 24
|
|
74
|
+
|
|
75
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
76
|
+
id: ticketId,
|
|
77
|
+
depth: 1,
|
|
78
|
+
overrideAccess: true,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (!ticket) {
|
|
82
|
+
return Response.json({ error: 'Ticket introuvable' }, { status: 404 })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const client = typeof ticket.client === 'object' ? ticket.client : null
|
|
86
|
+
if (!client?.email) {
|
|
87
|
+
return Response.json({ error: 'Client sans email' }, { status: 400 })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const now = new Date()
|
|
91
|
+
const deadline = new Date(now.getTime() + hours * 60 * 60 * 1000)
|
|
92
|
+
|
|
93
|
+
const settings = await readSupportSettings(payload)
|
|
94
|
+
const ticketNumber = ticket.ticketNumber || 'TK-????'
|
|
95
|
+
const subject = ticket.subject || 'Support'
|
|
96
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || ''
|
|
97
|
+
const portalUrl = `${baseUrl}/support/tickets/${ticketId}`
|
|
98
|
+
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_REPLY_TO || ''
|
|
99
|
+
|
|
100
|
+
// "En attente depuis le …" — anchored on our last public reply (that's
|
|
101
|
+
// what the client owes a response to), falling back to the first
|
|
102
|
+
// response then to ticket creation.
|
|
103
|
+
let waitingSince: string | undefined = ticket.firstResponseAt || ticket.createdAt
|
|
104
|
+
try {
|
|
105
|
+
const lastAdminMsg = await dbFind(payload, slugs.ticketMessages, {
|
|
106
|
+
where: {
|
|
107
|
+
and: [
|
|
108
|
+
{ ticket: { equals: ticketId } },
|
|
109
|
+
{ authorType: { equals: 'admin' } },
|
|
110
|
+
{ isInternal: { equals: false } },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
sort: '-createdAt',
|
|
114
|
+
limit: 1,
|
|
115
|
+
depth: 0,
|
|
116
|
+
overrideAccess: true,
|
|
117
|
+
})
|
|
118
|
+
if (lastAdminMsg.docs.length > 0 && lastAdminMsg.docs[0].createdAt) {
|
|
119
|
+
waitingSince = lastAdminMsg.docs[0].createdAt as string
|
|
120
|
+
}
|
|
121
|
+
} catch { /* fallback already set */ }
|
|
122
|
+
|
|
123
|
+
const sinceLabel = waitingSince ? formatFr(new Date(waitingSince), false) : null
|
|
124
|
+
const deadlineLabel = formatFr(deadline, true)
|
|
125
|
+
|
|
126
|
+
await payload.sendEmail({
|
|
127
|
+
to: client.email,
|
|
128
|
+
...(replyTo ? { replyTo } : {}),
|
|
129
|
+
subject: `Rappel : [${ticketNumber}] ${subject} — votre réponse est attendue`,
|
|
130
|
+
html: emailWrapper(`Votre ticket attend votre réponse`, [
|
|
131
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || '')}</strong>,`),
|
|
132
|
+
emailParagraph(
|
|
133
|
+
`Votre ticket <strong>${escapeHtml(ticketNumber)}</strong> — <em>${escapeHtml(String(subject))}</em> — est en attente de votre réponse${sinceLabel ? ` depuis le ${escapeHtml(sinceLabel)}` : ''}.`,
|
|
134
|
+
),
|
|
135
|
+
emailParagraph(
|
|
136
|
+
`<strong>Sans retour de votre part, ce ticket sera automatiquement clôturé le ${escapeHtml(deadlineLabel)}.</strong>`,
|
|
137
|
+
),
|
|
138
|
+
emailParagraph(
|
|
139
|
+
`Si vous avez encore besoin d'assistance, il vous suffit de répondre à ce message — votre réponse maintiendra le ticket ouvert.`,
|
|
140
|
+
),
|
|
141
|
+
emailButton('Répondre au ticket', portalUrl, 'primary'),
|
|
142
|
+
].join(''), {
|
|
143
|
+
kind: 'alert',
|
|
144
|
+
preheader: `Sans réponse de votre part, votre ticket ${ticketNumber} sera clôturé le ${deadlineLabel}.`,
|
|
145
|
+
}),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// Internal note for the conversation timeline (never sent to the client).
|
|
149
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
150
|
+
data: {
|
|
151
|
+
ticket: ticketId,
|
|
152
|
+
body: `Relance envoyée à ${client.email}. Fermeture automatique programmée le ${deadlineLabel} sans réponse du client.`,
|
|
153
|
+
authorType: 'admin',
|
|
154
|
+
isInternal: true,
|
|
155
|
+
skipNotification: true,
|
|
156
|
+
},
|
|
157
|
+
overrideAccess: true,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Arm the ticket. `autoCloseRemindedAt: now` keeps the day-based cron
|
|
161
|
+
// reminder from firing a second time; `autoCloseScheduledAt` is the hard
|
|
162
|
+
// 24h-style deadline the cron close step honours.
|
|
163
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
164
|
+
id: ticketId,
|
|
165
|
+
data: {
|
|
166
|
+
status: 'waiting_client',
|
|
167
|
+
autoCloseRemindedAt: now.toISOString(),
|
|
168
|
+
autoCloseScheduledAt: deadline.toISOString(),
|
|
169
|
+
},
|
|
170
|
+
overrideAccess: true,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return Response.json({
|
|
174
|
+
success: true,
|
|
175
|
+
sentTo: client.email,
|
|
176
|
+
scheduledCloseAt: deadline.toISOString(),
|
|
177
|
+
})
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const authResponse = handleAuthError(error)
|
|
180
|
+
if (authResponse) return authResponse
|
|
181
|
+
console.error('[send-reminder] Error:', error)
|
|
182
|
+
return Response.json({ error: 'Erreur interne' }, { status: 500 })
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { invalidateSupportSettingsCache } from '../utils/readSettings'
|
|
5
|
+
import { dbFind } from '../utils/db'
|
|
4
6
|
|
|
5
7
|
const PREF_KEY = 'support-settings'
|
|
6
8
|
|
|
@@ -63,8 +65,7 @@ export function createSettingsGetEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
63
65
|
|
|
64
66
|
requireAdmin(req, slugs)
|
|
65
67
|
|
|
66
|
-
const prefs = await payload
|
|
67
|
-
collection: 'payload-preferences' as any,
|
|
68
|
+
const prefs = await dbFind(payload, 'payload-preferences', {
|
|
68
69
|
where: { key: { equals: PREF_KEY } },
|
|
69
70
|
limit: 1,
|
|
70
71
|
depth: 0,
|
|
@@ -132,6 +133,8 @@ export function createSettingsPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
132
133
|
},
|
|
133
134
|
})
|
|
134
135
|
|
|
136
|
+
invalidateSupportSettingsCache()
|
|
137
|
+
|
|
135
138
|
return Response.json(merged)
|
|
136
139
|
} catch (error) {
|
|
137
140
|
const authResponse = handleAuthError(error)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFind } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
const PREF_KEY = 'email-signature'
|
|
6
7
|
|
|
@@ -17,8 +18,7 @@ export function createSignatureGetEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
17
18
|
|
|
18
19
|
requireAdmin(req, slugs)
|
|
19
20
|
|
|
20
|
-
const prefs = await payload
|
|
21
|
-
collection: 'payload-preferences' as any,
|
|
21
|
+
const prefs = await dbFind(payload, 'payload-preferences', {
|
|
22
22
|
where: { key: { equals: `${PREF_KEY}-${req.user.id}` } },
|
|
23
23
|
limit: 1,
|
|
24
24
|
depth: 0,
|
|
@@ -56,8 +56,7 @@ export function createSignaturePostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
56
56
|
const { signature } = (await req.json!()) as { signature: string }
|
|
57
57
|
const key = `${PREF_KEY}-${req.user.id}`
|
|
58
58
|
|
|
59
|
-
const existing = await payload
|
|
60
|
-
collection: 'payload-preferences' as any,
|
|
59
|
+
const existing = await dbFind(payload, 'payload-preferences', {
|
|
61
60
|
where: { key: { equals: key } },
|
|
62
61
|
limit: 1,
|
|
63
62
|
depth: 0,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFind } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* GET /api/support/sla-check
|
|
@@ -19,8 +20,7 @@ export function createSlaCheckEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
19
20
|
const now = new Date()
|
|
20
21
|
const nowISO = now.toISOString()
|
|
21
22
|
|
|
22
|
-
const { docs: tickets } = await payload.
|
|
23
|
-
collection: slugs.tickets as any,
|
|
23
|
+
const { docs: tickets } = await dbFind(payload, slugs.tickets, {
|
|
24
24
|
where: {
|
|
25
25
|
and: [
|
|
26
26
|
{ status: { in: ['open', 'waiting_client'] } },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFindByID, dbCreate, dbUpdate } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* POST /api/support/split-ticket
|
|
@@ -21,8 +22,7 @@ export function createSplitTicketEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
21
22
|
return Response.json({ error: 'messageId required' }, { status: 400 })
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const message = await payload.
|
|
25
|
-
collection: slugs.ticketMessages as any,
|
|
25
|
+
const message = await dbFindByID(payload, slugs.ticketMessages, {
|
|
26
26
|
id: messageId,
|
|
27
27
|
depth: 1,
|
|
28
28
|
overrideAccess: true,
|
|
@@ -42,8 +42,7 @@ export function createSplitTicketEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
42
42
|
: sourceTicket.client
|
|
43
43
|
|
|
44
44
|
// Create new ticket
|
|
45
|
-
const newTicket = await payload.
|
|
46
|
-
collection: slugs.tickets as any,
|
|
45
|
+
const newTicket = await dbCreate(payload, slugs.tickets, {
|
|
47
46
|
data: {
|
|
48
47
|
subject: subject || `Split: ${sourceTicket.subject}`,
|
|
49
48
|
client: clientId,
|
|
@@ -61,8 +60,7 @@ export function createSplitTicketEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
61
60
|
file: typeof a.file === 'object' ? a.file.id : a.file,
|
|
62
61
|
})) || []
|
|
63
62
|
|
|
64
|
-
await payload.
|
|
65
|
-
collection: slugs.ticketMessages as any,
|
|
63
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
66
64
|
data: {
|
|
67
65
|
ticket: newTicket.id,
|
|
68
66
|
body: message.body,
|
|
@@ -77,8 +75,7 @@ export function createSplitTicketEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
77
75
|
})
|
|
78
76
|
|
|
79
77
|
// Add system note on source ticket
|
|
80
|
-
await payload.
|
|
81
|
-
collection: slugs.ticketMessages as any,
|
|
78
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
82
79
|
data: {
|
|
83
80
|
ticket: sourceTicket.id,
|
|
84
81
|
body: `Message extrait vers le ticket ${newTicket.ticketNumber}`,
|
|
@@ -95,8 +92,7 @@ export function createSplitTicketEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
95
92
|
: []
|
|
96
93
|
|
|
97
94
|
if (!existingRelated.includes(newTicket.id)) {
|
|
98
|
-
await payload.
|
|
99
|
-
collection: slugs.tickets as any,
|
|
95
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
100
96
|
id: sourceTicket.id,
|
|
101
97
|
data: { relatedTickets: [...existingRelated, newTicket.id] },
|
|
102
98
|
overrideAccess: true,
|
|
@@ -104,8 +100,7 @@ export function createSplitTicketEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
// Log activity
|
|
107
|
-
await payload.
|
|
108
|
-
collection: slugs.ticketActivityLog as any,
|
|
103
|
+
await dbCreate(payload, slugs.ticketActivityLog, {
|
|
109
104
|
data: {
|
|
110
105
|
ticket: sourceTicket.id,
|
|
111
106
|
action: 'split',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
import { dbFind } from '../utils/db'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* GET /api/support/statuses
|
|
@@ -17,8 +18,7 @@ export function createStatusesEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
17
18
|
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const { docs } = await payload.
|
|
21
|
-
collection: slugs.ticketStatuses as any,
|
|
21
|
+
const { docs } = await dbFind(payload, slugs.ticketStatuses, {
|
|
22
22
|
sort: 'sortOrder',
|
|
23
23
|
limit: 100,
|
|
24
24
|
depth: 0,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireClient, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFind, dbFindByID, dbCreate } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* POST /api/support/tickets/:id/feedback
|
|
@@ -46,8 +47,7 @@ export function createTicketFeedbackEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
// Find ticket and check it belongs to the requesting client
|
|
49
|
-
const ticket = (await payload.
|
|
50
|
-
collection: slugs.tickets as any,
|
|
50
|
+
const ticket = (await dbFindByID(payload, slugs.tickets, {
|
|
51
51
|
id: ticketId,
|
|
52
52
|
depth: 0,
|
|
53
53
|
overrideAccess: true,
|
|
@@ -66,8 +66,7 @@ export function createTicketFeedbackEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Check no feedback already exists for this ticket
|
|
69
|
-
const existing = await payload
|
|
70
|
-
collection: 'ticket-feedback' as any,
|
|
69
|
+
const existing = await dbFind(payload, 'ticket-feedback', {
|
|
71
70
|
where: { ticket: { equals: ticketId } },
|
|
72
71
|
limit: 1,
|
|
73
72
|
depth: 0,
|
|
@@ -80,8 +79,7 @@ export function createTicketFeedbackEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
80
79
|
)
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
const feedback = await payload
|
|
84
|
-
collection: 'ticket-feedback' as any,
|
|
82
|
+
const feedback = await dbCreate(payload, 'ticket-feedback', {
|
|
85
83
|
data: {
|
|
86
84
|
ticket: ticketId,
|
|
87
85
|
client: req.user.id,
|
|
@@ -2,6 +2,7 @@ import type { Endpoint } from 'payload'
|
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
4
|
import { generateTicketSynthesis } from '../utils/generateTicketSynthesis'
|
|
5
|
+
import { dbFindByID } from '../utils/db'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* POST /api/support/ticket-synthesis?ticketId=X[&force=true]
|
|
@@ -38,8 +39,7 @@ export function createTicketSynthesisEndpoint(slugs: CollectionSlugs): Endpoint
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
if (!force) {
|
|
41
|
-
const existing = await payload.
|
|
42
|
-
collection: slugs.tickets as any,
|
|
42
|
+
const existing = await dbFindByID(payload, slugs.tickets, {
|
|
43
43
|
id: ticketId,
|
|
44
44
|
depth: 0,
|
|
45
45
|
overrideAccess: true,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { createHmac } from 'crypto'
|
|
4
|
+
import { dbFindByID, dbUpdate, dbCreate } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
// 1x1 transparent GIF (43 bytes)
|
|
6
7
|
const TRANSPARENT_GIF = Buffer.from(
|
|
@@ -34,23 +35,16 @@ export function createTrackOpenEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
34
35
|
const parsedId = ticketId ? Number(ticketId) : NaN
|
|
35
36
|
const parsedMsgId = messageId ? Number(messageId) : NaN
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
+
// Open-tracking REQUIRES a valid HMAC signature. Without one we never
|
|
39
|
+
// process (no DB writes) — this prevents enumeration/abuse of t/m params
|
|
40
|
+
// and the silent "tracking writes without auth" hole.
|
|
38
41
|
const secret = process.env.PAYLOAD_SECRET || ''
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
headers: {
|
|
46
|
-
'Content-Type': 'image/gif',
|
|
47
|
-
'Content-Length': String(TRANSPARENT_GIF.length),
|
|
48
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
|
49
|
-
},
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
} else if (secret && (!sig || !ticketId || !messageId)) {
|
|
53
|
-
// Missing signature param: return GIF but don't process
|
|
42
|
+
const validSig =
|
|
43
|
+
!!secret && !!ticketId && !!messageId && !!sig &&
|
|
44
|
+
sig === generateTrackingToken(ticketId, messageId, secret)
|
|
45
|
+
|
|
46
|
+
if (!validSig) {
|
|
47
|
+
// Return transparent GIF silently (don't leak information, don't process)
|
|
54
48
|
return new Response(TRANSPARENT_GIF, {
|
|
55
49
|
status: 200,
|
|
56
50
|
headers: {
|
|
@@ -65,8 +59,7 @@ export function createTrackOpenEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
65
59
|
try {
|
|
66
60
|
const payload = req.payload
|
|
67
61
|
|
|
68
|
-
const ticket = await payload.
|
|
69
|
-
collection: slugs.tickets as any,
|
|
62
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
70
63
|
id: parsedId,
|
|
71
64
|
depth: 0,
|
|
72
65
|
overrideAccess: true,
|
|
@@ -78,8 +71,7 @@ export function createTrackOpenEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
78
71
|
const fiveMinAgo = Date.now() - 5 * 60 * 1000
|
|
79
72
|
|
|
80
73
|
if (lastRead < fiveMinAgo) {
|
|
81
|
-
await payload.
|
|
82
|
-
collection: slugs.tickets as any,
|
|
74
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
83
75
|
id: parsedId,
|
|
84
76
|
data: { lastClientReadAt: new Date().toISOString() },
|
|
85
77
|
overrideAccess: true,
|
|
@@ -89,8 +81,7 @@ export function createTrackOpenEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
89
81
|
|
|
90
82
|
// Track at message level
|
|
91
83
|
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
92
|
-
const msg = await payload.
|
|
93
|
-
collection: slugs.ticketMessages as any,
|
|
84
|
+
const msg = await dbFindByID(payload, slugs.ticketMessages, {
|
|
94
85
|
id: parsedMsgId,
|
|
95
86
|
depth: 0,
|
|
96
87
|
overrideAccess: true,
|
|
@@ -98,15 +89,13 @@ export function createTrackOpenEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
98
89
|
}) as any
|
|
99
90
|
|
|
100
91
|
if (msg && !msg.emailOpenedAt) {
|
|
101
|
-
await payload.
|
|
102
|
-
collection: slugs.ticketMessages as any,
|
|
92
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
103
93
|
id: parsedMsgId,
|
|
104
94
|
data: { emailOpenedAt: new Date().toISOString() },
|
|
105
95
|
overrideAccess: true,
|
|
106
96
|
})
|
|
107
97
|
|
|
108
|
-
const ticketInfo = await payload.
|
|
109
|
-
collection: slugs.tickets as any,
|
|
98
|
+
const ticketInfo = await dbFindByID(payload, slugs.tickets, {
|
|
110
99
|
id: parsedId,
|
|
111
100
|
depth: 1,
|
|
112
101
|
overrideAccess: true,
|
|
@@ -119,8 +108,7 @@ export function createTrackOpenEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
119
108
|
|
|
120
109
|
// Try to create admin notification (collection may not exist)
|
|
121
110
|
try {
|
|
122
|
-
await payload
|
|
123
|
-
collection: 'admin-notifications' as any,
|
|
111
|
+
await dbCreate(payload, 'admin-notifications', {
|
|
124
112
|
data: {
|
|
125
113
|
title: `Email ouvert — ${ticketInfo?.ticketNumber || 'TK-????'}`,
|
|
126
114
|
message: `${clientName} a ouvert votre email pour "${ticketInfo?.subject || 'ticket'}"`,
|