@consilioweb/payload-support 0.5.1

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 (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +525 -0
  3. package/dist/client.cjs +7 -0
  4. package/dist/client.d.cts +3 -0
  5. package/dist/client.d.ts +3 -0
  6. package/dist/client.js +5 -0
  7. package/dist/index.cjs +7766 -0
  8. package/dist/index.d.cts +384 -0
  9. package/dist/index.d.ts +384 -0
  10. package/dist/index.js +7730 -0
  11. package/dist/views.d.cts +30 -0
  12. package/dist/views.d.ts +30 -0
  13. package/package.json +131 -0
  14. package/src/client.ts +1 -0
  15. package/src/collections/AuthLogs.ts +65 -0
  16. package/src/collections/CannedResponses.ts +69 -0
  17. package/src/collections/ChatMessages.ts +98 -0
  18. package/src/collections/EmailLogs.ts +94 -0
  19. package/src/collections/KnowledgeBase.ts +99 -0
  20. package/src/collections/Macros.ts +98 -0
  21. package/src/collections/PendingEmails.ts +122 -0
  22. package/src/collections/SatisfactionSurveys.ts +98 -0
  23. package/src/collections/SlaPolicies.ts +123 -0
  24. package/src/collections/SupportClients.ts +210 -0
  25. package/src/collections/TicketActivityLog.ts +81 -0
  26. package/src/collections/TicketMessages.ts +364 -0
  27. package/src/collections/TicketStatuses.ts +108 -0
  28. package/src/collections/Tickets.ts +704 -0
  29. package/src/collections/TimeEntries.ts +105 -0
  30. package/src/collections/WebhookEndpoints.ts +96 -0
  31. package/src/collections/index.ts +16 -0
  32. package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
  33. package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
  34. package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
  35. package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
  36. package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
  37. package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
  38. package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
  39. package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
  40. package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
  41. package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
  42. package/src/components/TicketConversation/config.ts +82 -0
  43. package/src/components/TicketConversation/constants.ts +74 -0
  44. package/src/components/TicketConversation/context.ts +63 -0
  45. package/src/components/TicketConversation/hooks/useAI.ts +180 -0
  46. package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
  47. package/src/components/TicketConversation/hooks/useReply.ts +190 -0
  48. package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
  49. package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
  50. package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
  51. package/src/components/TicketConversation/index.tsx +1110 -0
  52. package/src/components/TicketConversation/locales/en.json +878 -0
  53. package/src/components/TicketConversation/locales/fr.json +878 -0
  54. package/src/components/TicketConversation/types.ts +54 -0
  55. package/src/components/TicketConversation/utils.ts +25 -0
  56. package/src/endpoints/admin-chat-stream.ts +238 -0
  57. package/src/endpoints/admin-chat.ts +263 -0
  58. package/src/endpoints/admin-stats.ts +200 -0
  59. package/src/endpoints/ai.ts +199 -0
  60. package/src/endpoints/apply-macro.ts +144 -0
  61. package/src/endpoints/auth-2fa.ts +163 -0
  62. package/src/endpoints/auto-close.ts +175 -0
  63. package/src/endpoints/billing.ts +167 -0
  64. package/src/endpoints/bulk-action.ts +103 -0
  65. package/src/endpoints/chat-stream.ts +127 -0
  66. package/src/endpoints/chat.ts +188 -0
  67. package/src/endpoints/chatbot.ts +113 -0
  68. package/src/endpoints/delete-account.ts +129 -0
  69. package/src/endpoints/email-stats.ts +109 -0
  70. package/src/endpoints/export-csv.ts +84 -0
  71. package/src/endpoints/export-data.ts +104 -0
  72. package/src/endpoints/import-conversation.ts +307 -0
  73. package/src/endpoints/index.ts +154 -0
  74. package/src/endpoints/login.ts +92 -0
  75. package/src/endpoints/merge-clients.ts +132 -0
  76. package/src/endpoints/merge-tickets.ts +137 -0
  77. package/src/endpoints/oauth-google.ts +179 -0
  78. package/src/endpoints/pending-emails-process.ts +224 -0
  79. package/src/endpoints/presence.ts +104 -0
  80. package/src/endpoints/process-scheduled.ts +144 -0
  81. package/src/endpoints/purge-logs.ts +58 -0
  82. package/src/endpoints/resend-notification.ts +99 -0
  83. package/src/endpoints/round-robin-config.ts +92 -0
  84. package/src/endpoints/satisfaction.ts +93 -0
  85. package/src/endpoints/search.ts +106 -0
  86. package/src/endpoints/seed-kb.ts +153 -0
  87. package/src/endpoints/settings.ts +144 -0
  88. package/src/endpoints/signature.ts +93 -0
  89. package/src/endpoints/sla-check.ts +124 -0
  90. package/src/endpoints/split-ticket.ts +131 -0
  91. package/src/endpoints/statuses.ts +45 -0
  92. package/src/endpoints/track-open.ts +154 -0
  93. package/src/endpoints/typing.ts +101 -0
  94. package/src/endpoints/user-prefs.ts +125 -0
  95. package/src/hooks/checkSLA.ts +414 -0
  96. package/src/hooks/ticketStatusEmail.ts +182 -0
  97. package/src/index.ts +51 -0
  98. package/src/plugin.ts +157 -0
  99. package/src/portal/LiveChat.tsx +1353 -0
  100. package/src/portal/auth/ChatWidget.tsx +350 -0
  101. package/src/portal/auth/ChatbotWidget.tsx +285 -0
  102. package/src/portal/auth/SupportHeader.tsx +409 -0
  103. package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
  104. package/src/portal/auth/dashboard/page.tsx +84 -0
  105. package/src/portal/auth/faq/FAQSearch.tsx +117 -0
  106. package/src/portal/auth/faq/page.tsx +199 -0
  107. package/src/portal/auth/layout.tsx +61 -0
  108. package/src/portal/auth/profile/page.tsx +705 -0
  109. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
  110. package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
  111. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
  112. package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
  113. package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
  114. package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
  115. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
  116. package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
  117. package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
  118. package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
  119. package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
  120. package/src/portal/auth/tickets/detail/page.tsx +738 -0
  121. package/src/portal/auth/tickets/new/page.tsx +515 -0
  122. package/src/portal/forgot-password/page.tsx +114 -0
  123. package/src/portal/layout.tsx +26 -0
  124. package/src/portal/locales/en.json +374 -0
  125. package/src/portal/locales/fr.json +374 -0
  126. package/src/portal/login/page.tsx +351 -0
  127. package/src/portal/page.tsx +162 -0
  128. package/src/portal/register/page.tsx +281 -0
  129. package/src/portal/reset-password/page.tsx +152 -0
  130. package/src/styles/BillingView.module.scss +311 -0
  131. package/src/styles/ChatView.module.scss +438 -0
  132. package/src/styles/CommandPalette.module.scss +160 -0
  133. package/src/styles/CrmView.module.scss +554 -0
  134. package/src/styles/EmailTracking.module.scss +238 -0
  135. package/src/styles/ImportConversation.module.scss +267 -0
  136. package/src/styles/Layout.module.scss +55 -0
  137. package/src/styles/Logs.module.scss +164 -0
  138. package/src/styles/NewTicket.module.scss +143 -0
  139. package/src/styles/PendingEmails.module.scss +629 -0
  140. package/src/styles/SupportDashboard.module.scss +649 -0
  141. package/src/styles/TicketDetail.module.scss +1043 -0
  142. package/src/styles/TicketInbox.module.scss +296 -0
  143. package/src/styles/TicketingSettings.module.scss +358 -0
  144. package/src/styles/TimeDashboard.module.scss +287 -0
  145. package/src/styles/_tokens.scss +78 -0
  146. package/src/styles/theme.css +633 -0
  147. package/src/types.ts +255 -0
  148. package/src/utils/adminNotification.ts +38 -0
  149. package/src/utils/auth.ts +46 -0
  150. package/src/utils/emailTemplate.ts +343 -0
  151. package/src/utils/fireWebhooks.ts +84 -0
  152. package/src/utils/index.ts +22 -0
  153. package/src/utils/rateLimiter.ts +52 -0
  154. package/src/utils/readSettings.ts +67 -0
  155. package/src/utils/slugs.ts +54 -0
  156. package/src/utils/webhookDispatcher.ts +120 -0
  157. package/src/views/BillingView/client.tsx +137 -0
  158. package/src/views/BillingView/index.tsx +33 -0
  159. package/src/views/ChatView/client.tsx +294 -0
  160. package/src/views/ChatView/index.tsx +33 -0
  161. package/src/views/CrmView/client.tsx +206 -0
  162. package/src/views/CrmView/index.tsx +33 -0
  163. package/src/views/EmailTrackingView/client.tsx +124 -0
  164. package/src/views/EmailTrackingView/index.tsx +33 -0
  165. package/src/views/ImportConversationView/client.tsx +133 -0
  166. package/src/views/ImportConversationView/index.tsx +33 -0
  167. package/src/views/LogsView/client.tsx +151 -0
  168. package/src/views/LogsView/index.tsx +30 -0
  169. package/src/views/NewTicketView/client.tsx +227 -0
  170. package/src/views/NewTicketView/index.tsx +30 -0
  171. package/src/views/PendingEmailsView/client.tsx +177 -0
  172. package/src/views/PendingEmailsView/index.tsx +33 -0
  173. package/src/views/SupportDashboardView/client.tsx +424 -0
  174. package/src/views/SupportDashboardView/index.tsx +33 -0
  175. package/src/views/TicketDetailView/client.tsx +775 -0
  176. package/src/views/TicketDetailView/index.tsx +33 -0
  177. package/src/views/TicketInboxView/client.tsx +313 -0
  178. package/src/views/TicketInboxView/index.tsx +30 -0
  179. package/src/views/TicketingSettingsView/client.tsx +866 -0
  180. package/src/views/TicketingSettingsView/index.tsx +33 -0
  181. package/src/views/TimeDashboardView/client.tsx +144 -0
  182. package/src/views/TimeDashboardView/index.tsx +33 -0
  183. package/src/views/shared/AdminViewHeader.tsx +69 -0
  184. package/src/views/shared/ErrorBoundary.tsx +68 -0
  185. package/src/views/shared/Skeleton.tsx +125 -0
  186. package/src/views/shared/adminTokens.ts +37 -0
  187. package/src/views/shared/config.ts +82 -0
  188. package/src/views/shared/index.ts +6 -0
  189. package/src/views.ts +16 -0
@@ -0,0 +1,98 @@
1
+ import type { CollectionConfig } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+
4
+ // ─── Collection factory ──────────────────────────────────
5
+
6
+ export function createMacrosCollection(slugs: CollectionSlugs): CollectionConfig {
7
+ return {
8
+ slug: slugs.macros,
9
+ labels: {
10
+ singular: 'Macro',
11
+ plural: 'Macros',
12
+ },
13
+ admin: {
14
+ useAsTitle: 'name',
15
+ group: 'Support',
16
+ defaultColumns: ['name', 'description', 'sortOrder', 'isActive', 'updatedAt'],
17
+ },
18
+ fields: [
19
+ {
20
+ name: 'name',
21
+ type: 'text',
22
+ required: true,
23
+ label: 'Nom',
24
+ admin: {
25
+ description: 'Nom court pour identifier la macro (ex: "Clôturer & remercier")',
26
+ },
27
+ },
28
+ {
29
+ name: 'description',
30
+ type: 'text',
31
+ label: 'Description',
32
+ admin: {
33
+ description: 'Description optionnelle de ce que fait la macro',
34
+ },
35
+ },
36
+ {
37
+ name: 'actions',
38
+ type: 'array',
39
+ required: true,
40
+ label: 'Actions',
41
+ minRows: 1,
42
+ admin: {
43
+ description: 'Liste des actions exécutées dans l\'ordre',
44
+ },
45
+ fields: [
46
+ {
47
+ name: 'type',
48
+ type: 'select',
49
+ required: true,
50
+ label: 'Type d\'action',
51
+ options: [
52
+ { label: 'Changer le statut', value: 'set_status' },
53
+ { label: 'Changer la priorité', value: 'set_priority' },
54
+ { label: 'Ajouter un tag', value: 'add_tag' },
55
+ { label: 'Envoyer une réponse', value: 'send_reply' },
56
+ { label: 'Assigner', value: 'assign' },
57
+ ],
58
+ },
59
+ {
60
+ name: 'value',
61
+ type: 'text',
62
+ required: true,
63
+ label: 'Valeur',
64
+ admin: {
65
+ description: 'Statut, priorité, tag, corps de la réponse, ou ID utilisateur selon le type',
66
+ },
67
+ },
68
+ ],
69
+ },
70
+ {
71
+ name: 'sortOrder',
72
+ type: 'number',
73
+ defaultValue: 0,
74
+ label: 'Ordre',
75
+ admin: {
76
+ position: 'sidebar',
77
+ },
78
+ },
79
+ {
80
+ name: 'isActive',
81
+ type: 'checkbox',
82
+ defaultValue: true,
83
+ label: 'Active',
84
+ admin: {
85
+ position: 'sidebar',
86
+ description: 'Désactiver pour masquer la macro sans la supprimer',
87
+ },
88
+ },
89
+ ],
90
+ access: {
91
+ create: ({ req }) => req.user?.collection === slugs.users,
92
+ read: ({ req }) => req.user?.collection === slugs.users,
93
+ update: ({ req }) => req.user?.collection === slugs.users,
94
+ delete: ({ req }) => req.user?.collection === slugs.users,
95
+ },
96
+ timestamps: true,
97
+ }
98
+ }
@@ -0,0 +1,122 @@
1
+ import type { CollectionConfig } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+
4
+ // ─── Collection factory ──────────────────────────────────
5
+
6
+ export function createPendingEmailsCollection(slugs: CollectionSlugs): CollectionConfig {
7
+ return {
8
+ slug: slugs.pendingEmails,
9
+ admin: {
10
+ group: 'Support',
11
+ useAsTitle: 'subject',
12
+ defaultColumns: ['status', 'senderEmail', 'subject', 'createdAt'],
13
+ hidden: true, // Managed via custom admin view
14
+ },
15
+ access: {
16
+ read: ({ req }) => Boolean(req.user?.collection === slugs.users),
17
+ update: ({ req }) => Boolean(req.user?.collection === slugs.users),
18
+ delete: ({ req }) => Boolean(req.user?.collection === slugs.users),
19
+ create: ({ req }) => {
20
+ // Allow admin users or webhook calls with secret header
21
+ if (req.user?.collection === slugs.users) return true
22
+ const webhookSecret = req.headers.get('x-webhook-secret')
23
+ if (webhookSecret && process.env.SUPPORT_WEBHOOK_SECRET && webhookSecret === process.env.SUPPORT_WEBHOOK_SECRET) return true
24
+ return false
25
+ },
26
+ },
27
+ fields: [
28
+ {
29
+ name: 'senderEmail',
30
+ type: 'email',
31
+ required: true,
32
+ index: true,
33
+ },
34
+ {
35
+ name: 'senderName',
36
+ type: 'text',
37
+ },
38
+ {
39
+ name: 'subject',
40
+ type: 'text',
41
+ required: true,
42
+ },
43
+ {
44
+ name: 'rawSubject',
45
+ type: 'text',
46
+ admin: { readOnly: true },
47
+ },
48
+ {
49
+ name: 'body',
50
+ type: 'textarea',
51
+ required: true,
52
+ },
53
+ {
54
+ name: 'bodyHtml',
55
+ type: 'textarea',
56
+ admin: { readOnly: true },
57
+ },
58
+ {
59
+ name: 'client',
60
+ type: 'relationship',
61
+ relationTo: slugs.supportClients,
62
+ },
63
+ {
64
+ name: 'attachments',
65
+ type: 'array',
66
+ fields: [
67
+ {
68
+ name: 'file',
69
+ type: 'upload',
70
+ relationTo: slugs.media,
71
+ required: true,
72
+ },
73
+ ],
74
+ },
75
+ {
76
+ name: 'status',
77
+ type: 'select',
78
+ defaultValue: 'pending',
79
+ index: true,
80
+ options: [
81
+ { label: 'En attente', value: 'pending' },
82
+ { label: 'Traité', value: 'processed' },
83
+ { label: 'Ignoré', value: 'ignored' },
84
+ ],
85
+ },
86
+ {
87
+ name: 'processedAction',
88
+ type: 'select',
89
+ options: [
90
+ { label: 'Ticket créé', value: 'ticket_created' },
91
+ { label: 'Message ajouté', value: 'message_added' },
92
+ { label: 'Ignoré', value: 'ignored' },
93
+ ],
94
+ admin: { condition: (data) => data?.status !== 'pending' },
95
+ },
96
+ {
97
+ name: 'processedTicket',
98
+ type: 'relationship',
99
+ relationTo: slugs.tickets,
100
+ admin: { condition: (data) => data?.processedAction === 'ticket_created' || data?.processedAction === 'message_added' },
101
+ },
102
+ {
103
+ name: 'processedAt',
104
+ type: 'date',
105
+ admin: { condition: (data) => data?.status !== 'pending' },
106
+ },
107
+ {
108
+ name: 'suggestedTickets',
109
+ type: 'json',
110
+ admin: { readOnly: true },
111
+ },
112
+ {
113
+ name: 'cc',
114
+ type: 'text',
115
+ },
116
+ {
117
+ name: 'recipientEmail',
118
+ type: 'text',
119
+ },
120
+ ],
121
+ }
122
+ }
@@ -0,0 +1,98 @@
1
+ import type { CollectionConfig } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+
4
+ // ─── Collection factory ──────────────────────────────────
5
+
6
+ export function createSatisfactionSurveysCollection(slugs: CollectionSlugs): CollectionConfig {
7
+ return {
8
+ slug: slugs.satisfactionSurveys,
9
+ labels: {
10
+ singular: 'Enquête satisfaction',
11
+ plural: 'Enquêtes satisfaction',
12
+ },
13
+ admin: {
14
+ hidden: true,
15
+ group: 'Support',
16
+ defaultColumns: ['source', 'rating', 'client', 'createdAt'],
17
+ },
18
+ fields: [
19
+ {
20
+ name: 'source',
21
+ type: 'select',
22
+ options: [
23
+ { label: 'Ticket', value: 'ticket' },
24
+ { label: 'Live Chat', value: 'live-chat' },
25
+ ],
26
+ defaultValue: 'ticket',
27
+ required: true,
28
+ label: 'Source',
29
+ admin: {
30
+ description: 'Ticket de support ou session de chat en direct',
31
+ },
32
+ },
33
+ {
34
+ name: 'ticket',
35
+ type: 'relationship',
36
+ relationTo: slugs.tickets,
37
+ unique: true,
38
+ label: 'Ticket',
39
+ admin: {
40
+ condition: (data) => data?.source === 'ticket',
41
+ },
42
+ },
43
+ {
44
+ name: 'chatSession',
45
+ type: 'text',
46
+ unique: true,
47
+ label: 'Session Chat',
48
+ admin: {
49
+ condition: (data) => data?.source === 'live-chat',
50
+ description: 'ID de la session live chat',
51
+ },
52
+ },
53
+ {
54
+ name: 'client',
55
+ type: 'relationship',
56
+ relationTo: slugs.supportClients,
57
+ required: true,
58
+ label: 'Client',
59
+ },
60
+ {
61
+ name: 'rating',
62
+ type: 'number',
63
+ required: true,
64
+ min: 1,
65
+ max: 5,
66
+ label: 'Note (1-5)',
67
+ admin: {
68
+ description: '1 = Très insatisfait, 5 = Très satisfait',
69
+ },
70
+ },
71
+ {
72
+ name: 'comment',
73
+ type: 'textarea',
74
+ label: 'Commentaire',
75
+ admin: {
76
+ description: 'Feedback libre du client',
77
+ },
78
+ },
79
+ ],
80
+ access: {
81
+ create: ({ req }) => {
82
+ if (req.user?.collection === slugs.users) return true
83
+ if (req.user?.collection === slugs.supportClients) return true
84
+ return false
85
+ },
86
+ read: ({ req }) => {
87
+ if (req.user?.collection === slugs.users) return true
88
+ if (req.user?.collection === slugs.supportClients) {
89
+ return { client: { equals: req.user.id } }
90
+ }
91
+ return false
92
+ },
93
+ update: () => false, // Surveys are immutable once submitted
94
+ delete: ({ req }) => req.user?.collection === slugs.users,
95
+ },
96
+ timestamps: true,
97
+ }
98
+ }
@@ -0,0 +1,123 @@
1
+ import type { CollectionConfig } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+
4
+ // ─── Collection factory ──────────────────────────────────
5
+
6
+ export function createSlaPoliciesCollection(slugs: CollectionSlugs): CollectionConfig {
7
+ return {
8
+ slug: slugs.slaPolicies,
9
+ labels: {
10
+ singular: 'Politique SLA',
11
+ plural: 'Politiques SLA',
12
+ },
13
+ admin: {
14
+ useAsTitle: 'name',
15
+ group: 'Support',
16
+ defaultColumns: ['name', 'priority', 'firstResponseTime', 'resolutionTime', 'businessHoursOnly', 'isDefault'],
17
+ },
18
+ fields: [
19
+ {
20
+ name: 'name',
21
+ type: 'text',
22
+ required: true,
23
+ label: 'Nom',
24
+ admin: {
25
+ description: 'Ex: Standard, Premium, Urgence',
26
+ },
27
+ },
28
+ {
29
+ type: 'row',
30
+ fields: [
31
+ {
32
+ name: 'firstResponseTime',
33
+ type: 'number',
34
+ required: true,
35
+ label: 'Délai 1ère réponse (min)',
36
+ min: 1,
37
+ admin: {
38
+ width: '50%',
39
+ description: 'Temps cible en minutes pour la première réponse',
40
+ },
41
+ },
42
+ {
43
+ name: 'resolutionTime',
44
+ type: 'number',
45
+ required: true,
46
+ label: 'Délai résolution (min)',
47
+ min: 1,
48
+ admin: {
49
+ width: '50%',
50
+ description: 'Temps cible en minutes pour la résolution complète',
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ {
56
+ name: 'priority',
57
+ type: 'select',
58
+ required: true,
59
+ label: 'Priorité',
60
+ options: [
61
+ { label: 'Basse', value: 'low' },
62
+ { label: 'Normale', value: 'normal' },
63
+ { label: 'Haute', value: 'high' },
64
+ { label: 'Urgente', value: 'urgent' },
65
+ ],
66
+ admin: {
67
+ description: 'Priorité de ticket à laquelle cette politique s\'applique',
68
+ },
69
+ },
70
+ {
71
+ type: 'row',
72
+ fields: [
73
+ {
74
+ name: 'businessHoursOnly',
75
+ type: 'checkbox',
76
+ defaultValue: true,
77
+ label: 'Heures ouvrées uniquement',
78
+ admin: {
79
+ width: '50%',
80
+ description: 'Compter uniquement les heures ouvrées (lun-ven, 9h-18h)',
81
+ },
82
+ },
83
+ {
84
+ name: 'escalateOnBreach',
85
+ type: 'checkbox',
86
+ defaultValue: false,
87
+ label: 'Escalade auto sur dépassement',
88
+ admin: {
89
+ width: '50%',
90
+ description: 'Notifier automatiquement en cas de dépassement SLA',
91
+ },
92
+ },
93
+ ],
94
+ },
95
+ {
96
+ name: 'escalateTo',
97
+ type: 'relationship',
98
+ relationTo: slugs.users,
99
+ label: 'Escalader à',
100
+ admin: {
101
+ description: 'Utilisateur à notifier en cas de dépassement SLA',
102
+ condition: (data) => data?.escalateOnBreach === true,
103
+ },
104
+ },
105
+ {
106
+ name: 'isDefault',
107
+ type: 'checkbox',
108
+ defaultValue: false,
109
+ label: 'Politique par défaut',
110
+ admin: {
111
+ description: 'Utiliser cette politique pour les tickets sans SLA spécifique (par priorité)',
112
+ },
113
+ },
114
+ ],
115
+ access: {
116
+ create: ({ req }) => req.user?.collection === slugs.users,
117
+ read: ({ req }) => req.user?.collection === slugs.users,
118
+ update: ({ req }) => req.user?.collection === slugs.users,
119
+ delete: ({ req }) => req.user?.collection === slugs.users,
120
+ },
121
+ timestamps: true,
122
+ }
123
+ }
@@ -0,0 +1,210 @@
1
+ import type { CollectionConfig, CollectionAfterChangeHook } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import { escapeHtml, emailWrapper, emailButton, emailParagraph } from '../utils/emailTemplate'
4
+ import { readSupportSettings } from '../utils/readSettings'
5
+
6
+ // ─── Hooks ───────────────────────────────────────────────
7
+
8
+ function createSendInvitationOnCreate(slugs: CollectionSlugs): CollectionAfterChangeHook {
9
+ return async ({ doc, operation, req }) => {
10
+ if (operation !== 'create') return doc
11
+
12
+ // Only when an admin creates the client (not self-registration)
13
+ if (req.user?.collection !== slugs.users) return doc
14
+
15
+ try {
16
+ const { payload } = req
17
+ const settings = await readSupportSettings(payload)
18
+
19
+ // Generate reset token without sending the default forgotPassword email
20
+ const token = await payload.forgotPassword({
21
+ collection: slugs.supportClients,
22
+ data: { email: doc.email },
23
+ disableEmail: true,
24
+ })
25
+
26
+ const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || ''
27
+ const resetUrl = `${baseUrl}/support/reset-password?token=${token}`
28
+
29
+ const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || ''
30
+ await payload.sendEmail({
31
+ to: doc.email,
32
+ ...(replyTo ? { replyTo } : {}),
33
+ subject: 'Activez votre compte support',
34
+ html: emailWrapper('Bienvenue sur votre espace support', [
35
+ emailParagraph(`Bonjour <strong>${escapeHtml(doc.firstName || '')}</strong>,`),
36
+ emailParagraph('Un espace support a ete cree pour vous. Vous pourrez y soumettre vos demandes, suivre vos tickets et echanger directement avec notre equipe.'),
37
+ emailParagraph('Pour activer votre compte, cliquez sur le bouton ci-dessous pour definir votre mot de passe :'),
38
+ emailButton('Definir mon mot de passe', resetUrl),
39
+ `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin: 20px 0;">
40
+ <tr><td style="padding: 6px 0; font-size: 14px; color: #374151; line-height: 1.6;">&#8226; Soumettre des demandes de support</td></tr>
41
+ <tr><td style="padding: 6px 0; font-size: 14px; color: #374151; line-height: 1.6;">&#8226; Suivre l'avancement de vos tickets en temps reel</td></tr>
42
+ <tr><td style="padding: 6px 0; font-size: 14px; color: #374151; line-height: 1.6;">&#8226; Joindre des fichiers et captures d'ecran</td></tr>
43
+ <tr><td style="padding: 6px 0; font-size: 14px; color: #374151; line-height: 1.6;">&#8226; Consulter l'historique complet de vos echanges</td></tr>
44
+ </table>`,
45
+ emailParagraph('<span style="font-size: 13px; color: #6b7280;">Ce lien est valable 1 heure.</span>'),
46
+ ].join('')),
47
+ })
48
+
49
+ // Invitation email sent successfully
50
+ } catch (err) {
51
+ console.error('[support-clients] Failed to send invitation email:', err)
52
+ }
53
+
54
+ return doc
55
+ }
56
+ }
57
+
58
+ // ─── Collection factory ──────────────────────────────────
59
+
60
+ export function createSupportClientsCollection(slugs: CollectionSlugs): CollectionConfig {
61
+ return {
62
+ slug: slugs.supportClients,
63
+ labels: {
64
+ singular: 'Client Support',
65
+ plural: 'Clients Support',
66
+ },
67
+ auth: {
68
+ tokenExpiration: 7200, // 2 hours
69
+ maxLoginAttempts: 10,
70
+ lockTime: 300 * 1000, // 5 minutes
71
+ cookies: {
72
+ secure: process.env.NODE_ENV === 'production',
73
+ sameSite: 'Lax',
74
+ },
75
+ forgotPassword: {
76
+ generateEmailSubject: () => 'Reinitialisation de votre mot de passe',
77
+ generateEmailHTML: (args) => {
78
+ const token = args?.token || ''
79
+ const user = args?.user as { firstName?: string } | undefined
80
+ const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || ''
81
+ const resetUrl = `${baseUrl}/support/reset-password?token=${token}`
82
+ const name = user?.firstName || ''
83
+
84
+ return emailWrapper('Reinitialisation de mot de passe', [
85
+ emailParagraph(`Bonjour${name ? ` <strong>${escapeHtml(name)}</strong>` : ''},`),
86
+ emailParagraph('Vous avez demande la reinitialisation de votre mot de passe pour votre espace support.'),
87
+ emailParagraph('Cliquez sur le bouton ci-dessous pour definir un nouveau mot de passe :'),
88
+ emailButton('Definir mon mot de passe', resetUrl),
89
+ emailParagraph('<span style="font-size: 13px; color: #6b7280;">Ce lien est valable 1 heure. Si vous n\'avez pas effectue cette demande, vous pouvez ignorer cet email.</span>'),
90
+ ].join(''))
91
+ },
92
+ },
93
+ },
94
+ admin: {
95
+ useAsTitle: 'company',
96
+ group: 'Support',
97
+ defaultColumns: ['company', 'email', 'firstName', 'lastName', 'createdAt'],
98
+ },
99
+ fields: [
100
+ {
101
+ type: 'row',
102
+ fields: [
103
+ {
104
+ name: 'company',
105
+ type: 'text',
106
+ required: true,
107
+ label: 'Entreprise',
108
+ admin: { width: '50%' },
109
+ },
110
+ {
111
+ name: 'phone',
112
+ type: 'text',
113
+ label: 'Telephone',
114
+ admin: { width: '50%' },
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ type: 'row',
120
+ fields: [
121
+ {
122
+ name: 'firstName',
123
+ type: 'text',
124
+ required: true,
125
+ label: 'Prenom',
126
+ admin: { width: '50%' },
127
+ },
128
+ {
129
+ name: 'lastName',
130
+ type: 'text',
131
+ required: true,
132
+ label: 'Nom',
133
+ admin: { width: '50%' },
134
+ },
135
+ ],
136
+ },
137
+ {
138
+ name: 'twoFactorEnabled',
139
+ type: 'checkbox',
140
+ defaultValue: false,
141
+ label: '2FA active',
142
+ admin: {
143
+ description: 'Verification par email a chaque connexion',
144
+ position: 'sidebar',
145
+ },
146
+ },
147
+ {
148
+ name: 'twoFactorCode',
149
+ type: 'text',
150
+ admin: { hidden: true },
151
+ },
152
+ {
153
+ name: 'twoFactorExpiry',
154
+ type: 'date',
155
+ admin: { hidden: true },
156
+ },
157
+ {
158
+ name: 'notifyOnReply',
159
+ type: 'checkbox',
160
+ defaultValue: true,
161
+ label: 'Notifications reponses',
162
+ admin: {
163
+ description: 'Recevoir un email a chaque reponse du support',
164
+ position: 'sidebar',
165
+ },
166
+ },
167
+ {
168
+ name: 'notifyOnStatusChange',
169
+ type: 'checkbox',
170
+ defaultValue: true,
171
+ label: 'Notifications statut',
172
+ admin: {
173
+ description: 'Recevoir un email quand le statut d\'un ticket change',
174
+ position: 'sidebar',
175
+ },
176
+ },
177
+ {
178
+ name: 'notes',
179
+ type: 'textarea',
180
+ label: 'Notes internes',
181
+ admin: {
182
+ description: 'Visible uniquement par les admins',
183
+ position: 'sidebar',
184
+ },
185
+ },
186
+ ],
187
+ hooks: {
188
+ afterChange: [createSendInvitationOnCreate(slugs)],
189
+ },
190
+ access: {
191
+ create: ({ req }) => req.user?.collection === slugs.users,
192
+ update: ({ req }) => {
193
+ if (req.user?.collection === slugs.users) return true
194
+ if (req.user?.collection === slugs.supportClients) {
195
+ return { id: { equals: req.user.id } }
196
+ }
197
+ return false
198
+ },
199
+ delete: ({ req }) => req.user?.collection === slugs.users,
200
+ read: ({ req }) => {
201
+ if (req.user?.collection === slugs.users) return true
202
+ if (req.user?.collection === slugs.supportClients) {
203
+ return { id: { equals: req.user.id } }
204
+ }
205
+ return false
206
+ },
207
+ },
208
+ timestamps: true,
209
+ }
210
+ }