@consilioweb/payload-support 0.16.0 → 1.1.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 +357 -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 +1763 -677
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1760 -677
- 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 +28 -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/per-team-sla.test.ts +44 -0
- package/src/__tests__/integration/push.test.ts +75 -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/PushSubscription.ts +28 -0
- package/src/collections/SatisfactionSurveys.ts +11 -2
- package/src/collections/SlaPolicies.ts +10 -0
- 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 +140 -31
- package/src/collections/Tickets.ts +31 -73
- package/src/collections/index.ts +4 -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 +77 -35
- 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 +22 -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/push.ts +49 -0
- 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 +63 -17
- package/src/plugin.ts +8 -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/push.ts +63 -0
- package/src/utils/readSettings.ts +22 -6
- package/src/utils/sanitizeHtml.ts +57 -0
- package/src/utils/slugs.ts +8 -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,4 +1,5 @@
|
|
|
1
|
-
import type { CollectionConfig, CollectionAfterChangeHook } from 'payload'
|
|
1
|
+
import type { CollectionConfig, CollectionAfterChangeHook, CollectionBeforeLoginHook } from 'payload'
|
|
2
|
+
import { APIError } from 'payload'
|
|
2
3
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
4
|
import { escapeHtml, emailWrapper, emailButton, emailParagraph } from '../utils/emailTemplate'
|
|
4
5
|
import { readSupportSettings } from '../utils/readSettings'
|
|
@@ -55,6 +56,40 @@ function createSendInvitationOnCreate(slugs: CollectionSlugs): CollectionAfterCh
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
// ─── 2FA enforcement ─────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
// A successful /support/2fa "verify" stamps `twoFactorVerifiedAt`. Login is only
|
|
62
|
+
// allowed within this window, after which the marker is consumed (single-use).
|
|
63
|
+
const TWO_FA_WINDOW_MS = 5 * 60 * 1000
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Enforce 2FA at the auth layer for EVERY login path (custom endpoint AND the
|
|
67
|
+
* native /api/<collection>/login), closing the bypass where a token was issued
|
|
68
|
+
* before any 2FA check. Payload verifies the password BEFORE beforeLogin, and
|
|
69
|
+
* throwing here rolls back the session it just created — so no token is issued.
|
|
70
|
+
*/
|
|
71
|
+
function createEnforce2FA(slugs: CollectionSlugs): CollectionBeforeLoginHook {
|
|
72
|
+
return async ({ req, user }) => {
|
|
73
|
+
if (!(user as { twoFactorEnabled?: boolean })?.twoFactorEnabled) return user
|
|
74
|
+
const raw = (user as { twoFactorVerifiedAt?: string | null }).twoFactorVerifiedAt
|
|
75
|
+
const verifiedAt = raw ? new Date(raw).getTime() : 0
|
|
76
|
+
if (!(verifiedAt > Date.now() - TWO_FA_WINDOW_MS)) {
|
|
77
|
+
// Credentials valid, but a fresh 2FA verification is required.
|
|
78
|
+
throw new APIError('2FA_REQUIRED', 401)
|
|
79
|
+
}
|
|
80
|
+
// Single-use: consume the marker so the verified window can't be replayed.
|
|
81
|
+
await req.payload.update({
|
|
82
|
+
collection: slugs.supportClients,
|
|
83
|
+
id: user.id,
|
|
84
|
+
data: { twoFactorVerifiedAt: null },
|
|
85
|
+
req,
|
|
86
|
+
overrideAccess: true,
|
|
87
|
+
})
|
|
88
|
+
;(user as { twoFactorVerifiedAt?: string | null }).twoFactorVerifiedAt = null
|
|
89
|
+
return user
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
// ─── Collection factory ──────────────────────────────────
|
|
59
94
|
|
|
60
95
|
export function createSupportClientsCollection(slugs: CollectionSlugs): CollectionConfig {
|
|
@@ -184,6 +219,10 @@ export function createSupportClientsCollection(slugs: CollectionSlugs): Collecti
|
|
|
184
219
|
name: 'opportunities',
|
|
185
220
|
type: 'textarea',
|
|
186
221
|
label: 'Opportunites commerciales',
|
|
222
|
+
access: {
|
|
223
|
+
// Admin-only: never serialize commercial notes onto the client's own doc.
|
|
224
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
225
|
+
},
|
|
187
226
|
admin: {
|
|
188
227
|
description: 'Notes commerciales (visible uniquement par les admins)',
|
|
189
228
|
},
|
|
@@ -227,6 +266,19 @@ export function createSupportClientsCollection(slugs: CollectionSlugs): Collecti
|
|
|
227
266
|
type: 'date',
|
|
228
267
|
admin: { hidden: true },
|
|
229
268
|
},
|
|
269
|
+
{
|
|
270
|
+
// Set by /support/2fa "verify"; gates login via the beforeLogin hook.
|
|
271
|
+
name: 'twoFactorVerifiedAt',
|
|
272
|
+
type: 'date',
|
|
273
|
+
admin: { hidden: true },
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
// Google OAuth subject id — accounts are linked by this, not email alone.
|
|
277
|
+
name: 'googleId',
|
|
278
|
+
type: 'text',
|
|
279
|
+
index: true,
|
|
280
|
+
admin: { hidden: true },
|
|
281
|
+
},
|
|
230
282
|
{
|
|
231
283
|
name: 'notifyOnReply',
|
|
232
284
|
type: 'checkbox',
|
|
@@ -293,6 +345,10 @@ export function createSupportClientsCollection(slugs: CollectionSlugs): Collecti
|
|
|
293
345
|
name: 'notes',
|
|
294
346
|
type: 'textarea',
|
|
295
347
|
label: 'Notes internes',
|
|
348
|
+
access: {
|
|
349
|
+
// Admin-only: never serialize internal notes onto the client's own doc.
|
|
350
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
351
|
+
},
|
|
296
352
|
admin: {
|
|
297
353
|
description: 'Visible uniquement par les admins',
|
|
298
354
|
position: 'sidebar',
|
|
@@ -300,6 +356,7 @@ export function createSupportClientsCollection(slugs: CollectionSlugs): Collecti
|
|
|
300
356
|
},
|
|
301
357
|
],
|
|
302
358
|
hooks: {
|
|
359
|
+
beforeLogin: [createEnforce2FA(slugs)],
|
|
303
360
|
afterChange: [createSendInvitationOnCreate(slugs)],
|
|
304
361
|
},
|
|
305
362
|
access: {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CollectionConfig } from 'payload'
|
|
2
|
+
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Support teams / workspaces. Agents belong to teams; tickets can be assigned to
|
|
6
|
+
* a team. When `SUPPORT_TEAM_SCOPING` is enabled, agents only see the tickets of
|
|
7
|
+
* their teams (plus team-less tickets) — see the Tickets read access.
|
|
8
|
+
*/
|
|
9
|
+
export function createSupportTeamCollection(slugs: CollectionSlugs): CollectionConfig {
|
|
10
|
+
return {
|
|
11
|
+
slug: slugs.supportTeams,
|
|
12
|
+
labels: { singular: 'Équipe support', plural: 'Équipes support' },
|
|
13
|
+
admin: { useAsTitle: 'name', group: 'Support', defaultColumns: ['name', 'members'] },
|
|
14
|
+
access: {
|
|
15
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
16
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
17
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
18
|
+
delete: ({ req }) => req.user?.collection === slugs.users,
|
|
19
|
+
},
|
|
20
|
+
fields: [
|
|
21
|
+
{ name: 'name', type: 'text', required: true, label: 'Nom' },
|
|
22
|
+
{ name: 'description', type: 'textarea', label: 'Description' },
|
|
23
|
+
{ name: 'members', type: 'relationship', relationTo: slugs.users, hasMany: true, index: true, label: 'Membres (agents)' },
|
|
24
|
+
],
|
|
25
|
+
timestamps: true,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { CollectionConfig } from 'payload'
|
|
1
|
+
import type { CollectionConfig, Where } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
import { resolveAccessibleTicketIds } from '../utils/ticketAccess'
|
|
3
4
|
|
|
4
5
|
// ─── Collection factory ──────────────────────────────────
|
|
5
6
|
|
|
@@ -21,6 +22,7 @@ export function createTicketActivityLogCollection(slugs: CollectionSlugs): Colle
|
|
|
21
22
|
type: 'relationship',
|
|
22
23
|
relationTo: slugs.tickets,
|
|
23
24
|
required: true,
|
|
25
|
+
index: true,
|
|
24
26
|
label: 'Ticket',
|
|
25
27
|
},
|
|
26
28
|
{
|
|
@@ -65,11 +67,14 @@ export function createTicketActivityLogCollection(slugs: CollectionSlugs): Colle
|
|
|
65
67
|
],
|
|
66
68
|
access: {
|
|
67
69
|
create: () => false, // Created only by hooks via overrideAccess
|
|
68
|
-
read: ({ req }) => {
|
|
70
|
+
read: async ({ req }) => {
|
|
69
71
|
if (req.user?.collection === slugs.users) return true
|
|
70
|
-
// Clients can read activity logs for
|
|
72
|
+
// Clients can read activity logs only for tickets they own/collaborate on.
|
|
73
|
+
// Constrain by `ticket IN (...)` rather than a nested relationship filter
|
|
74
|
+
// (security boundary). Empty set → sentinel id (fail closed).
|
|
71
75
|
if (req.user?.collection === slugs.supportClients) {
|
|
72
|
-
|
|
76
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id)
|
|
77
|
+
return { ticket: { in: ids.length > 0 ? ids : [-1] } } as Where
|
|
73
78
|
}
|
|
74
79
|
return false
|
|
75
80
|
},
|
|
@@ -6,6 +6,11 @@ import { createAdminNotification } from '../utils/adminNotification'
|
|
|
6
6
|
import { dispatchWebhook } from '../utils/webhookDispatcher'
|
|
7
7
|
import { readSupportSettings } from '../utils/readSettings'
|
|
8
8
|
import { createCheckSlaOnReply } from '../hooks/checkSLA'
|
|
9
|
+
import { resolveAccessibleTicketIds } from '../utils/ticketAccess'
|
|
10
|
+
import { sanitizeMessageHtml } from '../utils/sanitizeHtml'
|
|
11
|
+
import { queueClientNotification } from '../utils/notificationQueue'
|
|
12
|
+
import { sendPushToUser } from '../utils/push'
|
|
13
|
+
import { dbFind, dbCreate, dbFindByID } from '../utils/db'
|
|
9
14
|
|
|
10
15
|
function createAssignAuthor(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
11
16
|
return async ({ data, operation, req }) => {
|
|
@@ -90,30 +95,51 @@ function createNotifyClient(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
|
90
95
|
|
|
91
96
|
const replyPreview = (doc.body || '').replace(/\s+/g, ' ').trim().slice(0, 90)
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
].join(''), {
|
|
106
|
-
kind: 'reply',
|
|
107
|
-
preheader: replyPreview ? `${replyPreview}…` : `Nouvelle reponse a votre ticket ${ticketNumber}`,
|
|
108
|
-
}),
|
|
109
|
-
})
|
|
98
|
+
// Digest: clients on daily/weekly frequency are queued, not emailed now.
|
|
99
|
+
const freq = (client as { notificationFrequency?: string }).notificationFrequency
|
|
100
|
+
if (freq === 'daily' || freq === 'weekly') {
|
|
101
|
+
await queueClientNotification(req.payload, slugs, {
|
|
102
|
+
client: (client as { id: number | string }).id,
|
|
103
|
+
type: 'reply',
|
|
104
|
+
title: `Nouvelle reponse — ${ticketNumber}`,
|
|
105
|
+
message: replyPreview || `Reponse a votre ticket ${ticketNumber}`,
|
|
106
|
+
ticket: ticketId,
|
|
107
|
+
})
|
|
108
|
+
return doc
|
|
109
|
+
}
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
// Fire-and-forget: don't block the reply mutation on the SMTP round-trip
|
|
112
|
+
// (200-500ms). The email + its emailSentAt bookkeeping run detached.
|
|
113
|
+
void (async () => {
|
|
114
|
+
try {
|
|
115
|
+
await req.payload.sendEmail({
|
|
116
|
+
to: client.email,
|
|
117
|
+
...(supportEmail ? { replyTo: supportEmail } : {}),
|
|
118
|
+
...(internalBcc ? { bcc: internalBcc } : {}),
|
|
119
|
+
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
120
|
+
html: emailWrapper(`Nouvelle reponse — ${ticketNumber}`, [
|
|
121
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || '')}</strong>,`),
|
|
122
|
+
emailParagraph(`Notre equipe a apporte une reponse a votre ticket <strong>${escapeHtml(ticketNumber)}</strong> — <em>${escapeHtml(subject)}</em>.`),
|
|
123
|
+
rawContent,
|
|
124
|
+
emailButton('Consulter le ticket', portalUrl),
|
|
125
|
+
emailParagraph('<span style="font-size: 13px; color: #6b7280;">Vous pouvez egalement repondre directement a cet email. Votre message sera automatiquement ajoute au ticket.</span>'),
|
|
126
|
+
emailTrackingPixel(ticketId, doc.id),
|
|
127
|
+
].join(''), {
|
|
128
|
+
kind: 'reply',
|
|
129
|
+
preheader: replyPreview ? `${replyPreview}…` : `Nouvelle reponse a votre ticket ${ticketNumber}`,
|
|
130
|
+
}),
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
await req.payload.update({
|
|
134
|
+
collection: slugs.ticketMessages,
|
|
135
|
+
id: doc.id,
|
|
136
|
+
data: { emailSentAt: new Date().toISOString(), emailSentTo: client.email },
|
|
137
|
+
overrideAccess: true,
|
|
138
|
+
})
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error('[support] Failed to notify client (async):', err)
|
|
141
|
+
}
|
|
142
|
+
})()
|
|
117
143
|
} catch (err) {
|
|
118
144
|
console.error('[support] Failed to notify client:', err)
|
|
119
145
|
}
|
|
@@ -161,8 +187,7 @@ function createSyncTicketReplyToChat(slugs: CollectionSlugs): CollectionAfterCha
|
|
|
161
187
|
const clientId = typeof ticket.client === 'object' ? (ticket.client as { id: number | string }).id : ticket.client
|
|
162
188
|
|
|
163
189
|
// Create a chat message so the client sees it in the widget
|
|
164
|
-
await payload.
|
|
165
|
-
collection: slugs.chatMessages as any,
|
|
190
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
166
191
|
data: {
|
|
167
192
|
session: ticket.chatSession,
|
|
168
193
|
client: clientId,
|
|
@@ -236,6 +261,16 @@ function createNotifyAdminOnClientMessage(slugs: CollectionSlugs, notificationSl
|
|
|
236
261
|
const preview = doc.body?.length > 500 ? doc.body.slice(0, 500) + '...' : doc.body
|
|
237
262
|
const headerTitle = isNewTicket ? `Nouveau ticket ${ticketNumber}` : `Nouveau message — ${ticketNumber}`
|
|
238
263
|
|
|
264
|
+
// Browser push to the assigned agent (RUNTIME no-op without VAPID keys).
|
|
265
|
+
const assignedId = assignedAdmin?.id ?? (ticket.assignedTo && typeof ticket.assignedTo !== 'object' ? ticket.assignedTo : undefined)
|
|
266
|
+
if (assignedId) {
|
|
267
|
+
void sendPushToUser(payload, slugs, assignedId, {
|
|
268
|
+
title: headerTitle,
|
|
269
|
+
body: `${clientName}: ${preview || subject}`,
|
|
270
|
+
url: `/admin/collections/${slugs.tickets}/${ticketId}`,
|
|
271
|
+
}).catch(() => { /* best-effort */ })
|
|
272
|
+
}
|
|
273
|
+
|
|
239
274
|
if (primaryEmail) {
|
|
240
275
|
await payload.sendEmail({
|
|
241
276
|
// INTERNAL email (to the support team) — recipients are the contact
|
|
@@ -302,6 +337,72 @@ function createDispatchWebhookOnReply(slugs: CollectionSlugs): CollectionAfterCh
|
|
|
302
337
|
}
|
|
303
338
|
}
|
|
304
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Sanitize submitted rich HTML before persistence. `bodyHtml` is rendered via
|
|
342
|
+
* dangerouslySetInnerHTML in the admin & portal, so unsanitized HTML is a
|
|
343
|
+
* stored-XSS vector (→ admin session theft → cross-client access). Runs for
|
|
344
|
+
* every writer (client AND admin) — neither needs to store scripts.
|
|
345
|
+
*/
|
|
346
|
+
function createSanitizeMessageHtml(): CollectionBeforeChangeHook {
|
|
347
|
+
return ({ data }) => {
|
|
348
|
+
if (data && typeof data.bodyHtml === 'string' && data.bodyHtml) {
|
|
349
|
+
data.bodyHtml = sanitizeMessageHtml(data.bodyHtml)
|
|
350
|
+
}
|
|
351
|
+
return data
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Matches `@someone@example.com` mentions inside a message body.
|
|
356
|
+
const MENTION_RE = /@([^\s@]+@[^\s@]+\.[^\s@]+)/g
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Resolve `@email` mentions in the message body to agent (users) IDs, stored on
|
|
360
|
+
* the `mentions` field so the UI can highlight them and we can notify them.
|
|
361
|
+
*/
|
|
362
|
+
function createResolveMentions(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
363
|
+
return async ({ data, operation, req }) => {
|
|
364
|
+
if (operation !== 'create') return data
|
|
365
|
+
const body = typeof data?.body === 'string' ? data.body : ''
|
|
366
|
+
const emails = Array.from(new Set([...body.matchAll(MENTION_RE)].map((m) => m[1].toLowerCase())))
|
|
367
|
+
if (emails.length === 0) return data
|
|
368
|
+
try {
|
|
369
|
+
const users = await dbFind(req.payload, slugs.users, { where: { email: { in: emails } }, limit: 50, depth: 0, overrideAccess: true })
|
|
370
|
+
const ids = users.docs.map((u) => (u as { id: number | string }).id)
|
|
371
|
+
if (ids.length > 0) data.mentions = ids
|
|
372
|
+
} catch { /* users lookup is best-effort */ }
|
|
373
|
+
return data
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Email the agents mentioned in a freshly-created message. */
|
|
378
|
+
function createNotifyMentions(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
379
|
+
return async ({ doc, operation, req }) => {
|
|
380
|
+
if (operation !== 'create') return doc
|
|
381
|
+
const mentions: Array<number | string | { id: number | string }> = Array.isArray(doc.mentions) ? doc.mentions : []
|
|
382
|
+
if (mentions.length === 0) return doc
|
|
383
|
+
const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
|
|
384
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || ''
|
|
385
|
+
for (const m of mentions) {
|
|
386
|
+
const userId = typeof m === 'object' ? m.id : m
|
|
387
|
+
try {
|
|
388
|
+
const user = await dbFindByID(req.payload, slugs.users, { id: userId, depth: 0, overrideAccess: true })
|
|
389
|
+
if (!user?.email) continue
|
|
390
|
+
await req.payload.sendEmail({
|
|
391
|
+
to: user.email,
|
|
392
|
+
subject: `Vous avez ete mentionne — ticket #${ticketId}`,
|
|
393
|
+
html: emailWrapper('Mention', [
|
|
394
|
+
emailParagraph(`Vous avez ete mentionne dans un message du ticket <strong>#${escapeHtml(String(ticketId))}</strong>.`),
|
|
395
|
+
...(baseUrl ? [emailButton('Ouvrir le ticket', `${baseUrl}/admin/collections/${slugs.tickets}/${ticketId}`, 'dark')] : []),
|
|
396
|
+
].join(''), { kind: 'system' }),
|
|
397
|
+
})
|
|
398
|
+
} catch (err) {
|
|
399
|
+
console.error('[support] Failed to notify mention:', err)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return doc
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
305
406
|
export function createTicketMessagesCollection(slugs: CollectionSlugs, options?: {
|
|
306
407
|
notificationSlug?: string
|
|
307
408
|
}): CollectionConfig {
|
|
@@ -312,14 +413,14 @@ export function createTicketMessagesCollection(slugs: CollectionSlugs, options?:
|
|
|
312
413
|
labels: { singular: 'Message', plural: 'Messages' },
|
|
313
414
|
admin: { hidden: true, group: 'Support', defaultColumns: ['ticket', 'authorType', 'createdAt'] },
|
|
314
415
|
fields: [
|
|
315
|
-
{ name: 'ticket', type: 'relationship', relationTo: slugs.tickets, required: true, label: 'Ticket' },
|
|
416
|
+
{ name: 'ticket', type: 'relationship', relationTo: slugs.tickets, required: true, index: true, label: 'Ticket' },
|
|
316
417
|
{ name: 'body', type: 'textarea', required: true, label: 'Message' },
|
|
317
418
|
{ name: 'bodyHtml', type: 'textarea', label: 'Message HTML', admin: { hidden: true } },
|
|
318
419
|
{
|
|
319
420
|
type: 'row',
|
|
320
421
|
fields: [
|
|
321
422
|
{
|
|
322
|
-
name: 'authorType', type: 'select', label: 'Type d\'auteur', defaultValue: 'admin',
|
|
423
|
+
name: 'authorType', type: 'select', index: true, label: 'Type d\'auteur', defaultValue: 'admin',
|
|
323
424
|
options: [
|
|
324
425
|
{ label: 'Client', value: 'client' },
|
|
325
426
|
{ label: 'Support', value: 'admin' },
|
|
@@ -348,8 +449,9 @@ export function createTicketMessagesCollection(slugs: CollectionSlugs, options?:
|
|
|
348
449
|
},
|
|
349
450
|
{ name: 'isInternal', type: 'checkbox', defaultValue: false, label: 'Note interne', admin: { position: 'sidebar' } },
|
|
350
451
|
{ name: 'isSolution', type: 'checkbox', defaultValue: false, label: 'Reponse solution', admin: { position: 'sidebar' } },
|
|
452
|
+
{ name: 'mentions', type: 'relationship', relationTo: slugs.users, hasMany: true, label: 'Agents mentionnes', admin: { hidden: true } },
|
|
351
453
|
{ name: 'skipNotification', type: 'checkbox', defaultValue: false, label: 'Sans notification', admin: { position: 'sidebar', condition: (data) => data?.skipNotification === true } },
|
|
352
|
-
{ name: 'scheduledAt', type: 'date', label: 'Programme pour', admin: { date: { pickerAppearance: 'dayAndTime' }, position: 'sidebar', condition: (data) => !!data?.scheduledAt } },
|
|
454
|
+
{ name: 'scheduledAt', type: 'date', index: true, label: 'Programme pour', admin: { date: { pickerAppearance: 'dayAndTime' }, position: 'sidebar', condition: (data) => !!data?.scheduledAt } },
|
|
353
455
|
{ name: 'scheduledSent', type: 'checkbox', defaultValue: false, admin: { hidden: true } },
|
|
354
456
|
{ name: 'editedAt', type: 'date', label: 'Modifie le', admin: { hidden: true } },
|
|
355
457
|
{ name: 'deletedAt', type: 'date', label: 'Supprime le', admin: { hidden: true } },
|
|
@@ -358,7 +460,7 @@ export function createTicketMessagesCollection(slugs: CollectionSlugs, options?:
|
|
|
358
460
|
{ name: 'emailOpenedAt', type: 'date', label: 'Email ouvert le', admin: { hidden: true } },
|
|
359
461
|
],
|
|
360
462
|
hooks: {
|
|
361
|
-
beforeChange: [createAssignAuthor(slugs)],
|
|
463
|
+
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
362
464
|
afterChange: [
|
|
363
465
|
createAutoUpdateStatus(slugs),
|
|
364
466
|
createNotifyClient(slugs),
|
|
@@ -368,16 +470,23 @@ export function createTicketMessagesCollection(slugs: CollectionSlugs, options?:
|
|
|
368
470
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
369
471
|
createFireMessageWebhooks(slugs),
|
|
370
472
|
createDispatchWebhookOnReply(slugs),
|
|
473
|
+
createNotifyMentions(slugs),
|
|
371
474
|
],
|
|
372
475
|
},
|
|
373
476
|
access: {
|
|
374
477
|
create: ({ req }) => req.user?.collection === slugs.users || req.user?.collection === slugs.supportClients,
|
|
375
|
-
read: ({ req }) => {
|
|
478
|
+
read: async ({ req }) => {
|
|
376
479
|
if (req.user?.collection === slugs.users) return true
|
|
377
480
|
if (req.user?.collection === slugs.supportClients) {
|
|
481
|
+
// Constrain to tickets the client owns or collaborates on, then filter
|
|
482
|
+
// messages by `ticket IN (...)`. We do NOT use a nested relationship
|
|
483
|
+
// filter ('ticket.client') as the security boundary — its behaviour
|
|
484
|
+
// through the SQLite adapter is not guaranteed. Empty set → sentinel
|
|
485
|
+
// id so the IN matches nothing (fail closed).
|
|
486
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id)
|
|
378
487
|
return {
|
|
379
488
|
and: [
|
|
380
|
-
{
|
|
489
|
+
{ ticket: { in: ids.length > 0 ? ids : [-1] } } as Where,
|
|
381
490
|
{ isInternal: { equals: false } } as Where,
|
|
382
491
|
{ or: [{ scheduledAt: { exists: false } } as Where, { scheduledSent: { equals: true } } as Where] } as Where,
|
|
383
492
|
],
|
|
@@ -13,8 +13,11 @@ import { createAdminNotification } from '../utils/adminNotification'
|
|
|
13
13
|
import { dispatchWebhook } from '../utils/webhookDispatcher'
|
|
14
14
|
import { readSupportSettings } from '../utils/readSettings'
|
|
15
15
|
import { createTicketStatusEmail } from '../hooks/ticketStatusEmail'
|
|
16
|
-
import { createAssignSlaDeadlines, createCheckSlaOnResolve } from '../hooks/checkSLA'
|
|
16
|
+
import { createAssignSlaDeadlines, createCheckSlaOnResolve, createPauseSlaOnHold } from '../hooks/checkSLA'
|
|
17
|
+
import { createApplyAutomationRules } from '../hooks/automationRules'
|
|
18
|
+
import { isTeamScopingEnabled, resolveAgentTeamIds } from '../utils/teamAccess'
|
|
17
19
|
import { generateTicketSynthesis } from '../utils/generateTicketSynthesis'
|
|
20
|
+
import { dbFind, dbCreate, dbDelete } from '../utils/db'
|
|
18
21
|
|
|
19
22
|
// ─── Hooks ───────────────────────────────────────────────
|
|
20
23
|
|
|
@@ -314,64 +317,6 @@ function createNotifyClientOnResolve(slugs: CollectionSlugs): CollectionAfterCha
|
|
|
314
317
|
}
|
|
315
318
|
}
|
|
316
319
|
|
|
317
|
-
function createAutoCalculateSLA(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
318
|
-
return async ({ doc, operation, req }) => {
|
|
319
|
-
if (operation !== 'create') return doc
|
|
320
|
-
try {
|
|
321
|
-
const { payload } = req
|
|
322
|
-
const ticketPriority = doc.priority || 'normal'
|
|
323
|
-
|
|
324
|
-
// Try to find a SLA policy matching the ticket's priority first
|
|
325
|
-
let policy: any = null
|
|
326
|
-
const byPriority = await payload.find({
|
|
327
|
-
collection: slugs.slaPolicies as any,
|
|
328
|
-
where: { priority: { equals: ticketPriority } },
|
|
329
|
-
limit: 1,
|
|
330
|
-
depth: 0,
|
|
331
|
-
overrideAccess: true,
|
|
332
|
-
})
|
|
333
|
-
if (byPriority.docs.length > 0) {
|
|
334
|
-
policy = byPriority.docs[0]
|
|
335
|
-
} else {
|
|
336
|
-
// Fall back to the default policy
|
|
337
|
-
const defaults = await payload.find({
|
|
338
|
-
collection: slugs.slaPolicies as any,
|
|
339
|
-
where: { isDefault: { equals: true } },
|
|
340
|
-
limit: 1,
|
|
341
|
-
depth: 0,
|
|
342
|
-
overrideAccess: true,
|
|
343
|
-
})
|
|
344
|
-
if (defaults.docs.length > 0) {
|
|
345
|
-
policy = defaults.docs[0]
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (!policy) return doc
|
|
350
|
-
|
|
351
|
-
// SLA fields are in minutes (firstResponseTime, resolutionTime)
|
|
352
|
-
const now = new Date()
|
|
353
|
-
const firstResponseMinutes = policy.firstResponseTime || 240 // default 4h
|
|
354
|
-
const resolutionMinutes = policy.resolutionTime || 1440 // default 24h
|
|
355
|
-
const firstResponseDue = new Date(now.getTime() + firstResponseMinutes * 60000)
|
|
356
|
-
const resolutionDue = new Date(now.getTime() + resolutionMinutes * 60000)
|
|
357
|
-
|
|
358
|
-
await payload.update({
|
|
359
|
-
collection: slugs.tickets as any,
|
|
360
|
-
id: doc.id,
|
|
361
|
-
data: {
|
|
362
|
-
slaPolicy: policy.id,
|
|
363
|
-
slaFirstResponseDue: firstResponseDue.toISOString(),
|
|
364
|
-
slaResolutionDue: resolutionDue.toISOString(),
|
|
365
|
-
},
|
|
366
|
-
overrideAccess: true,
|
|
367
|
-
})
|
|
368
|
-
} catch (err) {
|
|
369
|
-
console.error('[support] Failed to auto-calculate SLA:', err)
|
|
370
|
-
}
|
|
371
|
-
return doc
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
320
|
function createFireTicketWebhooks(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
376
321
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
377
322
|
const { payload } = req
|
|
@@ -478,8 +423,7 @@ function createCascadeDelete(slugs: CollectionSlugs): CollectionBeforeDeleteHook
|
|
|
478
423
|
return async ({ id, req }) => {
|
|
479
424
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys]
|
|
480
425
|
for (const slug of collections) {
|
|
481
|
-
await req.payload
|
|
482
|
-
collection: slug as any,
|
|
426
|
+
await dbDelete(req.payload, slug, {
|
|
483
427
|
where: { ticket: { equals: id } },
|
|
484
428
|
overrideAccess: true,
|
|
485
429
|
})
|
|
@@ -619,7 +563,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
619
563
|
type: 'row',
|
|
620
564
|
fields: [
|
|
621
565
|
{
|
|
622
|
-
name: 'status', type: 'select', defaultValue: 'open', label: 'Statut',
|
|
566
|
+
name: 'status', type: 'select', index: true, defaultValue: 'open', label: 'Statut',
|
|
623
567
|
options: [
|
|
624
568
|
{ label: 'Ouvert', value: 'open' },
|
|
625
569
|
{ label: 'En attente client', value: 'waiting_client' },
|
|
@@ -630,7 +574,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
630
574
|
admin: { width: '50%' },
|
|
631
575
|
},
|
|
632
576
|
{
|
|
633
|
-
name: 'priority', type: 'select', defaultValue: 'normal', label: 'Priorite',
|
|
577
|
+
name: 'priority', type: 'select', index: true, defaultValue: 'normal', label: 'Priorite',
|
|
634
578
|
options: [
|
|
635
579
|
{ label: 'Basse', value: 'low' },
|
|
636
580
|
{ label: 'Normale', value: 'normal' },
|
|
@@ -644,7 +588,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
644
588
|
{
|
|
645
589
|
type: 'row',
|
|
646
590
|
fields: [
|
|
647
|
-
{ name: 'client', type: 'relationship', relationTo: slugs.supportClients, required: true, label: 'Client', admin: { width: '50%' } },
|
|
591
|
+
{ name: 'client', type: 'relationship', relationTo: slugs.supportClients, required: true, index: true, label: 'Client', admin: { width: '50%' } },
|
|
648
592
|
],
|
|
649
593
|
},
|
|
650
594
|
// Billing collapsible
|
|
@@ -710,8 +654,8 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
710
654
|
{
|
|
711
655
|
type: 'row',
|
|
712
656
|
fields: [
|
|
713
|
-
{ name: 'slaFirstResponseDue', type: 'date', label: 'Echeance 1ere reponse (SLA)', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
714
|
-
{ name: 'slaResolutionDue', type: 'date', label: 'Echeance resolution (SLA)', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
657
|
+
{ name: 'slaFirstResponseDue', type: 'date', index: true, label: 'Echeance 1ere reponse (SLA)', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
658
|
+
{ name: 'slaResolutionDue', type: 'date', index: true, label: 'Echeance resolution (SLA)', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
715
659
|
],
|
|
716
660
|
},
|
|
717
661
|
{
|
|
@@ -719,6 +663,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
719
663
|
fields: [
|
|
720
664
|
{ name: 'slaFirstResponseBreached', type: 'checkbox', label: '1ere reponse depassee', admin: { readOnly: true, width: '50%' } },
|
|
721
665
|
{ name: 'slaResolutionBreached', type: 'checkbox', label: 'Resolution depassee', admin: { readOnly: true, width: '50%' } },
|
|
666
|
+
{ name: 'slaPausedAt', type: 'date', label: 'SLA en pause depuis', admin: { hidden: true } },
|
|
722
667
|
],
|
|
723
668
|
},
|
|
724
669
|
],
|
|
@@ -739,14 +684,14 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
739
684
|
},
|
|
740
685
|
{ name: 'mergedInto', type: 'relationship', relationTo: slugs.tickets, label: 'Fusionne dans', admin: { readOnly: true } },
|
|
741
686
|
{ name: 'autoCloseRemindedAt', type: 'date', label: 'Rappel auto-close envoye', admin: { readOnly: true, date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
742
|
-
{ 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.' } },
|
|
687
|
+
{ 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.' } },
|
|
743
688
|
],
|
|
744
689
|
},
|
|
745
690
|
// Sidebar
|
|
746
691
|
{ name: 'ticketNumber', type: 'text', unique: true, label: 'N° Ticket', admin: { position: 'sidebar' } },
|
|
747
692
|
{ name: 'slaPolicy', type: 'relationship', relationTo: slugs.slaPolicies, label: 'Politique SLA', admin: { position: 'sidebar' } },
|
|
748
693
|
{
|
|
749
|
-
name: 'category', type: 'select', label: 'Categorie',
|
|
694
|
+
name: 'category', type: 'select', index: true, label: 'Categorie',
|
|
750
695
|
options: [
|
|
751
696
|
{ label: 'Bug / Dysfonctionnement', value: 'bug' },
|
|
752
697
|
{ label: 'Modification de contenu', value: 'content' },
|
|
@@ -756,7 +701,8 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
756
701
|
],
|
|
757
702
|
admin: { position: 'sidebar' },
|
|
758
703
|
},
|
|
759
|
-
{ name: 'assignedTo', type: 'relationship', relationTo: slugs.users, label: 'Assigne a', admin: { position: 'sidebar' } },
|
|
704
|
+
{ name: 'assignedTo', type: 'relationship', relationTo: slugs.users, index: true, label: 'Assigne a', admin: { position: 'sidebar' } },
|
|
705
|
+
{ name: 'team', type: 'relationship', relationTo: slugs.supportTeams, index: true, label: 'Equipe', admin: { position: 'sidebar' } },
|
|
760
706
|
{
|
|
761
707
|
name: 'source', type: 'select', defaultValue: 'portal', label: 'Source',
|
|
762
708
|
options: [
|
|
@@ -764,6 +710,8 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
764
710
|
{ label: 'Chat en direct', value: 'live-chat' },
|
|
765
711
|
{ label: 'Email', value: 'email' },
|
|
766
712
|
{ label: 'Admin', value: 'admin' },
|
|
713
|
+
{ label: 'WhatsApp', value: 'whatsapp' },
|
|
714
|
+
{ label: 'Messenger', value: 'messenger' },
|
|
767
715
|
],
|
|
768
716
|
admin: { position: 'sidebar' },
|
|
769
717
|
},
|
|
@@ -811,8 +759,8 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
811
759
|
afterChange: [
|
|
812
760
|
createTrackSLA(slugs),
|
|
813
761
|
createTrackAiSummaryOnResolve(slugs),
|
|
814
|
-
createAutoCalculateSLA(slugs),
|
|
815
762
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
763
|
+
createPauseSlaOnHold(slugs),
|
|
816
764
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
817
765
|
createLogTicketActivity(slugs),
|
|
818
766
|
createNotifyOnAssignment(slugs),
|
|
@@ -821,6 +769,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
821
769
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
822
770
|
createFireTicketWebhooks(slugs),
|
|
823
771
|
createDispatchWebhookOnTicket(slugs),
|
|
772
|
+
createApplyAutomationRules(slugs),
|
|
824
773
|
],
|
|
825
774
|
beforeDelete: [createCascadeDelete(slugs)],
|
|
826
775
|
},
|
|
@@ -831,15 +780,24 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
831
780
|
return false
|
|
832
781
|
},
|
|
833
782
|
read: async ({ req }) => {
|
|
834
|
-
if (req.user?.collection === slugs.users)
|
|
783
|
+
if (req.user?.collection === slugs.users) {
|
|
784
|
+
if (!isTeamScopingEnabled()) return true
|
|
785
|
+
// Team scoping (opt-in): agents see their teams' tickets + team-less ones.
|
|
786
|
+
const teamIds = await resolveAgentTeamIds(req.payload, slugs, req.user.id)
|
|
787
|
+
return {
|
|
788
|
+
or: [
|
|
789
|
+
{ team: { in: teamIds.length > 0 ? teamIds : [-1] } },
|
|
790
|
+
{ team: { exists: false } },
|
|
791
|
+
],
|
|
792
|
+
} as Where
|
|
793
|
+
}
|
|
835
794
|
if (req.user?.collection === slugs.supportClients) {
|
|
836
795
|
// Owner OR collaborator (via ticket-collaborators table).
|
|
837
796
|
// We resolve the collaborator ticket-ids upfront so we keep a single Where
|
|
838
797
|
// expression — Payload's access fns support async returns.
|
|
839
798
|
let collabTicketIds: Array<number | string> = []
|
|
840
799
|
try {
|
|
841
|
-
const rows = await req.payload
|
|
842
|
-
collection: 'ticket-collaborators' as any,
|
|
800
|
+
const rows = await dbFind(req.payload, 'ticket-collaborators', {
|
|
843
801
|
where: { client: { equals: req.user.id } },
|
|
844
802
|
limit: 1000,
|
|
845
803
|
depth: 0,
|
package/src/collections/index.ts
CHANGED
|
@@ -17,3 +17,7 @@ export { createTicketStatusesCollection } from './TicketStatuses'
|
|
|
17
17
|
export { createClientSummariesCollection } from './ClientSummaries'
|
|
18
18
|
export { createTicketFeedbackCollection } from './TicketFeedback'
|
|
19
19
|
export { createTicketCollaboratorsCollection } from './TicketCollaborators'
|
|
20
|
+
export { createNotificationQueueCollection } from './NotificationQueue'
|
|
21
|
+
export { createAutomationRulesCollection } from './AutomationRules'
|
|
22
|
+
export { createSupportTeamCollection } from './SupportTeam'
|
|
23
|
+
export { createPushSubscriptionCollection } from './PushSubscription'
|