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