@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,92 @@
1
+ import type { Endpoint } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import { requireAdmin, handleAuthError } from '../utils/auth'
4
+
5
+ const PREF_KEY = 'support-round-robin'
6
+
7
+ /**
8
+ * GET /api/support/round-robin-config — Get round-robin enabled status
9
+ */
10
+ export function createRoundRobinConfigGetEndpoint(slugs: CollectionSlugs): Endpoint {
11
+ return {
12
+ path: '/support/round-robin-config',
13
+ method: 'get',
14
+ handler: async (req) => {
15
+ try {
16
+ const payload = req.payload
17
+
18
+ requireAdmin(req, slugs)
19
+
20
+ const prefs = await payload.find({
21
+ collection: 'payload-preferences' as any,
22
+ where: { key: { equals: PREF_KEY } },
23
+ limit: 1,
24
+ depth: 0,
25
+ overrideAccess: true,
26
+ })
27
+
28
+ const enabled = prefs.docs.length > 0
29
+ ? (prefs.docs[0].value as { enabled?: boolean })?.enabled === true
30
+ : false
31
+
32
+ return Response.json({ enabled })
33
+ } catch (error) {
34
+ const authResponse = handleAuthError(error)
35
+ if (authResponse) return authResponse
36
+ console.warn('[round-robin-config] GET error:', error)
37
+ return Response.json({ error: 'Error' }, { status: 500 })
38
+ }
39
+ },
40
+ }
41
+ }
42
+
43
+ /**
44
+ * POST /api/support/round-robin-config — Enable/disable round-robin
45
+ */
46
+ export function createRoundRobinConfigPostEndpoint(slugs: CollectionSlugs): Endpoint {
47
+ return {
48
+ path: '/support/round-robin-config',
49
+ method: 'post',
50
+ handler: async (req) => {
51
+ try {
52
+ const payload = req.payload
53
+
54
+ requireAdmin(req, slugs)
55
+
56
+ const { enabled } = (await req.json!()) as { enabled: boolean }
57
+
58
+ const existing = await payload.find({
59
+ collection: 'payload-preferences' as any,
60
+ where: { key: { equals: PREF_KEY } },
61
+ limit: 1,
62
+ depth: 0,
63
+ overrideAccess: true,
64
+ })
65
+
66
+ await payload.db.upsert({
67
+ collection: 'payload-preferences',
68
+ data: {
69
+ key: PREF_KEY,
70
+ user: { relationTo: req.user!.collection, value: req.user!.id },
71
+ value: { enabled: !!enabled },
72
+ },
73
+ req: { payload, user: req.user } as any,
74
+ where: {
75
+ and: [
76
+ { key: { equals: PREF_KEY } },
77
+ { 'user.value': { equals: req.user!.id } },
78
+ { 'user.relationTo': { equals: req.user!.collection } },
79
+ ],
80
+ },
81
+ })
82
+
83
+ return Response.json({ enabled: !!enabled })
84
+ } catch (error) {
85
+ const authResponse = handleAuthError(error)
86
+ if (authResponse) return authResponse
87
+ console.error('[round-robin-config] POST error:', error)
88
+ return Response.json({ error: 'Error' }, { status: 500 })
89
+ }
90
+ },
91
+ }
92
+ }
@@ -0,0 +1,93 @@
1
+ import type { Endpoint } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import { requireClient, handleAuthError } from '../utils/auth'
4
+
5
+ /**
6
+ * POST /api/support/satisfaction
7
+ * Client submits a satisfaction survey for a resolved ticket.
8
+ */
9
+ export function createSatisfactionEndpoint(slugs: CollectionSlugs): Endpoint {
10
+ return {
11
+ path: '/support/satisfaction',
12
+ method: 'post',
13
+ handler: async (req) => {
14
+ try {
15
+ const payload = req.payload
16
+
17
+ requireClient(req, slugs)
18
+
19
+ const body = await req.json!()
20
+ const { ticketId, rating, comment } = body
21
+
22
+ if (!ticketId || !rating || !Number.isInteger(rating) || rating < 1 || rating > 5) {
23
+ return Response.json(
24
+ { error: 'ticketId et rating (entier 1-5) sont requis.' },
25
+ { status: 400 },
26
+ )
27
+ }
28
+
29
+ if (comment && typeof comment === 'string' && comment.length > 5000) {
30
+ return Response.json(
31
+ { error: 'Le commentaire ne peut pas dépasser 5000 caractères.' },
32
+ { status: 400 },
33
+ )
34
+ }
35
+
36
+ // Verify ticket belongs to client and is resolved
37
+ const ticket = await payload.findByID({
38
+ collection: slugs.tickets as any,
39
+ id: ticketId,
40
+ depth: 0,
41
+ overrideAccess: false,
42
+ user: req.user,
43
+ }) as any
44
+
45
+ if (!ticket) {
46
+ return Response.json({ error: 'Ticket introuvable.' }, { status: 404 })
47
+ }
48
+
49
+ if (ticket.status !== 'resolved') {
50
+ return Response.json(
51
+ { error: 'Le ticket doit être résolu pour laisser un avis.' },
52
+ { status: 400 },
53
+ )
54
+ }
55
+
56
+ // Check if survey already exists
57
+ const existing = await payload.find({
58
+ collection: slugs.satisfactionSurveys as any,
59
+ where: { ticket: { equals: ticketId } },
60
+ limit: 1,
61
+ depth: 0,
62
+ overrideAccess: true,
63
+ })
64
+
65
+ if (existing.docs.length > 0) {
66
+ return Response.json(
67
+ { error: 'Vous avez déjà évalué ce ticket.' },
68
+ { status: 409 },
69
+ )
70
+ }
71
+
72
+ const survey = await payload.create({
73
+ collection: slugs.satisfactionSurveys as any,
74
+ data: {
75
+ source: 'ticket',
76
+ ticket: ticketId,
77
+ client: req.user.id,
78
+ rating: Math.round(rating),
79
+ ...(comment ? { comment: comment.trim() } : {}),
80
+ },
81
+ overrideAccess: true,
82
+ })
83
+
84
+ return Response.json({ success: true, survey })
85
+ } catch (error) {
86
+ const authResponse = handleAuthError(error)
87
+ if (authResponse) return authResponse
88
+ console.error('[satisfaction] Error:', error)
89
+ return Response.json({ error: 'Erreur interne.' }, { status: 500 })
90
+ }
91
+ },
92
+ }
93
+ }
@@ -0,0 +1,106 @@
1
+ import type { Endpoint } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import { requireAdmin, handleAuthError } from '../utils/auth'
4
+
5
+ /**
6
+ * GET /api/support/search?q=term
7
+ * Global search across tickets, messages, clients, knowledge base.
8
+ * Admin-only.
9
+ */
10
+ export function createSearchEndpoint(slugs: CollectionSlugs): Endpoint {
11
+ return {
12
+ path: '/support/search',
13
+ method: 'get',
14
+ handler: async (req) => {
15
+ try {
16
+ const payload = req.payload
17
+
18
+ requireAdmin(req, slugs)
19
+
20
+ const url = new URL(req.url!)
21
+ const q = url.searchParams.get('q')?.trim()
22
+ if (!q || q.length < 2) {
23
+ return Response.json({ tickets: [], clients: [], messages: [], articles: [] })
24
+ }
25
+
26
+ const [ticketsRes, clientsRes, messagesRes, articlesRes] = await Promise.all([
27
+ payload.find({
28
+ collection: slugs.tickets as any,
29
+ where: {
30
+ or: [
31
+ { ticketNumber: { contains: q } },
32
+ { subject: { contains: q } },
33
+ ],
34
+ },
35
+ sort: '-updatedAt',
36
+ limit: 8,
37
+ depth: 1,
38
+ overrideAccess: true,
39
+ }),
40
+ payload.find({
41
+ collection: slugs.supportClients as any,
42
+ where: {
43
+ or: [
44
+ { firstName: { contains: q } },
45
+ { lastName: { contains: q } },
46
+ { email: { contains: q } },
47
+ { company: { contains: q } },
48
+ ],
49
+ },
50
+ limit: 5,
51
+ depth: 0,
52
+ overrideAccess: true,
53
+ }),
54
+ payload.find({
55
+ collection: slugs.ticketMessages as any,
56
+ where: { body: { contains: q } },
57
+ sort: '-createdAt',
58
+ limit: 5,
59
+ depth: 1,
60
+ overrideAccess: true,
61
+ }),
62
+ payload.find({
63
+ collection: slugs.knowledgeBase as any,
64
+ where: { title: { contains: q } },
65
+ limit: 5,
66
+ depth: 0,
67
+ overrideAccess: true,
68
+ }),
69
+ ])
70
+
71
+ return Response.json({
72
+ tickets: ticketsRes.docs.map((t: any) => ({
73
+ id: t.id,
74
+ ticketNumber: t.ticketNumber,
75
+ subject: t.subject,
76
+ status: t.status,
77
+ client: typeof t.client === 'object' ? { firstName: t.client?.firstName, company: t.client?.company } : null,
78
+ })),
79
+ clients: clientsRes.docs.map((c: any) => ({
80
+ id: c.id,
81
+ firstName: c.firstName,
82
+ lastName: c.lastName,
83
+ email: c.email,
84
+ company: c.company,
85
+ })),
86
+ messages: messagesRes.docs.map((m: any) => ({
87
+ id: m.id,
88
+ body: typeof m.body === 'string' ? m.body.slice(0, 100) : '',
89
+ ticketId: typeof m.ticket === 'object' ? m.ticket?.id : m.ticket,
90
+ ticketNumber: typeof m.ticket === 'object' ? m.ticket?.ticketNumber : null,
91
+ })),
92
+ articles: articlesRes.docs.map((a: any) => ({
93
+ id: a.id,
94
+ title: a.title,
95
+ slug: a.slug,
96
+ })),
97
+ })
98
+ } catch (error) {
99
+ const authResponse = handleAuthError(error)
100
+ if (authResponse) return authResponse
101
+ console.error('[search] Error:', error)
102
+ return Response.json({ error: 'Internal server error' }, { status: 500 })
103
+ }
104
+ },
105
+ }
106
+ }
@@ -0,0 +1,153 @@
1
+ import type { Endpoint } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import { requireAdmin, handleAuthError } from '../utils/auth'
4
+
5
+ const KB_ARTICLES = [
6
+ {
7
+ title: 'Comment créer un ticket de support ?',
8
+ slug: 'comment-creer-un-ticket',
9
+ category: 'getting-started',
10
+ body: 'Pour créer un ticket de support, connectez-vous à votre espace client puis cliquez sur "Nouveau ticket". Remplissez le sujet et la description de votre demande. Vous pouvez ajouter des pièces jointes (captures d\'écran, documents) pour nous aider à comprendre votre problème. Notre équipe vous répondra dans les meilleurs délais.',
11
+ },
12
+ {
13
+ title: 'Comment suivre l\'avancement de mon ticket ?',
14
+ slug: 'suivre-avancement-ticket',
15
+ category: 'tickets',
16
+ body: 'Rendez-vous sur votre tableau de bord support. Vous y trouverez la liste de tous vos tickets avec leur statut actuel (Ouvert, En attente, Résolu). Cliquez sur un ticket pour voir la conversation complète et ajouter des messages. Vous recevez aussi des notifications par email à chaque réponse de notre équipe.',
17
+ },
18
+ {
19
+ title: 'Quels sont les délais de réponse ?',
20
+ slug: 'delais-de-reponse',
21
+ category: 'tickets',
22
+ body: 'Notre équipe s\'engage à répondre à votre ticket dans un délai de 2 heures ouvrées (lundi-vendredi, 9h-18h). Les tickets marqués "Urgent" sont traités en priorité. En dehors des heures ouvrées, votre ticket sera traité dès la reprise d\'activité.',
23
+ },
24
+ {
25
+ title: 'Comment modifier mon mot de passe ?',
26
+ slug: 'modifier-mot-de-passe',
27
+ category: 'account',
28
+ body: 'Accédez à votre profil depuis le menu en haut à droite. Dans la section "Sécurité", vous trouverez le formulaire de changement de mot de passe. Entrez votre mot de passe actuel puis définissez votre nouveau mot de passe (minimum 8 caractères). Cliquez sur "Sauvegarder" pour confirmer.',
29
+ },
30
+ {
31
+ title: 'Comment activer l\'authentification à deux facteurs (2FA) ?',
32
+ slug: 'activer-2fa',
33
+ category: 'account',
34
+ body: 'L\'authentification à deux facteurs renforce la sécurité de votre compte. Accédez à votre profil, section "Sécurité", et activez le toggle 2FA. Lors de votre prochaine connexion, un code de vérification sera envoyé par email. Entrez ce code pour accéder à votre espace.',
35
+ },
36
+ {
37
+ title: 'Comment ajouter des pièces jointes à un ticket ?',
38
+ slug: 'ajouter-pieces-jointes',
39
+ category: 'tickets',
40
+ body: 'Vous pouvez joindre des fichiers à vos messages en cliquant sur le bouton "Joindre un fichier" sous l\'éditeur de message, ou en glissant-déposant directement vos fichiers. Les formats acceptés sont : images (PNG, JPG, GIF), documents (PDF, DOC, DOCX, TXT) et archives (ZIP). Taille maximale : 5 Mo par fichier.',
41
+ },
42
+ {
43
+ title: 'Mon site web ne s\'affiche plus, que faire ?',
44
+ slug: 'site-ne-saffiche-plus',
45
+ category: 'technical',
46
+ body: 'Si votre site ne s\'affiche plus : 1) Vérifiez votre connexion internet. 2) Videz le cache de votre navigateur. 3) Essayez en navigation privée. 4) Si le problème persiste, créez un ticket urgent en précisant le message d\'erreur et l\'URL concernée.',
47
+ },
48
+ {
49
+ title: 'Comment demander une modification sur mon site ?',
50
+ slug: 'demander-modification-site',
51
+ category: 'getting-started',
52
+ body: 'Créez un ticket avec la catégorie "Modification de contenu". Décrivez précisément la modification souhaitée : page concernée, texte à modifier, images à remplacer, etc. Joignez des captures d\'écran si nécessaire.',
53
+ },
54
+ {
55
+ title: 'Quels sont les tarifs de support ?',
56
+ slug: 'tarifs-support',
57
+ category: 'billing',
58
+ body: 'Le support technique est inclus dans votre contrat de maintenance. Les demandes de modification de contenu et les nouvelles fonctionnalités sont facturées au temps passé selon le taux horaire défini dans votre contrat.',
59
+ },
60
+ {
61
+ title: 'Comment exporter mes données personnelles (RGPD) ?',
62
+ slug: 'export-donnees-rgpd',
63
+ category: 'account',
64
+ body: 'Conformément au RGPD, vous pouvez demander l\'export de toutes vos données personnelles. Rendez-vous dans votre profil, section "Données personnelles", et cliquez sur "Exporter mes données".',
65
+ },
66
+ {
67
+ title: 'Comment fonctionne la connexion Google (SSO) ?',
68
+ slug: 'connexion-google-sso',
69
+ category: 'account',
70
+ body: 'Vous pouvez vous connecter avec votre compte Google. Sur la page de connexion, cliquez sur "Se connecter avec Google". Si c\'est votre première connexion, un compte sera automatiquement créé.',
71
+ },
72
+ {
73
+ title: 'Que signifient les différents statuts de ticket ?',
74
+ slug: 'statuts-ticket',
75
+ category: 'tickets',
76
+ body: 'Ouvert : votre ticket a été reçu et est en cours de traitement. En attente : nous attendons une réponse de votre part. Résolu : le problème a été résolu. Vous pouvez rouvrir un ticket résolu en y répondant.',
77
+ },
78
+ ]
79
+
80
+ /**
81
+ * POST /api/support/seed-kb
82
+ * Seed the knowledge base with default FAQ articles. Admin-only.
83
+ */
84
+ export function createSeedKbEndpoint(slugs: CollectionSlugs): Endpoint {
85
+ return {
86
+ path: '/support/seed-kb',
87
+ method: 'post',
88
+ handler: async (req) => {
89
+ try {
90
+ const payload = req.payload
91
+
92
+ requireAdmin(req, slugs)
93
+
94
+ let created = 0
95
+ let skipped = 0
96
+
97
+ for (const article of KB_ARTICLES) {
98
+ const existing = await payload.find({
99
+ collection: slugs.knowledgeBase as any,
100
+ where: { slug: { equals: article.slug } },
101
+ limit: 1,
102
+ depth: 0,
103
+ overrideAccess: true,
104
+ })
105
+
106
+ if (existing.docs.length > 0) {
107
+ skipped++
108
+ continue
109
+ }
110
+
111
+ const lexicalBody = {
112
+ root: {
113
+ type: 'root',
114
+ children: article.body.split('. ').map((sentence) => ({
115
+ type: 'paragraph',
116
+ children: [{ type: 'text', text: sentence.trim() + (sentence.endsWith('.') ? '' : '.'), version: 1 }],
117
+ direction: 'ltr',
118
+ format: '',
119
+ indent: 0,
120
+ version: 1,
121
+ })),
122
+ direction: 'ltr',
123
+ format: '',
124
+ indent: 0,
125
+ version: 1,
126
+ },
127
+ }
128
+
129
+ await payload.create({
130
+ collection: slugs.knowledgeBase as any,
131
+ data: {
132
+ title: article.title,
133
+ slug: article.slug,
134
+ category: article.category,
135
+ body: lexicalBody as any,
136
+ published: true,
137
+ sortOrder: created + 1,
138
+ },
139
+ overrideAccess: true,
140
+ })
141
+ created++
142
+ }
143
+
144
+ return Response.json({ created, skipped, total: KB_ARTICLES.length })
145
+ } catch (error) {
146
+ const authResponse = handleAuthError(error)
147
+ if (authResponse) return authResponse
148
+ console.error('[seed-kb] Error:', error)
149
+ return Response.json({ error: 'Internal server error' }, { status: 500 })
150
+ }
151
+ },
152
+ }
153
+ }
@@ -0,0 +1,144 @@
1
+ import type { Endpoint } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import { requireAdmin, handleAuthError } from '../utils/auth'
4
+
5
+ const PREF_KEY = 'support-settings'
6
+
7
+ interface SupportSettings {
8
+ email: {
9
+ fromAddress: string
10
+ fromName: string
11
+ replyToAddress: string
12
+ }
13
+ ai: {
14
+ provider: 'anthropic' | 'openai' | 'gemini' | 'ollama'
15
+ model: string
16
+ enableSentiment: boolean
17
+ enableSynthesis: boolean
18
+ enableSuggestion: boolean
19
+ enableRewrite: boolean
20
+ }
21
+ sla: {
22
+ firstResponseMinutes: number
23
+ resolutionMinutes: number
24
+ businessHoursOnly: boolean
25
+ escalationEmail: string
26
+ }
27
+ autoClose: {
28
+ enabled: boolean
29
+ daysBeforeClose: number
30
+ reminderDaysBefore: number
31
+ }
32
+ }
33
+
34
+ const DEFAULT_SUPPORT_SETTINGS: SupportSettings = {
35
+ email: { fromAddress: '', fromName: 'Support', replyToAddress: '' },
36
+ ai: {
37
+ provider: 'anthropic',
38
+ model: 'claude-haiku-4-5-20251001',
39
+ enableSentiment: true,
40
+ enableSynthesis: true,
41
+ enableSuggestion: true,
42
+ enableRewrite: true,
43
+ },
44
+ sla: {
45
+ firstResponseMinutes: 120,
46
+ resolutionMinutes: 1440,
47
+ businessHoursOnly: true,
48
+ escalationEmail: '',
49
+ },
50
+ autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 },
51
+ }
52
+
53
+ /**
54
+ * GET /api/support/settings — Read support settings
55
+ */
56
+ export function createSettingsGetEndpoint(slugs: CollectionSlugs): Endpoint {
57
+ return {
58
+ path: '/support/settings',
59
+ method: 'get',
60
+ handler: async (req) => {
61
+ try {
62
+ const payload = req.payload
63
+
64
+ requireAdmin(req, slugs)
65
+
66
+ const prefs = await payload.find({
67
+ collection: 'payload-preferences' as any,
68
+ where: { key: { equals: PREF_KEY } },
69
+ limit: 1,
70
+ depth: 0,
71
+ overrideAccess: true,
72
+ })
73
+
74
+ let settings = { ...DEFAULT_SUPPORT_SETTINGS }
75
+ if (prefs.docs.length > 0) {
76
+ const stored = prefs.docs[0].value as Partial<SupportSettings>
77
+ settings = {
78
+ email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...stored.email },
79
+ ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...stored.ai },
80
+ sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...stored.sla },
81
+ autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...stored.autoClose },
82
+ }
83
+ }
84
+
85
+ return Response.json(settings)
86
+ } catch (error) {
87
+ const authResponse = handleAuthError(error)
88
+ if (authResponse) return authResponse
89
+ console.warn('[support/settings] GET error:', error)
90
+ return Response.json({ error: 'Error' }, { status: 500 })
91
+ }
92
+ },
93
+ }
94
+ }
95
+
96
+ /**
97
+ * POST /api/support/settings — Save all support settings (admin-only)
98
+ */
99
+ export function createSettingsPostEndpoint(slugs: CollectionSlugs): Endpoint {
100
+ return {
101
+ path: '/support/settings',
102
+ method: 'post',
103
+ handler: async (req) => {
104
+ try {
105
+ const payload = req.payload
106
+
107
+ requireAdmin(req, slugs)
108
+
109
+ const body = (await req.json!()) as Partial<SupportSettings>
110
+
111
+ const merged: SupportSettings = {
112
+ email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...body.email },
113
+ ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...body.ai },
114
+ sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...body.sla },
115
+ autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...body.autoClose },
116
+ }
117
+
118
+ await payload.db.upsert({
119
+ collection: 'payload-preferences',
120
+ data: {
121
+ key: PREF_KEY,
122
+ user: { relationTo: req.user!.collection, value: req.user!.id },
123
+ value: merged as unknown as Record<string, unknown>,
124
+ },
125
+ req: { payload, user: req.user } as any,
126
+ where: {
127
+ and: [
128
+ { key: { equals: PREF_KEY } },
129
+ { 'user.value': { equals: req.user!.id } },
130
+ { 'user.relationTo': { equals: req.user!.collection } },
131
+ ],
132
+ },
133
+ })
134
+
135
+ return Response.json(merged)
136
+ } catch (error) {
137
+ const authResponse = handleAuthError(error)
138
+ if (authResponse) return authResponse
139
+ console.error('[support/settings] Error saving settings:', error)
140
+ return Response.json({ error: 'Error' }, { status: 500 })
141
+ }
142
+ },
143
+ }
144
+ }
@@ -0,0 +1,93 @@
1
+ import type { Endpoint } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import { requireAdmin, handleAuthError } from '../utils/auth'
4
+
5
+ const PREF_KEY = 'email-signature'
6
+
7
+ /**
8
+ * GET /api/support/signature — Get current admin's email signature
9
+ */
10
+ export function createSignatureGetEndpoint(slugs: CollectionSlugs): Endpoint {
11
+ return {
12
+ path: '/support/signature',
13
+ method: 'get',
14
+ handler: async (req) => {
15
+ try {
16
+ const payload = req.payload
17
+
18
+ requireAdmin(req, slugs)
19
+
20
+ const prefs = await payload.find({
21
+ collection: 'payload-preferences' as any,
22
+ where: { key: { equals: `${PREF_KEY}-${req.user.id}` } },
23
+ limit: 1,
24
+ depth: 0,
25
+ overrideAccess: true,
26
+ })
27
+
28
+ const signature = prefs.docs.length > 0
29
+ ? (prefs.docs[0].value as { signature?: string })?.signature || ''
30
+ : ''
31
+
32
+ return Response.json({ signature })
33
+ } catch (error) {
34
+ const authResponse = handleAuthError(error)
35
+ if (authResponse) return authResponse
36
+ console.error('[signature] GET error:', error)
37
+ return Response.json({ signature: '' })
38
+ }
39
+ },
40
+ }
41
+ }
42
+
43
+ /**
44
+ * POST /api/support/signature — Save current admin's email signature
45
+ */
46
+ export function createSignaturePostEndpoint(slugs: CollectionSlugs): Endpoint {
47
+ return {
48
+ path: '/support/signature',
49
+ method: 'post',
50
+ handler: async (req) => {
51
+ try {
52
+ const payload = req.payload
53
+
54
+ requireAdmin(req, slugs)
55
+
56
+ const { signature } = (await req.json!()) as { signature: string }
57
+ const key = `${PREF_KEY}-${req.user.id}`
58
+
59
+ const existing = await payload.find({
60
+ collection: 'payload-preferences' as any,
61
+ where: { key: { equals: key } },
62
+ limit: 1,
63
+ depth: 0,
64
+ overrideAccess: true,
65
+ })
66
+
67
+ await payload.db.upsert({
68
+ collection: 'payload-preferences',
69
+ data: {
70
+ key,
71
+ user: { relationTo: req.user!.collection, value: req.user!.id },
72
+ value: { signature: signature || '' },
73
+ },
74
+ req: { payload, user: req.user } as any,
75
+ where: {
76
+ and: [
77
+ { key: { equals: key } },
78
+ { 'user.value': { equals: req.user!.id } },
79
+ { 'user.relationTo': { equals: req.user!.collection } },
80
+ ],
81
+ },
82
+ })
83
+
84
+ return Response.json({ signature: signature || '' })
85
+ } catch (error) {
86
+ const authResponse = handleAuthError(error)
87
+ if (authResponse) return authResponse
88
+ console.error('[signature] POST error:', error)
89
+ return Response.json({ error: 'Error saving signature' }, { status: 500 })
90
+ }
91
+ },
92
+ }
93
+ }