@consilioweb/payload-support 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +525 -0
  3. package/dist/client.cjs +7 -0
  4. package/dist/client.d.cts +3 -0
  5. package/dist/client.d.ts +3 -0
  6. package/dist/client.js +5 -0
  7. package/dist/index.cjs +7766 -0
  8. package/dist/index.d.cts +384 -0
  9. package/dist/index.d.ts +384 -0
  10. package/dist/index.js +7730 -0
  11. package/dist/views.d.cts +30 -0
  12. package/dist/views.d.ts +30 -0
  13. package/package.json +131 -0
  14. package/src/client.ts +1 -0
  15. package/src/collections/AuthLogs.ts +65 -0
  16. package/src/collections/CannedResponses.ts +69 -0
  17. package/src/collections/ChatMessages.ts +98 -0
  18. package/src/collections/EmailLogs.ts +94 -0
  19. package/src/collections/KnowledgeBase.ts +99 -0
  20. package/src/collections/Macros.ts +98 -0
  21. package/src/collections/PendingEmails.ts +122 -0
  22. package/src/collections/SatisfactionSurveys.ts +98 -0
  23. package/src/collections/SlaPolicies.ts +123 -0
  24. package/src/collections/SupportClients.ts +210 -0
  25. package/src/collections/TicketActivityLog.ts +81 -0
  26. package/src/collections/TicketMessages.ts +364 -0
  27. package/src/collections/TicketStatuses.ts +108 -0
  28. package/src/collections/Tickets.ts +704 -0
  29. package/src/collections/TimeEntries.ts +105 -0
  30. package/src/collections/WebhookEndpoints.ts +96 -0
  31. package/src/collections/index.ts +16 -0
  32. package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
  33. package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
  34. package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
  35. package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
  36. package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
  37. package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
  38. package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
  39. package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
  40. package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
  41. package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
  42. package/src/components/TicketConversation/config.ts +82 -0
  43. package/src/components/TicketConversation/constants.ts +74 -0
  44. package/src/components/TicketConversation/context.ts +63 -0
  45. package/src/components/TicketConversation/hooks/useAI.ts +180 -0
  46. package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
  47. package/src/components/TicketConversation/hooks/useReply.ts +190 -0
  48. package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
  49. package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
  50. package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
  51. package/src/components/TicketConversation/index.tsx +1110 -0
  52. package/src/components/TicketConversation/locales/en.json +878 -0
  53. package/src/components/TicketConversation/locales/fr.json +878 -0
  54. package/src/components/TicketConversation/types.ts +54 -0
  55. package/src/components/TicketConversation/utils.ts +25 -0
  56. package/src/endpoints/admin-chat-stream.ts +238 -0
  57. package/src/endpoints/admin-chat.ts +263 -0
  58. package/src/endpoints/admin-stats.ts +200 -0
  59. package/src/endpoints/ai.ts +199 -0
  60. package/src/endpoints/apply-macro.ts +144 -0
  61. package/src/endpoints/auth-2fa.ts +163 -0
  62. package/src/endpoints/auto-close.ts +175 -0
  63. package/src/endpoints/billing.ts +167 -0
  64. package/src/endpoints/bulk-action.ts +103 -0
  65. package/src/endpoints/chat-stream.ts +127 -0
  66. package/src/endpoints/chat.ts +188 -0
  67. package/src/endpoints/chatbot.ts +113 -0
  68. package/src/endpoints/delete-account.ts +129 -0
  69. package/src/endpoints/email-stats.ts +109 -0
  70. package/src/endpoints/export-csv.ts +84 -0
  71. package/src/endpoints/export-data.ts +104 -0
  72. package/src/endpoints/import-conversation.ts +307 -0
  73. package/src/endpoints/index.ts +154 -0
  74. package/src/endpoints/login.ts +92 -0
  75. package/src/endpoints/merge-clients.ts +132 -0
  76. package/src/endpoints/merge-tickets.ts +137 -0
  77. package/src/endpoints/oauth-google.ts +179 -0
  78. package/src/endpoints/pending-emails-process.ts +224 -0
  79. package/src/endpoints/presence.ts +104 -0
  80. package/src/endpoints/process-scheduled.ts +144 -0
  81. package/src/endpoints/purge-logs.ts +58 -0
  82. package/src/endpoints/resend-notification.ts +99 -0
  83. package/src/endpoints/round-robin-config.ts +92 -0
  84. package/src/endpoints/satisfaction.ts +93 -0
  85. package/src/endpoints/search.ts +106 -0
  86. package/src/endpoints/seed-kb.ts +153 -0
  87. package/src/endpoints/settings.ts +144 -0
  88. package/src/endpoints/signature.ts +93 -0
  89. package/src/endpoints/sla-check.ts +124 -0
  90. package/src/endpoints/split-ticket.ts +131 -0
  91. package/src/endpoints/statuses.ts +45 -0
  92. package/src/endpoints/track-open.ts +154 -0
  93. package/src/endpoints/typing.ts +101 -0
  94. package/src/endpoints/user-prefs.ts +125 -0
  95. package/src/hooks/checkSLA.ts +414 -0
  96. package/src/hooks/ticketStatusEmail.ts +182 -0
  97. package/src/index.ts +51 -0
  98. package/src/plugin.ts +157 -0
  99. package/src/portal/LiveChat.tsx +1353 -0
  100. package/src/portal/auth/ChatWidget.tsx +350 -0
  101. package/src/portal/auth/ChatbotWidget.tsx +285 -0
  102. package/src/portal/auth/SupportHeader.tsx +409 -0
  103. package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
  104. package/src/portal/auth/dashboard/page.tsx +84 -0
  105. package/src/portal/auth/faq/FAQSearch.tsx +117 -0
  106. package/src/portal/auth/faq/page.tsx +199 -0
  107. package/src/portal/auth/layout.tsx +61 -0
  108. package/src/portal/auth/profile/page.tsx +705 -0
  109. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
  110. package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
  111. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
  112. package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
  113. package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
  114. package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
  115. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
  116. package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
  117. package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
  118. package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
  119. package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
  120. package/src/portal/auth/tickets/detail/page.tsx +738 -0
  121. package/src/portal/auth/tickets/new/page.tsx +515 -0
  122. package/src/portal/forgot-password/page.tsx +114 -0
  123. package/src/portal/layout.tsx +26 -0
  124. package/src/portal/locales/en.json +374 -0
  125. package/src/portal/locales/fr.json +374 -0
  126. package/src/portal/login/page.tsx +351 -0
  127. package/src/portal/page.tsx +162 -0
  128. package/src/portal/register/page.tsx +281 -0
  129. package/src/portal/reset-password/page.tsx +152 -0
  130. package/src/styles/BillingView.module.scss +311 -0
  131. package/src/styles/ChatView.module.scss +438 -0
  132. package/src/styles/CommandPalette.module.scss +160 -0
  133. package/src/styles/CrmView.module.scss +554 -0
  134. package/src/styles/EmailTracking.module.scss +238 -0
  135. package/src/styles/ImportConversation.module.scss +267 -0
  136. package/src/styles/Layout.module.scss +55 -0
  137. package/src/styles/Logs.module.scss +164 -0
  138. package/src/styles/NewTicket.module.scss +143 -0
  139. package/src/styles/PendingEmails.module.scss +629 -0
  140. package/src/styles/SupportDashboard.module.scss +649 -0
  141. package/src/styles/TicketDetail.module.scss +1043 -0
  142. package/src/styles/TicketInbox.module.scss +296 -0
  143. package/src/styles/TicketingSettings.module.scss +358 -0
  144. package/src/styles/TimeDashboard.module.scss +287 -0
  145. package/src/styles/_tokens.scss +78 -0
  146. package/src/styles/theme.css +633 -0
  147. package/src/types.ts +255 -0
  148. package/src/utils/adminNotification.ts +38 -0
  149. package/src/utils/auth.ts +46 -0
  150. package/src/utils/emailTemplate.ts +343 -0
  151. package/src/utils/fireWebhooks.ts +84 -0
  152. package/src/utils/index.ts +22 -0
  153. package/src/utils/rateLimiter.ts +52 -0
  154. package/src/utils/readSettings.ts +67 -0
  155. package/src/utils/slugs.ts +54 -0
  156. package/src/utils/webhookDispatcher.ts +120 -0
  157. package/src/views/BillingView/client.tsx +137 -0
  158. package/src/views/BillingView/index.tsx +33 -0
  159. package/src/views/ChatView/client.tsx +294 -0
  160. package/src/views/ChatView/index.tsx +33 -0
  161. package/src/views/CrmView/client.tsx +206 -0
  162. package/src/views/CrmView/index.tsx +33 -0
  163. package/src/views/EmailTrackingView/client.tsx +124 -0
  164. package/src/views/EmailTrackingView/index.tsx +33 -0
  165. package/src/views/ImportConversationView/client.tsx +133 -0
  166. package/src/views/ImportConversationView/index.tsx +33 -0
  167. package/src/views/LogsView/client.tsx +151 -0
  168. package/src/views/LogsView/index.tsx +30 -0
  169. package/src/views/NewTicketView/client.tsx +227 -0
  170. package/src/views/NewTicketView/index.tsx +30 -0
  171. package/src/views/PendingEmailsView/client.tsx +177 -0
  172. package/src/views/PendingEmailsView/index.tsx +33 -0
  173. package/src/views/SupportDashboardView/client.tsx +424 -0
  174. package/src/views/SupportDashboardView/index.tsx +33 -0
  175. package/src/views/TicketDetailView/client.tsx +775 -0
  176. package/src/views/TicketDetailView/index.tsx +33 -0
  177. package/src/views/TicketInboxView/client.tsx +313 -0
  178. package/src/views/TicketInboxView/index.tsx +30 -0
  179. package/src/views/TicketingSettingsView/client.tsx +866 -0
  180. package/src/views/TicketingSettingsView/index.tsx +33 -0
  181. package/src/views/TimeDashboardView/client.tsx +144 -0
  182. package/src/views/TimeDashboardView/index.tsx +33 -0
  183. package/src/views/shared/AdminViewHeader.tsx +69 -0
  184. package/src/views/shared/ErrorBoundary.tsx +68 -0
  185. package/src/views/shared/Skeleton.tsx +125 -0
  186. package/src/views/shared/adminTokens.ts +37 -0
  187. package/src/views/shared/config.ts +82 -0
  188. package/src/views/shared/index.ts +6 -0
  189. package/src/views.ts +16 -0
@@ -0,0 +1,84 @@
1
+ import 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 &laquo;&nbsp;{query}&nbsp;&raquo;
69
+ </p>
70
+ <p className="mt-1 text-xs text-slate-400 dark:text-slate-500">Essayez avec d&apos;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
+ }