@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.
- package/LICENSE +21 -0
- package/README.md +525 -0
- package/dist/client.cjs +7 -0
- package/dist/client.d.cts +3 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +5 -0
- package/dist/index.cjs +7766 -0
- package/dist/index.d.cts +384 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.js +7730 -0
- package/dist/views.d.cts +30 -0
- package/dist/views.d.ts +30 -0
- package/package.json +131 -0
- package/src/client.ts +1 -0
- package/src/collections/AuthLogs.ts +65 -0
- package/src/collections/CannedResponses.ts +69 -0
- package/src/collections/ChatMessages.ts +98 -0
- package/src/collections/EmailLogs.ts +94 -0
- package/src/collections/KnowledgeBase.ts +99 -0
- package/src/collections/Macros.ts +98 -0
- package/src/collections/PendingEmails.ts +122 -0
- package/src/collections/SatisfactionSurveys.ts +98 -0
- package/src/collections/SlaPolicies.ts +123 -0
- package/src/collections/SupportClients.ts +210 -0
- package/src/collections/TicketActivityLog.ts +81 -0
- package/src/collections/TicketMessages.ts +364 -0
- package/src/collections/TicketStatuses.ts +108 -0
- package/src/collections/Tickets.ts +704 -0
- package/src/collections/TimeEntries.ts +105 -0
- package/src/collections/WebhookEndpoints.ts +96 -0
- package/src/collections/index.ts +16 -0
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
- package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
- package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
- package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
- package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
- package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
- package/src/components/TicketConversation/config.ts +82 -0
- package/src/components/TicketConversation/constants.ts +74 -0
- package/src/components/TicketConversation/context.ts +63 -0
- package/src/components/TicketConversation/hooks/useAI.ts +180 -0
- package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
- package/src/components/TicketConversation/hooks/useReply.ts +190 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
- package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
- package/src/components/TicketConversation/index.tsx +1110 -0
- package/src/components/TicketConversation/locales/en.json +878 -0
- package/src/components/TicketConversation/locales/fr.json +878 -0
- package/src/components/TicketConversation/types.ts +54 -0
- package/src/components/TicketConversation/utils.ts +25 -0
- package/src/endpoints/admin-chat-stream.ts +238 -0
- package/src/endpoints/admin-chat.ts +263 -0
- package/src/endpoints/admin-stats.ts +200 -0
- package/src/endpoints/ai.ts +199 -0
- package/src/endpoints/apply-macro.ts +144 -0
- package/src/endpoints/auth-2fa.ts +163 -0
- package/src/endpoints/auto-close.ts +175 -0
- package/src/endpoints/billing.ts +167 -0
- package/src/endpoints/bulk-action.ts +103 -0
- package/src/endpoints/chat-stream.ts +127 -0
- package/src/endpoints/chat.ts +188 -0
- package/src/endpoints/chatbot.ts +113 -0
- package/src/endpoints/delete-account.ts +129 -0
- package/src/endpoints/email-stats.ts +109 -0
- package/src/endpoints/export-csv.ts +84 -0
- package/src/endpoints/export-data.ts +104 -0
- package/src/endpoints/import-conversation.ts +307 -0
- package/src/endpoints/index.ts +154 -0
- package/src/endpoints/login.ts +92 -0
- package/src/endpoints/merge-clients.ts +132 -0
- package/src/endpoints/merge-tickets.ts +137 -0
- package/src/endpoints/oauth-google.ts +179 -0
- package/src/endpoints/pending-emails-process.ts +224 -0
- package/src/endpoints/presence.ts +104 -0
- package/src/endpoints/process-scheduled.ts +144 -0
- package/src/endpoints/purge-logs.ts +58 -0
- package/src/endpoints/resend-notification.ts +99 -0
- package/src/endpoints/round-robin-config.ts +92 -0
- package/src/endpoints/satisfaction.ts +93 -0
- package/src/endpoints/search.ts +106 -0
- package/src/endpoints/seed-kb.ts +153 -0
- package/src/endpoints/settings.ts +144 -0
- package/src/endpoints/signature.ts +93 -0
- package/src/endpoints/sla-check.ts +124 -0
- package/src/endpoints/split-ticket.ts +131 -0
- package/src/endpoints/statuses.ts +45 -0
- package/src/endpoints/track-open.ts +154 -0
- package/src/endpoints/typing.ts +101 -0
- package/src/endpoints/user-prefs.ts +125 -0
- package/src/hooks/checkSLA.ts +414 -0
- package/src/hooks/ticketStatusEmail.ts +182 -0
- package/src/index.ts +51 -0
- package/src/plugin.ts +157 -0
- package/src/portal/LiveChat.tsx +1353 -0
- package/src/portal/auth/ChatWidget.tsx +350 -0
- package/src/portal/auth/ChatbotWidget.tsx +285 -0
- package/src/portal/auth/SupportHeader.tsx +409 -0
- package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
- package/src/portal/auth/dashboard/page.tsx +84 -0
- package/src/portal/auth/faq/FAQSearch.tsx +117 -0
- package/src/portal/auth/faq/page.tsx +199 -0
- package/src/portal/auth/layout.tsx +61 -0
- package/src/portal/auth/profile/page.tsx +705 -0
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
- package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
- package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
- package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
- package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
- package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
- package/src/portal/auth/tickets/detail/page.tsx +738 -0
- package/src/portal/auth/tickets/new/page.tsx +515 -0
- package/src/portal/forgot-password/page.tsx +114 -0
- package/src/portal/layout.tsx +26 -0
- package/src/portal/locales/en.json +374 -0
- package/src/portal/locales/fr.json +374 -0
- package/src/portal/login/page.tsx +351 -0
- package/src/portal/page.tsx +162 -0
- package/src/portal/register/page.tsx +281 -0
- package/src/portal/reset-password/page.tsx +152 -0
- package/src/styles/BillingView.module.scss +311 -0
- package/src/styles/ChatView.module.scss +438 -0
- package/src/styles/CommandPalette.module.scss +160 -0
- package/src/styles/CrmView.module.scss +554 -0
- package/src/styles/EmailTracking.module.scss +238 -0
- package/src/styles/ImportConversation.module.scss +267 -0
- package/src/styles/Layout.module.scss +55 -0
- package/src/styles/Logs.module.scss +164 -0
- package/src/styles/NewTicket.module.scss +143 -0
- package/src/styles/PendingEmails.module.scss +629 -0
- package/src/styles/SupportDashboard.module.scss +649 -0
- package/src/styles/TicketDetail.module.scss +1043 -0
- package/src/styles/TicketInbox.module.scss +296 -0
- package/src/styles/TicketingSettings.module.scss +358 -0
- package/src/styles/TimeDashboard.module.scss +287 -0
- package/src/styles/_tokens.scss +78 -0
- package/src/styles/theme.css +633 -0
- package/src/types.ts +255 -0
- package/src/utils/adminNotification.ts +38 -0
- package/src/utils/auth.ts +46 -0
- package/src/utils/emailTemplate.ts +343 -0
- package/src/utils/fireWebhooks.ts +84 -0
- package/src/utils/index.ts +22 -0
- package/src/utils/rateLimiter.ts +52 -0
- package/src/utils/readSettings.ts +67 -0
- package/src/utils/slugs.ts +54 -0
- package/src/utils/webhookDispatcher.ts +120 -0
- package/src/views/BillingView/client.tsx +137 -0
- package/src/views/BillingView/index.tsx +33 -0
- package/src/views/ChatView/client.tsx +294 -0
- package/src/views/ChatView/index.tsx +33 -0
- package/src/views/CrmView/client.tsx +206 -0
- package/src/views/CrmView/index.tsx +33 -0
- package/src/views/EmailTrackingView/client.tsx +124 -0
- package/src/views/EmailTrackingView/index.tsx +33 -0
- package/src/views/ImportConversationView/client.tsx +133 -0
- package/src/views/ImportConversationView/index.tsx +33 -0
- package/src/views/LogsView/client.tsx +151 -0
- package/src/views/LogsView/index.tsx +30 -0
- package/src/views/NewTicketView/client.tsx +227 -0
- package/src/views/NewTicketView/index.tsx +30 -0
- package/src/views/PendingEmailsView/client.tsx +177 -0
- package/src/views/PendingEmailsView/index.tsx +33 -0
- package/src/views/SupportDashboardView/client.tsx +424 -0
- package/src/views/SupportDashboardView/index.tsx +33 -0
- package/src/views/TicketDetailView/client.tsx +775 -0
- package/src/views/TicketDetailView/index.tsx +33 -0
- package/src/views/TicketInboxView/client.tsx +313 -0
- package/src/views/TicketInboxView/index.tsx +30 -0
- package/src/views/TicketingSettingsView/client.tsx +866 -0
- package/src/views/TicketingSettingsView/index.tsx +33 -0
- package/src/views/TimeDashboardView/client.tsx +144 -0
- package/src/views/TimeDashboardView/index.tsx +33 -0
- package/src/views/shared/AdminViewHeader.tsx +69 -0
- package/src/views/shared/ErrorBoundary.tsx +68 -0
- package/src/views/shared/Skeleton.tsx +125 -0
- package/src/views/shared/adminTokens.ts +37 -0
- package/src/views/shared/config.ts +82 -0
- package/src/views/shared/index.ts +6 -0
- 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
|
+
}
|