@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,84 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { headers as getHeaders } from 'next/headers'
|
|
3
|
+
import { getPayload } from 'payload'
|
|
4
|
+
import configPromise from '@payload-config'
|
|
5
|
+
import { DashboardClient } from './DashboardClient'
|
|
6
|
+
|
|
7
|
+
export default async function SupportDashboardPage() {
|
|
8
|
+
const payload = await getPayload({ config: configPromise })
|
|
9
|
+
const headers = await getHeaders()
|
|
10
|
+
const { user } = await payload.auth({ headers })
|
|
11
|
+
|
|
12
|
+
if (!user) return null
|
|
13
|
+
|
|
14
|
+
const tickets = await payload.find({
|
|
15
|
+
collection: 'tickets',
|
|
16
|
+
where: {
|
|
17
|
+
client: { equals: user.id },
|
|
18
|
+
},
|
|
19
|
+
sort: '-updatedAt',
|
|
20
|
+
limit: 200,
|
|
21
|
+
depth: 1,
|
|
22
|
+
overrideAccess: false,
|
|
23
|
+
user,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Fetch all messages for these tickets
|
|
27
|
+
const ticketIds = tickets.docs.map((t) => t.id)
|
|
28
|
+
const allMessages = ticketIds.length > 0
|
|
29
|
+
? await payload.find({
|
|
30
|
+
collection: 'ticket-messages',
|
|
31
|
+
where: {
|
|
32
|
+
ticket: { in: ticketIds },
|
|
33
|
+
},
|
|
34
|
+
sort: '-createdAt',
|
|
35
|
+
limit: 500,
|
|
36
|
+
depth: 0,
|
|
37
|
+
overrideAccess: false,
|
|
38
|
+
user,
|
|
39
|
+
})
|
|
40
|
+
: { docs: [] }
|
|
41
|
+
|
|
42
|
+
// Build map of lastClientReadAt per ticket
|
|
43
|
+
const readAtMap: Record<string | number, string | null> = {}
|
|
44
|
+
for (const t of tickets.docs) {
|
|
45
|
+
readAtMap[t.id] = t.lastClientReadAt || null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Build message meta per ticket (allMessages sorted by -createdAt, so first entry per ticket is the latest)
|
|
49
|
+
const ticketMeta: Record<string | number, { hasNew: boolean; count: number; lastMessagePreview: string }> = {}
|
|
50
|
+
for (const msg of allMessages.docs) {
|
|
51
|
+
const tid = typeof msg.ticket === 'object' ? msg.ticket.id : msg.ticket
|
|
52
|
+
if (!ticketMeta[tid]) {
|
|
53
|
+
const lastRead = readAtMap[tid]
|
|
54
|
+
const isUnread = msg.authorType === 'admin' && (!lastRead || new Date(msg.createdAt) > new Date(lastRead))
|
|
55
|
+
const preview = (msg.body || '').replace(/\n/g, ' ').slice(0, 80)
|
|
56
|
+
ticketMeta[tid] = { hasNew: isUnread, count: 1, lastMessagePreview: preview }
|
|
57
|
+
} else {
|
|
58
|
+
ticketMeta[tid].count++
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Serialize tickets for client component
|
|
63
|
+
const serializedTickets = tickets.docs.map((ticket) => {
|
|
64
|
+
const meta = ticketMeta[ticket.id]
|
|
65
|
+
const isClosed = ticket.status === 'resolved'
|
|
66
|
+
return {
|
|
67
|
+
id: ticket.id,
|
|
68
|
+
ticketNumber: ticket.ticketNumber,
|
|
69
|
+
subject: ticket.subject,
|
|
70
|
+
status: ticket.status,
|
|
71
|
+
priority: ticket.priority,
|
|
72
|
+
category: ticket.category,
|
|
73
|
+
projectName: ticket.project && typeof ticket.project === 'object' ? ticket.project.name : null,
|
|
74
|
+
updatedAt: ticket.updatedAt,
|
|
75
|
+
createdAt: ticket.createdAt,
|
|
76
|
+
hasNewMessage: (meta?.hasNew && !isClosed) || false,
|
|
77
|
+
messageCount: meta?.count || 0,
|
|
78
|
+
totalTimeMinutes: ticket.totalTimeMinutes,
|
|
79
|
+
lastMessagePreview: meta?.lastMessagePreview || null,
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return <DashboardClient tickets={serializedTickets} />
|
|
84
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState, useMemo } from 'react'
|
|
4
|
+
|
|
5
|
+
interface Article {
|
|
6
|
+
id: string
|
|
7
|
+
title: string
|
|
8
|
+
category: string
|
|
9
|
+
slug: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FAQSearch({ articles }: { articles: Article[] }) {
|
|
13
|
+
const [query, setQuery] = useState('')
|
|
14
|
+
const [isFocused, setIsFocused] = useState(false)
|
|
15
|
+
|
|
16
|
+
const results = useMemo(() => {
|
|
17
|
+
if (!query.trim()) return []
|
|
18
|
+
const q = query.toLowerCase()
|
|
19
|
+
return articles.filter((a) => a.title.toLowerCase().includes(q))
|
|
20
|
+
}, [query, articles])
|
|
21
|
+
|
|
22
|
+
const showResults = query.trim().length > 0
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="relative mb-10">
|
|
26
|
+
{/* Search input */}
|
|
27
|
+
<div className={`relative rounded-2xl border bg-white dark:bg-slate-800/50 shadow-sm backdrop-blur-sm transition-all duration-200 ${
|
|
28
|
+
isFocused
|
|
29
|
+
? 'border-blue-500 ring-4 ring-blue-500/10 shadow-md'
|
|
30
|
+
: 'border-slate-200 dark:border-slate-700/50 hover:border-slate-300 dark:hover:border-slate-600'
|
|
31
|
+
}`}>
|
|
32
|
+
<div className="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2">
|
|
33
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className={`h-5 w-5 transition-colors duration-200 ${isFocused ? 'text-blue-500' : 'text-slate-400 dark:text-slate-500'}`}>
|
|
34
|
+
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
|
|
35
|
+
</svg>
|
|
36
|
+
</div>
|
|
37
|
+
<input
|
|
38
|
+
type="text"
|
|
39
|
+
value={query}
|
|
40
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
41
|
+
onFocus={() => setIsFocused(true)}
|
|
42
|
+
onBlur={() => setIsFocused(false)}
|
|
43
|
+
placeholder="Rechercher un article..."
|
|
44
|
+
className="w-full bg-transparent py-4 pl-12 pr-4 text-sm text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none"
|
|
45
|
+
/>
|
|
46
|
+
{query && (
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={() => setQuery('')}
|
|
50
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 flex h-7 w-7 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-600 dark:hover:text-slate-300"
|
|
51
|
+
>
|
|
52
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
|
|
53
|
+
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
|
54
|
+
</svg>
|
|
55
|
+
</button>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Search results dropdown */}
|
|
60
|
+
{showResults && (
|
|
61
|
+
<div className="mt-2 rounded-xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800 shadow-lg">
|
|
62
|
+
{results.length === 0 ? (
|
|
63
|
+
<div className="flex flex-col items-center py-8 px-4">
|
|
64
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="mb-2 h-8 w-8 text-slate-300 dark:text-slate-600">
|
|
65
|
+
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
|
|
66
|
+
</svg>
|
|
67
|
+
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">
|
|
68
|
+
Aucun resultat pour « {query} »
|
|
69
|
+
</p>
|
|
70
|
+
<p className="mt-1 text-xs text-slate-400 dark:text-slate-500">Essayez avec d'autres mots-cles</p>
|
|
71
|
+
</div>
|
|
72
|
+
) : (
|
|
73
|
+
<div className="divide-y divide-slate-100 dark:divide-slate-700/50">
|
|
74
|
+
<div className="px-4 py-2.5">
|
|
75
|
+
<p className="text-xs font-medium text-slate-400 dark:text-slate-500">
|
|
76
|
+
{results.length} resultat{results.length > 1 ? 's' : ''}
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
{results.map((r) => (
|
|
80
|
+
<button
|
|
81
|
+
key={r.id}
|
|
82
|
+
onClick={() => {
|
|
83
|
+
// Find and open the details element
|
|
84
|
+
const details = document.querySelectorAll('details')
|
|
85
|
+
details.forEach((d) => {
|
|
86
|
+
const summary = d.querySelector('summary span')
|
|
87
|
+
if (summary?.textContent === r.title) {
|
|
88
|
+
d.open = true
|
|
89
|
+
d.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
setQuery('')
|
|
93
|
+
}}
|
|
94
|
+
className="group flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-slate-50 dark:hover:bg-slate-700/50"
|
|
95
|
+
>
|
|
96
|
+
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-slate-100 dark:bg-slate-700 transition-colors group-hover:bg-blue-50 dark:group-hover:bg-blue-900/30">
|
|
97
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4 text-slate-400 group-hover:text-blue-500 transition-colors">
|
|
98
|
+
<path fillRule="evenodd" d="M4.25 2A2.25 2.25 0 002 4.25v11.5A2.25 2.25 0 004.25 18h11.5A2.25 2.25 0 0018 15.75V4.25A2.25 2.25 0 0015.75 2H4.25zm4.03 6.28a.75.75 0 00-1.06-1.06L4.97 9.47a.75.75 0 000 1.06l2.25 2.25a.75.75 0 001.06-1.06L6.56 10l1.72-1.72zm2.44-1.06a.75.75 0 011.06 0l2.25 2.25a.75.75 0 010 1.06l-2.25 2.25a.75.75 0 11-1.06-1.06L12.44 10l-1.72-1.72a.75.75 0 010-1.06z" clipRule="evenodd" />
|
|
99
|
+
</svg>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="min-w-0 flex-1">
|
|
102
|
+
<p className="truncate text-sm font-medium text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
|
103
|
+
{r.title}
|
|
104
|
+
</p>
|
|
105
|
+
</div>
|
|
106
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4 flex-shrink-0 text-slate-300 dark:text-slate-600 transition-colors group-hover:text-blue-400">
|
|
107
|
+
<path fillRule="evenodd" d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z" clipRule="evenodd" />
|
|
108
|
+
</svg>
|
|
109
|
+
</button>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { getPayload } from 'payload'
|
|
3
|
+
import configPromise from '@payload-config'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { RichText } from '@payloadcms/richtext-lexical/react'
|
|
6
|
+
import { FAQSearch } from './FAQSearch'
|
|
7
|
+
|
|
8
|
+
export const revalidate = 300
|
|
9
|
+
|
|
10
|
+
const categoryConfig: Record<string, { label: string; icon: React.ReactNode; color: string }> = {
|
|
11
|
+
'getting-started': {
|
|
12
|
+
label: 'Premiers pas',
|
|
13
|
+
color: 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400',
|
|
14
|
+
icon: (
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-5 w-5">
|
|
16
|
+
<path fillRule="evenodd" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" clipRule="evenodd" />
|
|
17
|
+
</svg>
|
|
18
|
+
),
|
|
19
|
+
},
|
|
20
|
+
tickets: {
|
|
21
|
+
label: 'Tickets & Support',
|
|
22
|
+
color: 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400',
|
|
23
|
+
icon: (
|
|
24
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-5 w-5">
|
|
25
|
+
<path fillRule="evenodd" d="M5.25 2A2.25 2.25 0 003 4.25v2.879a2.25 2.25 0 00.659 1.59l7.5 7.502a2.25 2.25 0 003.182 0l2.879-2.879a2.25 2.25 0 000-3.182l-7.5-7.502A2.25 2.25 0 008.129 2H5.25zM6.5 6a.5.5 0 100-1 .5.5 0 000 1z" clipRule="evenodd" />
|
|
26
|
+
</svg>
|
|
27
|
+
),
|
|
28
|
+
},
|
|
29
|
+
account: {
|
|
30
|
+
label: 'Compte & Profil',
|
|
31
|
+
color: 'bg-violet-50 dark:bg-violet-900/20 text-violet-600 dark:text-violet-400',
|
|
32
|
+
icon: (
|
|
33
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-5 w-5">
|
|
34
|
+
<path d="M10 8a3 3 0 100-6 3 3 0 000 6zM3.465 14.493a1.23 1.23 0 00.41 1.412A9.957 9.957 0 0010 18c2.31 0 4.438-.784 6.131-2.1.43-.333.604-.903.408-1.41a7.002 7.002 0 00-13.074.003z" />
|
|
35
|
+
</svg>
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
billing: {
|
|
39
|
+
label: 'Facturation',
|
|
40
|
+
color: 'bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400',
|
|
41
|
+
icon: (
|
|
42
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-5 w-5">
|
|
43
|
+
<path fillRule="evenodd" d="M2.5 4A1.5 1.5 0 001 5.5V6h18v-.5A1.5 1.5 0 0017.5 4h-15zM19 8.5H1v6A1.5 1.5 0 002.5 16h15a1.5 1.5 0 001.5-1.5v-6zM3 13.25a.75.75 0 01.75-.75h1.5a.75.75 0 010 1.5h-1.5a.75.75 0 01-.75-.75zm4.75-.75a.75.75 0 000 1.5h3.5a.75.75 0 000-1.5h-3.5z" clipRule="evenodd" />
|
|
44
|
+
</svg>
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
technical: {
|
|
48
|
+
label: 'Technique',
|
|
49
|
+
color: 'bg-slate-100 dark:bg-slate-700/50 text-slate-600 dark:text-slate-400',
|
|
50
|
+
icon: (
|
|
51
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-5 w-5">
|
|
52
|
+
<path fillRule="evenodd" d="M7.84 1.804A1 1 0 018.82 1h2.36a1 1 0 01.98.804l.331 1.652a6.993 6.993 0 011.929 1.115l1.598-.54a1 1 0 011.186.447l1.18 2.044a1 1 0 01-.205 1.251l-1.267 1.113a7.047 7.047 0 010 2.228l1.267 1.113a1 1 0 01.206 1.25l-1.18 2.045a1 1 0 01-1.187.447l-1.598-.54a6.993 6.993 0 01-1.929 1.115l-.33 1.652a1 1 0 01-.98.804H8.82a1 1 0 01-.98-.804l-.331-1.652a6.993 6.993 0 01-1.929-1.115l-1.598.54a1 1 0 01-1.186-.447l-1.18-2.044a1 1 0 01.205-1.251l1.267-1.114a7.05 7.05 0 010-2.227L1.821 7.773a1 1 0 01-.206-1.25l1.18-2.045a1 1 0 011.187-.447l1.598.54A6.993 6.993 0 017.51 3.456l.33-1.652zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
|
53
|
+
</svg>
|
|
54
|
+
),
|
|
55
|
+
},
|
|
56
|
+
general: {
|
|
57
|
+
label: 'General',
|
|
58
|
+
color: 'bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400',
|
|
59
|
+
icon: (
|
|
60
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-5 w-5">
|
|
61
|
+
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clipRule="evenodd" />
|
|
62
|
+
</svg>
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default async function FAQPage() {
|
|
68
|
+
const payload = await getPayload({ config: configPromise })
|
|
69
|
+
|
|
70
|
+
const articles = await payload.find({
|
|
71
|
+
collection: 'knowledge-base',
|
|
72
|
+
where: { published: { equals: true } },
|
|
73
|
+
sort: 'sortOrder',
|
|
74
|
+
limit: 200,
|
|
75
|
+
depth: 0,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Group by category
|
|
79
|
+
const grouped: Record<string, typeof articles.docs> = {}
|
|
80
|
+
for (const article of articles.docs) {
|
|
81
|
+
const cat = (article.category as string) || 'general'
|
|
82
|
+
if (!grouped[cat]) grouped[cat] = []
|
|
83
|
+
grouped[cat]!.push(article)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const totalArticles = articles.docs.length
|
|
87
|
+
const totalCategories = Object.keys(grouped).length
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="mx-auto max-w-4xl px-4 pb-12">
|
|
91
|
+
{/* Back navigation */}
|
|
92
|
+
<Link
|
|
93
|
+
href="/support/dashboard"
|
|
94
|
+
className="group mb-8 inline-flex items-center gap-2 text-sm font-medium text-slate-500 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
|
95
|
+
>
|
|
96
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4 transition-transform group-hover:-translate-x-0.5">
|
|
97
|
+
<path fillRule="evenodd" d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" clipRule="evenodd" />
|
|
98
|
+
</svg>
|
|
99
|
+
Retour aux tickets
|
|
100
|
+
</Link>
|
|
101
|
+
|
|
102
|
+
{/* Hero header */}
|
|
103
|
+
<div className="mb-10 text-center">
|
|
104
|
+
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-blue-50 dark:bg-blue-900/20">
|
|
105
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-7 w-7 text-blue-600 dark:text-blue-400">
|
|
106
|
+
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm11.378-3.917c-.89-.777-2.366-.777-3.255 0a.75.75 0 01-.988-1.129c1.454-1.272 3.776-1.272 5.23 0 1.513 1.324 1.513 3.518 0 4.842a3.75 3.75 0 01-.837.552c-.676.328-1.028.774-1.028 1.152v.75a.75.75 0 01-1.5 0v-.75c0-1.279 1.06-2.107 1.875-2.502.182-.088.351-.199.503-.331.83-.727.83-1.857 0-2.584zM12 18a.75.75 0 100-1.5.75.75 0 000 1.5z" clipRule="evenodd" />
|
|
107
|
+
</svg>
|
|
108
|
+
</div>
|
|
109
|
+
<h1 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
|
|
110
|
+
Base de connaissances
|
|
111
|
+
</h1>
|
|
112
|
+
<p className="mx-auto mt-3 max-w-lg text-base text-slate-500 dark:text-slate-400">
|
|
113
|
+
Trouvez des reponses a vos questions parmi nos {totalArticles} articles
|
|
114
|
+
{totalCategories > 1 && ` dans ${totalCategories} categories`}.
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Search */}
|
|
119
|
+
<FAQSearch articles={articles.docs.map((a) => ({
|
|
120
|
+
id: String(a.id),
|
|
121
|
+
title: a.title,
|
|
122
|
+
category: a.category as string,
|
|
123
|
+
slug: a.slug,
|
|
124
|
+
}))} />
|
|
125
|
+
|
|
126
|
+
{/* Categories */}
|
|
127
|
+
<div className="space-y-10">
|
|
128
|
+
{Object.entries(grouped).map(([cat, items]) => {
|
|
129
|
+
const config = categoryConfig[cat] || {
|
|
130
|
+
label: cat,
|
|
131
|
+
color: 'bg-slate-100 dark:bg-slate-700/50 text-slate-600 dark:text-slate-400',
|
|
132
|
+
icon: (
|
|
133
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-5 w-5">
|
|
134
|
+
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clipRule="evenodd" />
|
|
135
|
+
</svg>
|
|
136
|
+
),
|
|
137
|
+
}
|
|
138
|
+
return (
|
|
139
|
+
<section key={cat}>
|
|
140
|
+
{/* Category header */}
|
|
141
|
+
<div className="mb-4 flex items-center gap-3">
|
|
142
|
+
<div className={`flex h-9 w-9 items-center justify-center rounded-xl ${config.color}`}>
|
|
143
|
+
{config.icon}
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<h2 className="text-lg font-semibold text-slate-900 dark:text-white">{config.label}</h2>
|
|
147
|
+
<p className="text-xs text-slate-400 dark:text-slate-500">{items.length} article{items.length > 1 ? 's' : ''}</p>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Articles */}
|
|
152
|
+
<div className="space-y-2">
|
|
153
|
+
{items.map((article) => (
|
|
154
|
+
<details
|
|
155
|
+
key={article.id}
|
|
156
|
+
className="group rounded-xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800/50 shadow-sm transition-all duration-200 hover:border-slate-300 dark:hover:border-slate-600 open:shadow-md"
|
|
157
|
+
>
|
|
158
|
+
<summary className="flex cursor-pointer items-center justify-between gap-4 px-5 py-4 text-sm font-semibold text-slate-900 dark:text-white list-none [&::-webkit-details-marker]:hidden">
|
|
159
|
+
<span className="leading-relaxed">{article.title}</span>
|
|
160
|
+
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg bg-slate-100 dark:bg-slate-700 transition-colors group-open:bg-blue-50 dark:group-open:bg-blue-900/30">
|
|
161
|
+
<svg
|
|
162
|
+
className="h-4 w-4 text-slate-400 transition-transform duration-200 group-open:rotate-180 group-open:text-blue-600 dark:group-open:text-blue-400"
|
|
163
|
+
fill="none"
|
|
164
|
+
viewBox="0 0 24 24"
|
|
165
|
+
stroke="currentColor"
|
|
166
|
+
strokeWidth={2}
|
|
167
|
+
>
|
|
168
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
169
|
+
</svg>
|
|
170
|
+
</div>
|
|
171
|
+
</summary>
|
|
172
|
+
<div className="border-t border-slate-100 dark:border-slate-700/50 px-5 py-4">
|
|
173
|
+
<div className="prose prose-sm prose-slate dark:prose-invert max-w-none prose-p:text-slate-600 dark:prose-p:text-slate-300 prose-p:leading-relaxed prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline">
|
|
174
|
+
{article.body && <RichText data={article.body} />}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</details>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
</section>
|
|
181
|
+
)
|
|
182
|
+
})}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Empty state */}
|
|
186
|
+
{articles.docs.length === 0 && (
|
|
187
|
+
<div className="flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-slate-200 dark:border-slate-700 py-16 px-6">
|
|
188
|
+
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-slate-100 dark:bg-slate-700">
|
|
189
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-6 w-6 text-slate-400">
|
|
190
|
+
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clipRule="evenodd" />
|
|
191
|
+
</svg>
|
|
192
|
+
</div>
|
|
193
|
+
<p className="text-base font-semibold text-slate-900 dark:text-white">Aucun article pour le moment</p>
|
|
194
|
+
<p className="mt-1 text-sm text-slate-400 dark:text-slate-500">Les articles FAQ seront bientot disponibles.</p>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { headers as getHeaders } from 'next/headers'
|
|
3
|
+
import { redirect } from 'next/navigation'
|
|
4
|
+
import { getPayload } from 'payload'
|
|
5
|
+
import configPromise from '@payload-config'
|
|
6
|
+
import { SupportHeader } from './SupportHeader'
|
|
7
|
+
import { ChatWidget } from './ChatWidget'
|
|
8
|
+
import { ChatbotWidget } from './ChatbotWidget'
|
|
9
|
+
|
|
10
|
+
export const dynamic = 'force-dynamic'
|
|
11
|
+
|
|
12
|
+
export type SupportUser = {
|
|
13
|
+
id: number
|
|
14
|
+
email: string
|
|
15
|
+
company: string
|
|
16
|
+
firstName: string
|
|
17
|
+
lastName: string
|
|
18
|
+
collection: 'support-clients'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isSupportUser(user: unknown): user is SupportUser {
|
|
22
|
+
return typeof user === 'object' && user !== null && 'company' in user
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getSupportUser(): Promise<SupportUser | null> {
|
|
26
|
+
try {
|
|
27
|
+
const payload = await getPayload({ config: configPromise })
|
|
28
|
+
const headers = await getHeaders()
|
|
29
|
+
|
|
30
|
+
// Use Payload's auth to verify the JWT from cookies
|
|
31
|
+
const { user } = await payload.auth({ headers })
|
|
32
|
+
|
|
33
|
+
if (user && isSupportUser(user)) {
|
|
34
|
+
return user
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('[support-layout] Failed to get user:', error)
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default async function SupportAuthLayout({ children }: { children: React.ReactNode }) {
|
|
45
|
+
const user = await getSupportUser()
|
|
46
|
+
|
|
47
|
+
if (!user) {
|
|
48
|
+
redirect('/support/login')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
<SupportHeader user={user} />
|
|
54
|
+
<main className="mx-auto w-full max-w-screen-2xl px-6 py-8 sm:px-10 lg:px-16 dark:text-white">
|
|
55
|
+
{children}
|
|
56
|
+
</main>
|
|
57
|
+
<ChatbotWidget />
|
|
58
|
+
<ChatWidget />
|
|
59
|
+
</>
|
|
60
|
+
)
|
|
61
|
+
}
|