@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,200 @@
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/admin-stats
7
+ * Admin-only endpoint returning support analytics.
8
+ * Uses count queries and paginated aggregation to avoid loading all tickets in memory.
9
+ */
10
+ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
11
+ return {
12
+ path: '/support/admin-stats',
13
+ method: 'get',
14
+ handler: async (req) => {
15
+ try {
16
+ const payload = req.payload
17
+
18
+ requireAdmin(req, slugs)
19
+
20
+ // ── Status counts via individual count queries ──
21
+ const statuses = ['open', 'waiting_client', 'resolved'] as const
22
+ const statusCounts = await Promise.all(
23
+ statuses.map(async (status) => {
24
+ const result = await payload.count({
25
+ collection: slugs.tickets as any,
26
+ where: { status: { equals: status } },
27
+ overrideAccess: true,
28
+ })
29
+ return [status, result.totalDocs] as const
30
+ }),
31
+ )
32
+ const byStatus: Record<string, number> = Object.fromEntries(statusCounts)
33
+ const total = statusCounts.reduce((sum, [, count]) => sum + count, 0)
34
+
35
+ // ── Priority counts ──
36
+ const priorities = ['low', 'normal', 'high', 'urgent'] as const
37
+ const priorityCounts = await Promise.all(
38
+ priorities.map(async (priority) => {
39
+ const result = await payload.count({
40
+ collection: slugs.tickets as any,
41
+ where: { priority: { equals: priority } },
42
+ overrideAccess: true,
43
+ })
44
+ return [priority, result.totalDocs] as const
45
+ }),
46
+ )
47
+ const byPriority: Record<string, number> = Object.fromEntries(priorityCounts)
48
+
49
+ // ── Category counts ──
50
+ const categories = ['bug', 'content', 'feature', 'question', 'hosting'] as const
51
+ const categoryCounts = await Promise.all(
52
+ categories.map(async (category) => {
53
+ const result = await payload.count({
54
+ collection: slugs.tickets as any,
55
+ where: { category: { equals: category } },
56
+ overrideAccess: true,
57
+ })
58
+ return [category, result.totalDocs] as const
59
+ }),
60
+ )
61
+ const byCategory: Record<string, number> = Object.fromEntries(
62
+ categoryCounts.filter(([, count]) => count > 0),
63
+ )
64
+
65
+ // ── Time-based counts ──
66
+ const now = new Date()
67
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
68
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
69
+
70
+ const [created7, created30] = await Promise.all([
71
+ payload.count({
72
+ collection: slugs.tickets as any,
73
+ where: { createdAt: { greater_than_equal: sevenDaysAgo.toISOString() } },
74
+ overrideAccess: true,
75
+ }),
76
+ payload.count({
77
+ collection: slugs.tickets as any,
78
+ where: { createdAt: { greater_than_equal: thirtyDaysAgo.toISOString() } },
79
+ overrideAccess: true,
80
+ }),
81
+ ])
82
+
83
+ // ── Averages: paginate with limit:100, only fetch needed fields ──
84
+ const PAGE_SIZE = 100
85
+ const MAX_PAGES = 50
86
+ let totalResponseTimeMs = 0
87
+ let responseTimeCount = 0
88
+ let totalResolutionTimeMs = 0
89
+ let resolutionTimeCount = 0
90
+ let totalTimeMinutes = 0
91
+
92
+ let page = 1
93
+ let hasMore = true
94
+
95
+ while (hasMore && page <= MAX_PAGES) {
96
+ const batch = await payload.find({
97
+ collection: slugs.tickets as any,
98
+ limit: PAGE_SIZE,
99
+ page,
100
+ depth: 0,
101
+ overrideAccess: true,
102
+ select: {
103
+ firstResponseAt: true,
104
+ resolvedAt: true,
105
+ createdAt: true,
106
+ totalTimeMinutes: true,
107
+ },
108
+ })
109
+
110
+ for (const t of batch.docs) {
111
+ const doc = t as Record<string, unknown>
112
+ if (doc.firstResponseAt && doc.createdAt) {
113
+ const responseTime = new Date(String(doc.firstResponseAt)).getTime() - new Date(String(doc.createdAt)).getTime()
114
+ if (responseTime > 0) { totalResponseTimeMs += responseTime; responseTimeCount++ }
115
+ }
116
+ if (doc.resolvedAt && doc.createdAt) {
117
+ const resolutionTime = new Date(String(doc.resolvedAt)).getTime() - new Date(String(doc.createdAt)).getTime()
118
+ if (resolutionTime > 0) { totalResolutionTimeMs += resolutionTime; resolutionTimeCount++ }
119
+ }
120
+ totalTimeMinutes += (doc.totalTimeMinutes as number) || 0
121
+ }
122
+
123
+ hasMore = batch.hasNextPage ?? false
124
+ page++
125
+ }
126
+
127
+ // ── Satisfaction: use count + paginated average ──
128
+ const surveyCount = await payload.count({
129
+ collection: slugs.satisfactionSurveys as any,
130
+ overrideAccess: true,
131
+ })
132
+
133
+ let satisfactionAvg = 0
134
+ if (surveyCount.totalDocs > 0) {
135
+ let totalRating = 0
136
+ let surveyPage = 1
137
+ let surveyHasMore = true
138
+ while (surveyHasMore && surveyPage <= MAX_PAGES) {
139
+ const batch = await payload.find({
140
+ collection: slugs.satisfactionSurveys as any,
141
+ limit: PAGE_SIZE,
142
+ page: surveyPage,
143
+ depth: 0,
144
+ overrideAccess: true,
145
+ select: { rating: true },
146
+ })
147
+ for (const s of batch.docs) {
148
+ totalRating += ((s as any).rating || 0)
149
+ }
150
+ surveyHasMore = batch.hasNextPage ?? false
151
+ surveyPage++
152
+ }
153
+ satisfactionAvg = Math.round((totalRating / surveyCount.totalDocs) * 10) / 10
154
+ }
155
+
156
+ const [clientCount, pendingEmailsCount] = await Promise.all([
157
+ payload.count({
158
+ collection: slugs.supportClients as any,
159
+ overrideAccess: true,
160
+ }),
161
+ payload.count({
162
+ collection: slugs.pendingEmails as any,
163
+ where: { status: { equals: 'pending' } },
164
+ overrideAccess: true,
165
+ }),
166
+ ])
167
+
168
+ return new Response(JSON.stringify({
169
+ total,
170
+ byStatus,
171
+ byPriority,
172
+ byCategory,
173
+ createdLast7Days: created7.totalDocs,
174
+ createdLast30Days: created30.totalDocs,
175
+ avgResponseTimeHours: responseTimeCount > 0
176
+ ? Math.round((totalResponseTimeMs / responseTimeCount / (1000 * 60 * 60)) * 10) / 10
177
+ : null,
178
+ avgResolutionTimeHours: resolutionTimeCount > 0
179
+ ? Math.round((totalResolutionTimeMs / resolutionTimeCount / (1000 * 60 * 60)) * 10) / 10
180
+ : null,
181
+ totalTimeMinutes,
182
+ satisfactionAvg,
183
+ satisfactionCount: surveyCount.totalDocs,
184
+ clientCount: clientCount.totalDocs,
185
+ pendingEmailsCount: pendingEmailsCount.totalDocs,
186
+ }), {
187
+ headers: {
188
+ 'Content-Type': 'application/json',
189
+ 'Cache-Control': 'private, max-age=300, stale-while-revalidate=600',
190
+ },
191
+ })
192
+ } catch (error) {
193
+ const authResponse = handleAuthError(error)
194
+ if (authResponse) return authResponse
195
+ console.error('[admin-stats] Error:', error)
196
+ return Response.json({ error: 'Internal error' }, { status: 500 })
197
+ }
198
+ },
199
+ }
200
+ }
@@ -0,0 +1,199 @@
1
+ import type { Endpoint } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+ import { requireAdmin, handleAuthError } from '../utils/auth'
4
+ import { readSupportSettings, type SupportSettings } from '../utils/readSettings'
5
+
6
+ function getClient(aiSettings: SupportSettings['ai']) {
7
+ // Dynamic import to avoid hard dependency
8
+ const Anthropic = require('@anthropic-ai/sdk').default
9
+ if (aiSettings.provider === 'ollama') {
10
+ const baseURL = process.env.OLLAMA_API_URL || 'https://ollama.orkelis.app/v1'
11
+ return new Anthropic({ apiKey: 'ollama', baseURL })
12
+ }
13
+ return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
14
+ }
15
+
16
+ function getModel(aiSettings: SupportSettings['ai']): string {
17
+ return aiSettings.model || 'claude-haiku-4-5-20251001'
18
+ }
19
+
20
+ type AiAction = 'sentiment' | 'synthesis' | 'suggest_reply' | 'rewrite'
21
+
22
+ /**
23
+ * POST /api/support/ai
24
+ * Admin-only endpoint for AI features in support.
25
+ */
26
+ export function createAiEndpoint(slugs: CollectionSlugs): Endpoint {
27
+ return {
28
+ path: '/support/ai',
29
+ method: 'post',
30
+ handler: async (req) => {
31
+ try {
32
+ const payload = req.payload
33
+
34
+ requireAdmin(req, slugs)
35
+
36
+ const settings = await readSupportSettings(payload)
37
+ const aiSettings = settings.ai
38
+ let body: Record<string, unknown>
39
+ try {
40
+ body = await req.json!()
41
+ } catch {
42
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
43
+ }
44
+ const { action } = body as { action: AiAction }
45
+
46
+ const anthropic = getClient(aiSettings)
47
+ const model = getModel(aiSettings)
48
+
49
+ if (action === 'sentiment') {
50
+ if (!aiSettings.enableSentiment) {
51
+ return Response.json({ sentiment: 'neutre', disabled: true })
52
+ }
53
+ const { text } = body as { text: string }
54
+ if (!text) return Response.json({ error: 'text required' }, { status: 400 })
55
+
56
+ const res = await anthropic.messages.create({
57
+ model,
58
+ max_tokens: 20,
59
+ messages: [
60
+ {
61
+ role: 'user',
62
+ content: `Analyse le sentiment de ce message de support client. Réponds UNIQUEMENT par un seul mot parmi : frustré, mécontent, neutre, satisfait, urgent. Pas d'explication.\n\nMessage : "${text.slice(0, 500)}"`,
63
+ },
64
+ ],
65
+ })
66
+
67
+ const raw = (res.content[0].type === 'text' ? res.content[0].text : '').toLowerCase().trim()
68
+ return Response.json({ sentiment: raw })
69
+ }
70
+
71
+ if (action === 'synthesis') {
72
+ if (!aiSettings.enableSynthesis) {
73
+ return Response.json({ synthesis: '', disabled: true })
74
+ }
75
+ const { messages: msgs, ticketSubject, clientName, clientCompany } = body as {
76
+ messages: Array<{ authorType: string; body: string; createdAt: string }>
77
+ ticketSubject: string
78
+ clientName?: string
79
+ clientCompany?: string
80
+ }
81
+
82
+ const conversation = msgs
83
+ .map((m) => {
84
+ const author = m.authorType === 'admin' ? 'Support' : 'Client'
85
+ const date = new Date(m.createdAt).toLocaleDateString('fr-FR', {
86
+ day: 'numeric',
87
+ month: 'short',
88
+ hour: '2-digit',
89
+ minute: '2-digit',
90
+ timeZone: 'Europe/Paris',
91
+ })
92
+ return `[${date}] ${author}: ${m.body}`
93
+ })
94
+ .join('\n\n')
95
+
96
+ const prompt = `Tu es un agent de support technique senior. Analyse cette conversation de support et génère une synthèse structurée.
97
+
98
+ Sujet du ticket : ${ticketSubject}
99
+ Client : ${clientName || 'Inconnu'}${clientCompany ? ` — ${clientCompany}` : ''}
100
+
101
+ Conversation :
102
+ ${conversation}
103
+
104
+ Génère une synthèse avec ces sections (en markdown) :
105
+ ## Résumé
106
+ 2-3 phrases résumant la situation
107
+
108
+ ## Chronologie
109
+ - Points clés de la conversation
110
+
111
+ ## Problème principal
112
+ Description du problème ou de la demande
113
+
114
+ ## Actions
115
+ - Ce qui a été fait
116
+ - Prochaines étapes
117
+
118
+ ## Notes importantes
119
+ Points d'attention éventuels`
120
+
121
+ const res = await anthropic.messages.create({
122
+ model,
123
+ max_tokens: 1000,
124
+ messages: [{ role: 'user', content: prompt }],
125
+ })
126
+
127
+ const text = res.content[0].type === 'text' ? res.content[0].text : ''
128
+ return Response.json({ synthesis: text })
129
+ }
130
+
131
+ if (action === 'suggest_reply') {
132
+ if (!aiSettings.enableSuggestion) {
133
+ return Response.json({ reply: '', disabled: true })
134
+ }
135
+ const { messages: msgs, clientName, clientCompany } = body as {
136
+ messages: Array<{ authorType: string; body: string }>
137
+ clientName?: string
138
+ clientCompany?: string
139
+ }
140
+
141
+ const conversation = msgs
142
+ .slice(-10)
143
+ .map((m) => {
144
+ const author = m.authorType === 'admin' ? 'Support' : 'Client'
145
+ return `${author}: ${m.body}`
146
+ })
147
+ .join('\n\n')
148
+
149
+ const prompt = `Tu es un agent de support technique. Tu réponds de manière professionnelle, chaleureuse et concise en français.
150
+
151
+ Contexte client : ${clientCompany || ''} — ${clientName || 'client'}
152
+
153
+ Conversation récente :
154
+ ${conversation}
155
+
156
+ Rédige une réponse appropriée au dernier message du client. Sois concis (3-5 phrases max), professionnel mais chaleureux. Tutoie si le client tutoie, vouvoie sinon. Ne mets pas de signature.`
157
+
158
+ const res = await anthropic.messages.create({
159
+ model,
160
+ max_tokens: 500,
161
+ messages: [{ role: 'user', content: prompt }],
162
+ })
163
+
164
+ const text = res.content[0].type === 'text' ? res.content[0].text : ''
165
+ return Response.json({ reply: text })
166
+ }
167
+
168
+ if (action === 'rewrite') {
169
+ if (!aiSettings.enableRewrite) {
170
+ return Response.json({ rewritten: '', disabled: true })
171
+ }
172
+ const { text } = body as { text: string }
173
+ if (!text?.trim()) return Response.json({ error: 'text required' }, { status: 400 })
174
+
175
+ const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de manière plus professionnelle et corrige les fautes d'orthographe/grammaire. Garde le même sens et le même ton (tutoiement/vouvoiement). Ne change pas le fond du message, améliore uniquement la forme. Réponds UNIQUEMENT avec le texte reformulé, sans commentaire ni explication.
176
+
177
+ Texte original :
178
+ ${text}`
179
+
180
+ const res = await anthropic.messages.create({
181
+ model,
182
+ max_tokens: 500,
183
+ messages: [{ role: 'user', content: prompt }],
184
+ })
185
+
186
+ const rewritten = res.content[0].type === 'text' ? res.content[0].text : ''
187
+ return Response.json({ rewritten })
188
+ }
189
+
190
+ return Response.json({ error: 'Invalid action' }, { status: 400 })
191
+ } catch (error) {
192
+ const authResponse = handleAuthError(error)
193
+ if (authResponse) return authResponse
194
+ console.error('[support/ai] Error:', error)
195
+ return Response.json({ error: 'Internal server error' }, { status: 500 })
196
+ }
197
+ },
198
+ }
199
+ }
@@ -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
+ interface MacroAction {
6
+ type: 'set_status' | 'set_priority' | 'add_tag' | 'send_reply' | 'assign'
7
+ value: string
8
+ }
9
+
10
+ /**
11
+ * POST /api/support/apply-macro
12
+ * Apply a macro (multi-action shortcut) to a ticket. Admin-only.
13
+ */
14
+ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
15
+ return {
16
+ path: '/support/apply-macro',
17
+ method: 'post',
18
+ handler: async (req) => {
19
+ try {
20
+ const payload = req.payload
21
+
22
+ requireAdmin(req, slugs)
23
+
24
+ const body = await req.json!()
25
+ const { macroId, ticketId } = body
26
+
27
+ if (!macroId || !ticketId) {
28
+ return Response.json({ error: 'macroId and ticketId are required' }, { status: 400 })
29
+ }
30
+
31
+ const macro = await payload.findByID({
32
+ collection: slugs.macros as any,
33
+ id: macroId,
34
+ depth: 0,
35
+ overrideAccess: true,
36
+ }) as any
37
+
38
+ if (!macro) return Response.json({ error: 'Macro not found' }, { status: 404 })
39
+ if (!macro.isActive) return Response.json({ error: 'Macro is disabled' }, { status: 400 })
40
+
41
+ const ticket = await payload.findByID({
42
+ collection: slugs.tickets as any,
43
+ id: ticketId,
44
+ depth: 0,
45
+ overrideAccess: true,
46
+ }) as any
47
+
48
+ if (!ticket) return Response.json({ error: 'Ticket not found' }, { status: 404 })
49
+
50
+ const appliedActions: { type: string; value: string; success: boolean; error?: string }[] = []
51
+ const actions: MacroAction[] = macro.actions || []
52
+
53
+ for (const action of actions) {
54
+ try {
55
+ switch (action.type) {
56
+ case 'set_status':
57
+ await payload.update({
58
+ collection: slugs.tickets as any,
59
+ id: ticketId,
60
+ data: { status: action.value },
61
+ overrideAccess: true,
62
+ })
63
+ appliedActions.push({ type: action.type, value: action.value, success: true })
64
+ break
65
+
66
+ case 'set_priority':
67
+ await payload.update({
68
+ collection: slugs.tickets as any,
69
+ id: ticketId,
70
+ data: { priority: action.value },
71
+ overrideAccess: true,
72
+ })
73
+ appliedActions.push({ type: action.type, value: action.value, success: true })
74
+ break
75
+
76
+ case 'add_tag': {
77
+ const currentTags = Array.isArray(ticket.tags) ? [...ticket.tags] : []
78
+ if (!currentTags.includes(action.value)) {
79
+ currentTags.push(action.value)
80
+ }
81
+ await payload.update({
82
+ collection: slugs.tickets as any,
83
+ id: ticketId,
84
+ data: { tags: currentTags },
85
+ overrideAccess: true,
86
+ })
87
+ appliedActions.push({ type: action.type, value: action.value, success: true })
88
+ break
89
+ }
90
+
91
+ case 'send_reply':
92
+ await payload.create({
93
+ collection: slugs.ticketMessages as any,
94
+ data: {
95
+ ticket: ticketId,
96
+ body: action.value,
97
+ authorType: 'admin',
98
+ isInternal: false,
99
+ },
100
+ overrideAccess: true,
101
+ })
102
+ appliedActions.push({ type: action.type, value: action.value, success: true })
103
+ break
104
+
105
+ case 'assign': {
106
+ const userId = parseInt(action.value, 10)
107
+ if (isNaN(userId)) {
108
+ appliedActions.push({ type: action.type, value: action.value, success: false, error: 'Invalid user ID' })
109
+ break
110
+ }
111
+ await payload.update({
112
+ collection: slugs.tickets as any,
113
+ id: ticketId,
114
+ data: { assignedTo: userId },
115
+ overrideAccess: true,
116
+ })
117
+ appliedActions.push({ type: action.type, value: action.value, success: true })
118
+ break
119
+ }
120
+
121
+ default:
122
+ appliedActions.push({ type: action.type, value: action.value, success: false, error: 'Unknown action type' })
123
+ }
124
+ } catch (err) {
125
+ const errorMsg = err instanceof Error ? err.message : String(err)
126
+ appliedActions.push({ type: action.type, value: action.value, success: false, error: errorMsg })
127
+ }
128
+ }
129
+
130
+ return Response.json({
131
+ applied: true,
132
+ macroName: macro.name,
133
+ ticketId,
134
+ actions: appliedActions,
135
+ })
136
+ } catch (error) {
137
+ const authResponse = handleAuthError(error)
138
+ if (authResponse) return authResponse
139
+ console.error('[apply-macro] Error:', error)
140
+ return Response.json({ error: 'Internal server error' }, { status: 500 })
141
+ }
142
+ },
143
+ }
144
+ }