@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.
Files changed (199) hide show
  1. package/README.md +357 -435
  2. package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
  4. package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
  5. package/dist/components/TicketConversation/SkeletonText.js +20 -0
  6. package/dist/components/TicketConversation/constants.cjs +27 -0
  7. package/dist/components/TicketConversation/constants.js +26 -1
  8. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
  9. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
  10. package/dist/components/TicketConversation/index.cjs +10 -147
  11. package/dist/components/TicketConversation/index.js +4 -141
  12. package/dist/index.cjs +1763 -677
  13. package/dist/index.d.cts +4 -0
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.js +1760 -677
  16. package/dist/views/BillingView/client.cjs +40 -54
  17. package/dist/views/BillingView/client.js +40 -54
  18. package/dist/views/ChatView/client.cjs +4 -3
  19. package/dist/views/ChatView/client.js +4 -3
  20. package/dist/views/CrmView/client.cjs +7 -7
  21. package/dist/views/CrmView/client.js +7 -7
  22. package/dist/views/EmailTrackingView/client.cjs +6 -5
  23. package/dist/views/EmailTrackingView/client.js +6 -5
  24. package/dist/views/ImportConversationView/client.cjs +4 -4
  25. package/dist/views/ImportConversationView/client.js +4 -4
  26. package/dist/views/LogsView/client.cjs +3 -2
  27. package/dist/views/LogsView/client.js +3 -2
  28. package/dist/views/NewTicketView/client.cjs +1 -1
  29. package/dist/views/NewTicketView/client.js +1 -1
  30. package/dist/views/PendingEmailsView/client.cjs +7 -6
  31. package/dist/views/PendingEmailsView/client.js +7 -6
  32. package/dist/views/SupportDashboardView/client.cjs +19 -22
  33. package/dist/views/SupportDashboardView/client.js +19 -22
  34. package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
  35. package/dist/views/TicketDetailView/NextActionItem.js +34 -0
  36. package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
  37. package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
  38. package/dist/views/TicketDetailView/client.cjs +59 -190
  39. package/dist/views/TicketDetailView/client.js +52 -183
  40. package/dist/views/TicketDetailView/constants.cjs +20 -0
  41. package/dist/views/TicketDetailView/constants.js +16 -0
  42. package/dist/views/TicketDetailView/helpers.cjs +23 -0
  43. package/dist/views/TicketDetailView/helpers.js +20 -0
  44. package/dist/views/TicketDetailView/types.cjs +2 -0
  45. package/dist/views/TicketDetailView/types.js +1 -0
  46. package/dist/views/TicketInboxView/client.cjs +14 -10
  47. package/dist/views/TicketInboxView/client.js +14 -10
  48. package/dist/views/TicketingSettingsView/client.cjs +101 -96
  49. package/dist/views/TicketingSettingsView/client.js +92 -88
  50. package/dist/views/TimeDashboardView/client.cjs +2 -2
  51. package/dist/views/TimeDashboardView/client.js +2 -2
  52. package/dist/views/shared/StatusPill.cjs +23 -12
  53. package/dist/views/shared/StatusPill.js +23 -12
  54. package/dist/views/shared/dateLocale.cjs +5 -0
  55. package/dist/views/shared/dateLocale.js +3 -0
  56. package/package.json +28 -15
  57. package/src/__tests__/generateTrackingToken.test.ts +53 -0
  58. package/src/__tests__/integration/ai-agent.test.ts +46 -0
  59. package/src/__tests__/integration/auth.test.ts +43 -0
  60. package/src/__tests__/integration/automation-rules.test.ts +68 -0
  61. package/src/__tests__/integration/buildTestPayload.ts +52 -0
  62. package/src/__tests__/integration/channels.test.ts +52 -0
  63. package/src/__tests__/integration/core-behaviors.test.ts +58 -0
  64. package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
  65. package/src/__tests__/integration/digest.test.ts +48 -0
  66. package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
  67. package/src/__tests__/integration/isolation.test.ts +81 -0
  68. package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
  69. package/src/__tests__/integration/nps.test.ts +37 -0
  70. package/src/__tests__/integration/per-team-sla.test.ts +44 -0
  71. package/src/__tests__/integration/push.test.ts +75 -0
  72. package/src/__tests__/integration/sanitization.test.ts +35 -0
  73. package/src/__tests__/integration/sla-pause.test.ts +39 -0
  74. package/src/__tests__/integration/smoke.test.ts +26 -0
  75. package/src/__tests__/integration/snooze.test.ts +68 -0
  76. package/src/__tests__/integration/teams.test.ts +38 -0
  77. package/src/__tests__/rateLimiter.test.ts +89 -0
  78. package/src/__tests__/sanitizeHtml.test.ts +165 -0
  79. package/src/__tests__/sla.test.ts +258 -0
  80. package/src/collections/AutomationRules.ts +66 -0
  81. package/src/collections/NotificationQueue.ts +30 -0
  82. package/src/collections/PushSubscription.ts +28 -0
  83. package/src/collections/SatisfactionSurveys.ts +11 -2
  84. package/src/collections/SlaPolicies.ts +10 -0
  85. package/src/collections/SupportClients.ts +58 -1
  86. package/src/collections/SupportTeam.ts +27 -0
  87. package/src/collections/TicketActivityLog.ts +9 -4
  88. package/src/collections/TicketMessages.ts +140 -31
  89. package/src/collections/Tickets.ts +31 -73
  90. package/src/collections/index.ts +4 -0
  91. package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
  92. package/src/components/TicketConversation/SkeletonText.tsx +23 -0
  93. package/src/components/TicketConversation/constants.ts +29 -0
  94. package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
  95. package/src/components/TicketConversation/index.tsx +4 -152
  96. package/src/endpoints/admin-chat-stream.ts +3 -4
  97. package/src/endpoints/admin-chat.ts +11 -20
  98. package/src/endpoints/admin-stats.ts +77 -35
  99. package/src/endpoints/ai-agent.ts +36 -0
  100. package/src/endpoints/apply-macro.ts +8 -14
  101. package/src/endpoints/auth-2fa.ts +12 -12
  102. package/src/endpoints/auto-close.ts +6 -10
  103. package/src/endpoints/billing.ts +17 -4
  104. package/src/endpoints/bulk-action.ts +7 -12
  105. package/src/endpoints/channels.ts +89 -0
  106. package/src/endpoints/chat-stream.ts +3 -4
  107. package/src/endpoints/chat.ts +7 -12
  108. package/src/endpoints/chatbot.ts +2 -2
  109. package/src/endpoints/client-intelligence.ts +4 -4
  110. package/src/endpoints/delete-account.ts +9 -16
  111. package/src/endpoints/email-stats.ts +2 -2
  112. package/src/endpoints/escalate.ts +2 -2
  113. package/src/endpoints/export-csv.ts +2 -2
  114. package/src/endpoints/export-data.ts +5 -8
  115. package/src/endpoints/import-conversation.ts +6 -10
  116. package/src/endpoints/index.ts +22 -1
  117. package/src/endpoints/invite-collaborator.ts +30 -15
  118. package/src/endpoints/invoice.ts +127 -0
  119. package/src/endpoints/kb-search.ts +2 -2
  120. package/src/endpoints/login.ts +12 -4
  121. package/src/endpoints/merge-clients.ts +14 -19
  122. package/src/endpoints/merge-tickets.ts +11 -18
  123. package/src/endpoints/oauth-google.ts +86 -39
  124. package/src/endpoints/pending-emails-process.ts +15 -25
  125. package/src/endpoints/process-digests.ts +93 -0
  126. package/src/endpoints/process-scheduled.ts +6 -10
  127. package/src/endpoints/process-snooze.ts +78 -0
  128. package/src/endpoints/purge-logs.ts +2 -2
  129. package/src/endpoints/push.ts +49 -0
  130. package/src/endpoints/resend-notification.ts +3 -4
  131. package/src/endpoints/round-robin-config.ts +3 -4
  132. package/src/endpoints/satisfaction.ts +21 -14
  133. package/src/endpoints/search.ts +5 -8
  134. package/src/endpoints/seed-kb.ts +3 -4
  135. package/src/endpoints/send-reminder.ts +6 -9
  136. package/src/endpoints/settings.ts +5 -2
  137. package/src/endpoints/signature.ts +3 -4
  138. package/src/endpoints/sla-check.ts +2 -2
  139. package/src/endpoints/split-ticket.ts +7 -12
  140. package/src/endpoints/statuses.ts +2 -2
  141. package/src/endpoints/ticket-feedback.ts +4 -6
  142. package/src/endpoints/ticket-synthesis.ts +2 -2
  143. package/src/endpoints/track-open.ts +16 -28
  144. package/src/endpoints/transfer-ticket.ts +21 -9
  145. package/src/endpoints/user-prefs.ts +3 -4
  146. package/src/hooks/automationRules.ts +84 -0
  147. package/src/hooks/checkSLA.ts +63 -17
  148. package/src/plugin.ts +8 -0
  149. package/src/portal/LiveChat.tsx +2 -63
  150. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
  151. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
  152. package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
  153. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
  154. package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
  155. package/src/portal/auth/tickets/detail/page.tsx +2 -2
  156. package/src/portal/helpers.ts +46 -0
  157. package/src/portal/login/page.tsx +3 -8
  158. package/src/portal/types.ts +15 -0
  159. package/src/types/lucide-react.d.ts +34 -0
  160. package/src/types/payload-config.d.ts +10 -0
  161. package/src/utils/adminNotification.ts +2 -2
  162. package/src/utils/aiAgent.ts +147 -0
  163. package/src/utils/auth.ts +5 -4
  164. package/src/utils/channels.ts +36 -0
  165. package/src/utils/db.ts +73 -0
  166. package/src/utils/emailTemplate.ts +8 -1
  167. package/src/utils/fireWebhooks.ts +4 -6
  168. package/src/utils/generateTicketSynthesis.ts +7 -12
  169. package/src/utils/invoicePdf.ts +88 -0
  170. package/src/utils/notificationQueue.ts +36 -0
  171. package/src/utils/push.ts +63 -0
  172. package/src/utils/readSettings.ts +22 -6
  173. package/src/utils/sanitizeHtml.ts +57 -0
  174. package/src/utils/slugs.ts +8 -0
  175. package/src/utils/teamAccess.ts +27 -0
  176. package/src/utils/ticketAccess.ts +53 -0
  177. package/src/utils/webhookDispatcher.ts +4 -6
  178. package/src/views/BillingView/client.tsx +49 -37
  179. package/src/views/ChatView/client.tsx +4 -3
  180. package/src/views/CrmView/client.tsx +7 -7
  181. package/src/views/EmailTrackingView/client.tsx +7 -6
  182. package/src/views/ImportConversationView/client.tsx +4 -4
  183. package/src/views/LogsView/client.tsx +3 -2
  184. package/src/views/NewTicketView/client.tsx +1 -1
  185. package/src/views/PendingEmailsView/client.tsx +7 -6
  186. package/src/views/SupportDashboardView/client.tsx +21 -21
  187. package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
  188. package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
  189. package/src/views/TicketDetailView/client.tsx +54 -179
  190. package/src/views/TicketDetailView/constants.ts +23 -0
  191. package/src/views/TicketDetailView/helpers.ts +17 -0
  192. package/src/views/TicketDetailView/types.ts +10 -0
  193. package/src/views/TicketInboxView/client.tsx +16 -10
  194. package/src/views/TicketingSettingsView/client.tsx +90 -87
  195. package/src/views/TimeDashboardView/client.tsx +2 -2
  196. package/src/views/shared/StatusPill.tsx +24 -12
  197. package/src/views/shared/dateLocale.ts +5 -0
  198. package/src/views/shared/locales/en.json +708 -0
  199. 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 their own tickets
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
- return { 'ticket.client': { equals: req.user.id } }
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
- await req.payload.sendEmail({
94
- to: client.email,
95
- ...(supportEmail ? { replyTo: supportEmail } : {}),
96
- ...(internalBcc ? { bcc: internalBcc } : {}),
97
- subject: `Re: [${ticketNumber}] ${subject}`,
98
- html: emailWrapper(`Nouvelle reponse — ${ticketNumber}`, [
99
- emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || '')}</strong>,`),
100
- emailParagraph(`Notre equipe a apporte une reponse a votre ticket <strong>${escapeHtml(ticketNumber)}</strong> — <em>${escapeHtml(subject)}</em>.`),
101
- rawContent,
102
- emailButton('Consulter le ticket', portalUrl),
103
- emailParagraph('<span style="font-size: 13px; color: #6b7280;">Vous pouvez egalement repondre directement a cet email. Votre message sera automatiquement ajoute au ticket.</span>'),
104
- emailTrackingPixel(ticketId, doc.id),
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
- await req.payload.update({
112
- collection: slugs.ticketMessages,
113
- id: doc.id,
114
- data: { emailSentAt: new Date().toISOString(), emailSentTo: client.email },
115
- overrideAccess: true,
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.create({
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
- { 'ticket.client': { equals: req.user.id } } as Where,
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.delete({
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) return true
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.find({
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,
@@ -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'