@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,84 @@
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/export-csv
7
+ * Admin-only endpoint to export all tickets as CSV.
8
+ */
9
+ export function createExportCsvEndpoint(slugs: CollectionSlugs): Endpoint {
10
+ return {
11
+ path: '/support/export-csv',
12
+ method: 'get',
13
+ handler: async (req) => {
14
+ try {
15
+ const payload = req.payload
16
+
17
+ requireAdmin(req, slugs)
18
+
19
+ const PAGE_SIZE = 500
20
+ let page = 1
21
+ let hasMore = true
22
+ const allDocs: Array<Record<string, unknown>> = []
23
+
24
+ while (hasMore) {
25
+ const batch = await payload.find({
26
+ collection: slugs.tickets as any,
27
+ limit: PAGE_SIZE,
28
+ page,
29
+ depth: 1,
30
+ sort: '-createdAt',
31
+ overrideAccess: true,
32
+ })
33
+ allDocs.push(...(batch.docs as unknown as Array<Record<string, unknown>>))
34
+ hasMore = batch.hasNextPage ?? false
35
+ page++
36
+ }
37
+
38
+ const csvHeaders = [
39
+ 'N° Ticket', 'Sujet', 'Statut', 'Priorité', 'Catégorie',
40
+ 'Client', 'Email Client', 'Projet', 'Tags', 'Assigné à',
41
+ 'Temps (min)', 'Créé le', 'Première réponse', 'Résolu le',
42
+ ]
43
+
44
+ const csvRows = allDocs.map((t) => {
45
+ const client = typeof t.client === 'object' ? (t.client as Record<string, unknown>) : null
46
+ const project = typeof t.project === 'object' ? (t.project as Record<string, unknown>) : null
47
+ const assignedTo = typeof t.assignedTo === 'object' ? (t.assignedTo as Record<string, unknown>) : null
48
+ const tags = Array.isArray(t.tags) ? t.tags.join(', ') : ''
49
+
50
+ return [
51
+ String(t.ticketNumber || ''),
52
+ `"${String(t.subject || '').replace(/"/g, '""')}"`,
53
+ String(t.status || ''),
54
+ String(t.priority || ''),
55
+ String(t.category || ''),
56
+ client ? `"${String(client.company || '').replace(/"/g, '""')}"` : '',
57
+ client ? String(client.email || '') : '',
58
+ project ? `"${String(project.name || '').replace(/"/g, '""')}"` : '',
59
+ `"${tags}"`,
60
+ assignedTo ? String(assignedTo.email || '') : '',
61
+ String(t.totalTimeMinutes || 0),
62
+ t.createdAt ? new Date(String(t.createdAt)).toISOString() : '',
63
+ t.firstResponseAt ? new Date(String(t.firstResponseAt)).toISOString() : '',
64
+ t.resolvedAt ? new Date(String(t.resolvedAt)).toISOString() : '',
65
+ ].join(',')
66
+ })
67
+
68
+ const csv = [csvHeaders.join(','), ...csvRows].join('\n')
69
+
70
+ return new Response(csv, {
71
+ headers: {
72
+ 'Content-Type': 'text/csv; charset=utf-8',
73
+ 'Content-Disposition': `attachment; filename="tickets-export-${new Date().toISOString().split('T')[0]}.csv"`,
74
+ },
75
+ })
76
+ } catch (error) {
77
+ const authResponse = handleAuthError(error)
78
+ if (authResponse) return authResponse
79
+ console.error('[export-csv] Error:', error)
80
+ return Response.json({ error: 'Internal error' }, { status: 500 })
81
+ }
82
+ },
83
+ }
84
+ }
@@ -0,0 +1,104 @@
1
+ import type { Endpoint } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import { requireClient, handleAuthError } from '../utils/auth'
4
+
5
+ /**
6
+ * GET /api/support/export-data
7
+ * RGPD Data Export — allows support clients to download all their personal data.
8
+ */
9
+ export function createExportDataEndpoint(slugs: CollectionSlugs): Endpoint {
10
+ return {
11
+ path: '/support/export-data',
12
+ method: 'get',
13
+ handler: async (req) => {
14
+ try {
15
+ const payload = req.payload
16
+
17
+ requireClient(req, slugs)
18
+
19
+ const [clientData, ticketsResult, messagesResult, surveysResult] = await Promise.all([
20
+ payload.findByID({
21
+ collection: slugs.supportClients as any,
22
+ id: req.user.id,
23
+ depth: 0,
24
+ overrideAccess: true,
25
+ }),
26
+ payload.find({
27
+ collection: slugs.tickets as any,
28
+ where: { client: { equals: req.user.id } },
29
+ limit: 1000,
30
+ depth: 0,
31
+ overrideAccess: true,
32
+ }),
33
+ payload.find({
34
+ collection: slugs.ticketMessages as any,
35
+ where: {
36
+ 'ticket.client': { equals: req.user.id },
37
+ authorType: { equals: 'client' },
38
+ },
39
+ limit: 5000,
40
+ depth: 0,
41
+ overrideAccess: true,
42
+ }),
43
+ payload.find({
44
+ collection: slugs.satisfactionSurveys as any,
45
+ where: { client: { equals: req.user.id } },
46
+ limit: 500,
47
+ depth: 0,
48
+ overrideAccess: true,
49
+ }),
50
+ ])
51
+
52
+ const c = clientData as any
53
+ const exportData = {
54
+ exportDate: new Date().toISOString(),
55
+ exportType: 'RGPD - Export des données personnelles',
56
+ profile: {
57
+ email: c.email,
58
+ firstName: c.firstName,
59
+ lastName: c.lastName,
60
+ company: c.company,
61
+ phone: c.phone || null,
62
+ createdAt: c.createdAt,
63
+ updatedAt: c.updatedAt,
64
+ },
65
+ tickets: ticketsResult.docs.map((t: any) => ({
66
+ ticketNumber: t.ticketNumber,
67
+ subject: t.subject,
68
+ status: t.status,
69
+ priority: t.priority,
70
+ category: t.category,
71
+ createdAt: t.createdAt,
72
+ updatedAt: t.updatedAt,
73
+ })),
74
+ messages: messagesResult.docs.map((m: any) => ({
75
+ ticketId: m.ticket,
76
+ body: m.body,
77
+ createdAt: m.createdAt,
78
+ })),
79
+ surveys: surveysResult.docs.map((s: any) => ({
80
+ ticketId: s.ticket,
81
+ rating: s.rating,
82
+ comment: s.comment,
83
+ createdAt: s.createdAt,
84
+ })),
85
+ }
86
+
87
+ const json = JSON.stringify(exportData, null, 2)
88
+
89
+ return new Response(json, {
90
+ status: 200,
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ 'Content-Disposition': `attachment; filename="support-export-${req.user.id}-${new Date().toISOString().slice(0, 10)}.json"`,
94
+ },
95
+ })
96
+ } catch (error) {
97
+ const authResponse = handleAuthError(error)
98
+ if (authResponse) return authResponse
99
+ console.error('[export-data] Error:', error)
100
+ return Response.json({ error: 'Erreur interne' }, { status: 500 })
101
+ }
102
+ },
103
+ }
104
+ }
@@ -0,0 +1,307 @@
1
+ import type { Endpoint } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import { RateLimiter } from '../utils/rateLimiter'
4
+ import { readSupportSettings } from '../utils/readSettings'
5
+ import crypto from 'crypto'
6
+
7
+ interface ParsedConversation {
8
+ client: { email: string; name: string; company: string }
9
+ subject: string
10
+ messages: { from: 'client' | 'admin'; name: string; date: string; content: string }[]
11
+ }
12
+
13
+ const importLimiter = new RateLimiter(3_600_000, 10) // 10 per hour
14
+
15
+ function parseStructuredMarkdown(markdown: string): ParsedConversation | null {
16
+ const clientMatch = markdown.match(
17
+ /\*\*Client\s*:\*\*\s*(.+?)\s*[—–-]\s*(.+?)\s*\(([^)]+@[^)]+)\)/i,
18
+ )
19
+ const subjectMatch = markdown.match(/\*\*Sujet\s*:\*\*\s*(.+)/i)
20
+
21
+ if (!clientMatch || !subjectMatch) return null
22
+
23
+ const client = {
24
+ name: clientMatch[1]!.trim(),
25
+ company: clientMatch[2]!.trim(),
26
+ email: clientMatch[3]!.trim().toLowerCase(),
27
+ }
28
+
29
+ const subject = subjectMatch[1]!.trim()
30
+ const adminEmail = (process.env.CONTACT_EMAIL || 'admin@example.com').toLowerCase()
31
+
32
+ const blocks = markdown.split(/## Message \d+/).slice(1)
33
+ const messages: ParsedConversation['messages'] = []
34
+
35
+ for (const block of blocks) {
36
+ const fromMatch = block.match(/\*\*De\s*:\*\*\s*(.+?)\s*\(([^)]+)\)/)
37
+ const dateMatch = block.match(/\*\*Date\s*:\*\*\s*(.+)/)
38
+
39
+ if (!fromMatch) continue
40
+
41
+ const name = fromMatch[1]!.trim()
42
+ const email = fromMatch[2]!.trim().toLowerCase()
43
+ const date = dateMatch ? dateMatch[1]!.trim() : ''
44
+
45
+ const lines = block.split('\n')
46
+ let contentStart = 0
47
+ let foundDate = false
48
+ for (let i = 0; i < lines.length; i++) {
49
+ if (lines[i]!.startsWith('**Date')) { foundDate = true; continue }
50
+ if (foundDate && lines[i]!.trim() === '') { contentStart = i + 1; break }
51
+ }
52
+
53
+ const content = lines.slice(contentStart).join('\n').replace(/\n---\s*$/s, '').trim()
54
+ if (!content) continue
55
+
56
+ messages.push({
57
+ from: email === adminEmail ? 'admin' : 'client',
58
+ name,
59
+ date,
60
+ content,
61
+ })
62
+ }
63
+
64
+ if (messages.length === 0) return null
65
+ return { client, subject, messages }
66
+ }
67
+
68
+ async function parseMarkdownWithAI(markdown: string): Promise<ParsedConversation | null> {
69
+ if (!process.env.ANTHROPIC_API_KEY) return null
70
+
71
+ try {
72
+ const Anthropic = require('@anthropic-ai/sdk').default
73
+ const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
74
+
75
+ const response = await anthropic.messages.create({
76
+ model: 'claude-haiku-4-5-20251001',
77
+ max_tokens: 4096,
78
+ messages: [
79
+ {
80
+ role: 'user',
81
+ content: `Parse this email conversation markdown and return strict JSON.
82
+
83
+ Extract:
84
+ - Client info (the external person, NOT the admin)
85
+ - Subject
86
+ - All messages chronologically
87
+
88
+ The admin email is: ${process.env.CONTACT_EMAIL || 'admin@example.com'}
89
+
90
+ Return JSON: { "client": { "email": "", "name": "", "company": "" }, "subject": "", "messages": [{ "from": "client"|"admin", "name": "", "date": "", "content": "" }] }
91
+
92
+ Markdown:
93
+ ---
94
+ ${markdown}
95
+ ---
96
+
97
+ ONLY JSON, nothing else.`,
98
+ },
99
+ ],
100
+ })
101
+
102
+ const text = response.content[0]?.type === 'text' ? response.content[0].text.trim() : ''
103
+ const jsonMatch = text.match(/\{[\s\S]*\}/)
104
+ if (!jsonMatch) return null
105
+
106
+ const parsed = JSON.parse(jsonMatch[0])
107
+ if (!parsed.client?.email || !parsed.messages?.length) return null
108
+
109
+ return {
110
+ client: {
111
+ email: parsed.client.email.toLowerCase().trim(),
112
+ name: parsed.client.name || parsed.client.email.split('@')[0],
113
+ company: parsed.client.company || 'Non renseigné',
114
+ },
115
+ subject: parsed.subject || 'Conversation importée',
116
+ messages: parsed.messages.map((m: any) => ({
117
+ from: m.from === 'admin' ? 'admin' : 'client',
118
+ name: m.name || '',
119
+ date: m.date || '',
120
+ content: m.content || '',
121
+ })),
122
+ }
123
+ } catch (err) {
124
+ console.error('[import-conversation] AI parsing failed:', err)
125
+ return null
126
+ }
127
+ }
128
+
129
+ /**
130
+ * POST /api/support/import-conversation
131
+ * Import a conversation from markdown into the ticket system.
132
+ */
133
+ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoint {
134
+ return {
135
+ path: '/support/import-conversation',
136
+ method: 'post',
137
+ handler: async (req) => {
138
+ try {
139
+ const payload = req.payload
140
+
141
+ // Auth: admin session OR webhook secret
142
+ const webhookSecret = req.headers.get('x-webhook-secret')
143
+ let isAuthed = false
144
+
145
+ if (webhookSecret && process.env.SUPPORT_WEBHOOK_SECRET && webhookSecret === process.env.SUPPORT_WEBHOOK_SECRET) {
146
+ isAuthed = true
147
+ } else if (req.user && req.user.collection === slugs.users) {
148
+ isAuthed = true
149
+ }
150
+
151
+ if (!isAuthed) {
152
+ return Response.json({ error: 'Unauthorized' }, { status: 401 })
153
+ }
154
+
155
+ const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
156
+ if (importLimiter.check(ip)) {
157
+ return Response.json({ error: 'Rate limit exceeded. Maximum 10 imports per hour.' }, { status: 429 })
158
+ }
159
+
160
+ let body: { markdown?: string; previewOnly?: boolean }
161
+ try {
162
+ body = await req.json!()
163
+ } catch {
164
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
165
+ }
166
+ const { markdown, previewOnly } = body
167
+
168
+ if (!markdown || typeof markdown !== 'string') {
169
+ return Response.json({ error: 'markdown field is required (string)' }, { status: 400 })
170
+ }
171
+
172
+ if (markdown.length > 512_000) {
173
+ return Response.json({ error: 'Markdown too large (max 500KB)' }, { status: 400 })
174
+ }
175
+
176
+ let conversation = parseStructuredMarkdown(markdown)
177
+ let parseMethod = 'structured'
178
+
179
+ if (!conversation) {
180
+ conversation = await parseMarkdownWithAI(markdown)
181
+ parseMethod = 'ai'
182
+ }
183
+
184
+ if (!conversation) {
185
+ return Response.json({
186
+ error: 'Could not parse conversation. Expected format: **Client :** Name — Company (email), **Sujet :** Subject, ## Message N blocks.',
187
+ }, { status: 422 })
188
+ }
189
+
190
+ if (previewOnly) {
191
+ return Response.json({
192
+ action: 'preview',
193
+ parseMethod,
194
+ client: conversation.client,
195
+ subject: conversation.subject,
196
+ messageCount: conversation.messages.length,
197
+ messages: conversation.messages.map((m) => ({
198
+ from: m.from,
199
+ name: m.name,
200
+ date: m.date,
201
+ preview: m.content.length > 100 ? m.content.substring(0, 100) + '...' : m.content,
202
+ })),
203
+ })
204
+ }
205
+
206
+ if (!conversation.client.email.includes('@')) {
207
+ return Response.json({ error: 'Invalid client email extracted' }, { status: 422 })
208
+ }
209
+
210
+ const settings = await readSupportSettings(payload)
211
+ const adminEmail = (process.env.CONTACT_EMAIL || '').toLowerCase()
212
+ const blockedEmails = [adminEmail, settings.email.replyToAddress || process.env.SUPPORT_REPLY_TO || ''].filter(Boolean)
213
+ if (blockedEmails.includes(conversation.client.email)) {
214
+ return Response.json({ error: 'Cannot create ticket from system email address' }, { status: 400 })
215
+ }
216
+
217
+ // Find or create support client
218
+ const clientResult = await payload.find({
219
+ collection: slugs.supportClients as any,
220
+ where: { email: { equals: conversation.client.email } },
221
+ limit: 1,
222
+ depth: 0,
223
+ overrideAccess: true,
224
+ })
225
+
226
+ let client = clientResult.docs[0]
227
+ let isNewClient = false
228
+
229
+ if (!client) {
230
+ const nameParts = conversation.client.name.split(' ')
231
+ const randomPassword = crypto.randomBytes(48).toString('base64url')
232
+
233
+ client = await payload.create({
234
+ collection: slugs.supportClients as any,
235
+ data: {
236
+ email: conversation.client.email,
237
+ password: randomPassword,
238
+ firstName: nameParts[0] || 'Inconnu',
239
+ lastName: nameParts.slice(1).join(' ') || conversation.client.email.split('@')[0] || 'Inconnu',
240
+ company: conversation.client.company || 'Non renseigné',
241
+ },
242
+ overrideAccess: true,
243
+ })
244
+ isNewClient = true
245
+ }
246
+
247
+ // Find admin user for authorAdmin
248
+ const adminUsers = await payload.find({
249
+ collection: slugs.users as any,
250
+ limit: 1,
251
+ depth: 0,
252
+ overrideAccess: true,
253
+ })
254
+ const adminUserId = adminUsers.docs[0]?.id
255
+
256
+ // Create ticket
257
+ const ticket = await payload.create({
258
+ collection: slugs.tickets as any,
259
+ data: {
260
+ subject: conversation.subject,
261
+ client: client.id,
262
+ status: 'open',
263
+ priority: 'normal',
264
+ category: 'question',
265
+ },
266
+ overrideAccess: true,
267
+ }) as any
268
+
269
+ // Import messages
270
+ let importedCount = 0
271
+ for (const msg of conversation.messages) {
272
+ const isAdmin = msg.from === 'admin'
273
+
274
+ await payload.create({
275
+ collection: slugs.ticketMessages as any,
276
+ data: {
277
+ ticket: ticket.id,
278
+ body: msg.content,
279
+ authorType: isAdmin ? 'admin' : 'email',
280
+ ...(isAdmin && adminUserId ? { authorAdmin: adminUserId } : {}),
281
+ ...(!isAdmin ? { authorClient: client.id } : {}),
282
+ isInternal: false,
283
+ skipNotification: true,
284
+ },
285
+ overrideAccess: true,
286
+ })
287
+ importedCount++
288
+ }
289
+
290
+ return Response.json({
291
+ action: 'conversation_imported',
292
+ parseMethod,
293
+ ticketNumber: ticket.ticketNumber,
294
+ ticketId: ticket.id,
295
+ clientEmail: conversation.client.email,
296
+ clientName: conversation.client.name,
297
+ clientCompany: conversation.client.company,
298
+ isNewClient,
299
+ messagesImported: importedCount,
300
+ })
301
+ } catch (error) {
302
+ console.error('[import-conversation] Error:', error)
303
+ return Response.json({ error: 'Internal server error' }, { status: 500 })
304
+ }
305
+ },
306
+ }
307
+ }
@@ -0,0 +1,154 @@
1
+ import type { Endpoint } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import type { SupportFeatures } from '../types'
4
+
5
+ import { createAiEndpoint } from './ai'
6
+ import { createSearchEndpoint } from './search'
7
+ import { createBulkActionEndpoint } from './bulk-action'
8
+ import { createMergeTicketsEndpoint } from './merge-tickets'
9
+ import { createSplitTicketEndpoint } from './split-ticket'
10
+ import { createTypingPostEndpoint, createTypingGetEndpoint } from './typing'
11
+ import { createPresencePostEndpoint, createPresenceGetEndpoint } from './presence'
12
+ import { createSettingsGetEndpoint, createSettingsPostEndpoint } from './settings'
13
+ import { createSignatureGetEndpoint, createSignaturePostEndpoint } from './signature'
14
+ import { createRoundRobinConfigGetEndpoint, createRoundRobinConfigPostEndpoint } from './round-robin-config'
15
+ import { createSlaCheckEndpoint } from './sla-check'
16
+ import { createAutoCloseEndpoint } from './auto-close'
17
+ import { createStatusesEndpoint } from './statuses'
18
+ import { createApplyMacroEndpoint } from './apply-macro'
19
+ import { createPurgeLogsEndpoint } from './purge-logs'
20
+ import { createChatbotEndpoint } from './chatbot'
21
+ import { createChatGetEndpoint, createChatPostEndpoint } from './chat'
22
+ import { createChatStreamEndpoint } from './chat-stream'
23
+ import { createAdminChatGetEndpoint, createAdminChatPostEndpoint } from './admin-chat'
24
+ import { createAdminChatStreamEndpoint } from './admin-chat-stream'
25
+ import { createAdminStatsEndpoint } from './admin-stats'
26
+ import { createBillingEndpoint } from './billing'
27
+ import { createEmailStatsEndpoint } from './email-stats'
28
+ import { createSatisfactionEndpoint } from './satisfaction'
29
+ import { createTrackOpenEndpoint } from './track-open'
30
+ import { createExportCsvEndpoint } from './export-csv'
31
+ import { createExportDataEndpoint } from './export-data'
32
+ import { createPendingEmailsProcessEndpoint } from './pending-emails-process'
33
+ import { createResendNotificationEndpoint } from './resend-notification'
34
+ import { createSeedKbEndpoint } from './seed-kb'
35
+ import { createLoginEndpoint } from './login'
36
+ import { createAuth2faEndpoint } from './auth-2fa'
37
+ import { createOAuthGoogleEndpoint, type OAuthGoogleOptions } from './oauth-google'
38
+ import { createDeleteAccountEndpoint } from './delete-account'
39
+ import { createMergeClientsEndpoint } from './merge-clients'
40
+ import { createImportConversationEndpoint } from './import-conversation'
41
+ import { createProcessScheduledEndpoint } from './process-scheduled'
42
+ import { createUserPrefsGetEndpoint, createUserPrefsPostEndpoint } from './user-prefs'
43
+
44
+ // Re-export all individual endpoint creators
45
+ export { createAiEndpoint } from './ai'
46
+ export { createSearchEndpoint } from './search'
47
+ export { createBulkActionEndpoint } from './bulk-action'
48
+ export { createMergeTicketsEndpoint } from './merge-tickets'
49
+ export { createSplitTicketEndpoint } from './split-ticket'
50
+ export { createTypingPostEndpoint, createTypingGetEndpoint } from './typing'
51
+ export { createPresencePostEndpoint, createPresenceGetEndpoint } from './presence'
52
+ export { createSettingsGetEndpoint, createSettingsPostEndpoint } from './settings'
53
+ export { createSignatureGetEndpoint, createSignaturePostEndpoint } from './signature'
54
+ export { createRoundRobinConfigGetEndpoint, createRoundRobinConfigPostEndpoint } from './round-robin-config'
55
+ export { createSlaCheckEndpoint } from './sla-check'
56
+ export { createAutoCloseEndpoint } from './auto-close'
57
+ export { createStatusesEndpoint } from './statuses'
58
+ export { createApplyMacroEndpoint } from './apply-macro'
59
+ export { createPurgeLogsEndpoint } from './purge-logs'
60
+ export { createChatbotEndpoint } from './chatbot'
61
+ export { createChatGetEndpoint, createChatPostEndpoint } from './chat'
62
+ export { createChatStreamEndpoint } from './chat-stream'
63
+ export { createAdminChatGetEndpoint, createAdminChatPostEndpoint } from './admin-chat'
64
+ export { createAdminChatStreamEndpoint } from './admin-chat-stream'
65
+ export { createAdminStatsEndpoint } from './admin-stats'
66
+ export { createBillingEndpoint } from './billing'
67
+ export { createEmailStatsEndpoint } from './email-stats'
68
+ export { createSatisfactionEndpoint } from './satisfaction'
69
+ export { createTrackOpenEndpoint } from './track-open'
70
+ export { createExportCsvEndpoint } from './export-csv'
71
+ export { createExportDataEndpoint } from './export-data'
72
+ export { createPendingEmailsProcessEndpoint } from './pending-emails-process'
73
+ export { createResendNotificationEndpoint } from './resend-notification'
74
+ export { createSeedKbEndpoint } from './seed-kb'
75
+ export { createLoginEndpoint } from './login'
76
+ export { createAuth2faEndpoint } from './auth-2fa'
77
+ export { createOAuthGoogleEndpoint } from './oauth-google'
78
+ export { createDeleteAccountEndpoint } from './delete-account'
79
+ export { createMergeClientsEndpoint } from './merge-clients'
80
+ export { createImportConversationEndpoint } from './import-conversation'
81
+ export { createProcessScheduledEndpoint } from './process-scheduled'
82
+ export { createUserPrefsGetEndpoint, createUserPrefsPostEndpoint } from './user-prefs'
83
+ export type { UserPrefs } from './user-prefs'
84
+
85
+ export interface SupportEndpointOptions {
86
+ oauth?: OAuthGoogleOptions
87
+ features?: Required<SupportFeatures>
88
+ }
89
+
90
+ /**
91
+ * Create all support endpoints for the Payload plugin.
92
+ * Returns an array of Endpoint objects to be registered via `endpoints` in the plugin config.
93
+ * Endpoints are conditionally included based on feature flags.
94
+ */
95
+ export function createSupportEndpoints(slugs: CollectionSlugs, options?: SupportEndpointOptions): Endpoint[] {
96
+ const f = options?.features
97
+
98
+ // Core endpoints (always present)
99
+ const endpoints: Endpoint[] = [
100
+ createSearchEndpoint(slugs),
101
+ createSettingsGetEndpoint(slugs),
102
+ createSettingsPostEndpoint(slugs),
103
+ createAdminStatsEndpoint(slugs),
104
+ createExportCsvEndpoint(slugs),
105
+ createExportDataEndpoint(slugs),
106
+ createSeedKbEndpoint(slugs),
107
+ createLoginEndpoint(slugs),
108
+ createAuth2faEndpoint(slugs),
109
+ createOAuthGoogleEndpoint(slugs, options?.oauth),
110
+ createDeleteAccountEndpoint(slugs),
111
+ createMergeClientsEndpoint(slugs),
112
+ createImportConversationEndpoint(slugs),
113
+ createPurgeLogsEndpoint(slugs),
114
+ createResendNotificationEndpoint(slugs),
115
+ createUserPrefsGetEndpoint(slugs),
116
+ createUserPrefsPostEndpoint(slugs),
117
+ ]
118
+
119
+ // Conditional endpoints based on feature flags
120
+ if (!f || f.ai !== false) endpoints.push(createAiEndpoint(slugs))
121
+ if (!f || f.bulkActions !== false) endpoints.push(createBulkActionEndpoint(slugs))
122
+ if (!f || f.merge !== false) endpoints.push(createMergeTicketsEndpoint(slugs))
123
+ if (!f || f.splitTicket !== false) endpoints.push(createSplitTicketEndpoint(slugs))
124
+ if (!f || f.collisionDetection !== false) {
125
+ endpoints.push(createTypingPostEndpoint(slugs), createTypingGetEndpoint(slugs))
126
+ endpoints.push(createPresencePostEndpoint(slugs), createPresenceGetEndpoint(slugs))
127
+ }
128
+ if (!f || f.signatures !== false) {
129
+ endpoints.push(createSignatureGetEndpoint(slugs), createSignaturePostEndpoint(slugs))
130
+ }
131
+ if (!f || f.sla !== false) endpoints.push(createSlaCheckEndpoint(slugs))
132
+ if (!f || f.autoClose !== false) endpoints.push(createAutoCloseEndpoint(slugs))
133
+ if (!f || f.customStatuses !== false) endpoints.push(createStatusesEndpoint(slugs))
134
+ if (!f || f.macros !== false) endpoints.push(createApplyMacroEndpoint(slugs))
135
+ if (!f || f.roundRobin !== false) {
136
+ endpoints.push(createRoundRobinConfigGetEndpoint(slugs), createRoundRobinConfigPostEndpoint(slugs))
137
+ }
138
+ if (!f || f.chatbot !== false) endpoints.push(createChatbotEndpoint(slugs))
139
+ if (!f || f.chat !== false) {
140
+ endpoints.push(createChatGetEndpoint(slugs), createChatPostEndpoint(slugs))
141
+ endpoints.push(createChatStreamEndpoint(slugs))
142
+ endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs))
143
+ endpoints.push(createAdminChatStreamEndpoint(slugs))
144
+ }
145
+ if (!f || f.timeTracking !== false) endpoints.push(createBillingEndpoint(slugs))
146
+ if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs))
147
+ if (!f || f.emailTracking !== false) {
148
+ endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs))
149
+ }
150
+ if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs))
151
+ if (!f || f.scheduledReplies !== false) endpoints.push(createProcessScheduledEndpoint(slugs))
152
+
153
+ return endpoints
154
+ }