@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,409 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useCallback, useRef } from 'react'
4
+ import Link from 'next/link'
5
+ import { useRouter, usePathname } from 'next/navigation'
6
+ import type { SupportUser } from './layout'
7
+
8
+ const DARK_MODE_KEY = 'support-dark-mode'
9
+ const _NOTIFICATION_KEY = 'support-notifications'
10
+
11
+ export function SupportHeader({ user }: { user: SupportUser }) {
12
+ const router = useRouter()
13
+ const pathname = usePathname()
14
+ const [darkMode, setDarkMode] = useState(false)
15
+ const [impersonation, setImpersonation] = useState<{ adminEmail: string; clientCompany: string } | null>(null)
16
+ const [unreadCount, setUnreadCount] = useState(0)
17
+ const [notificationPermission, setNotificationPermission] = useState<NotificationPermission | 'unsupported'>('default')
18
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
19
+ const [userMenuOpen, setUserMenuOpen] = useState(false)
20
+ const prevUnreadCountRef = useRef(0)
21
+ const userMenuRef = useRef<HTMLDivElement>(null)
22
+
23
+ // Fetch unread ticket count
24
+ const fetchUnread = useCallback(async () => {
25
+ try {
26
+ const res = await fetch('/api/tickets?where[status][not_equals]=resolved&limit=100&depth=0', { credentials: 'include' })
27
+ if (!res.ok) return
28
+ const data = await res.json()
29
+ const docs = data.docs || []
30
+ let count = 0
31
+ for (const t of docs) {
32
+ if (t.updatedAt && (!t.lastClientReadAt || new Date(t.lastClientReadAt) < new Date(t.updatedAt))) {
33
+ count++
34
+ }
35
+ }
36
+
37
+ // Show browser notification when unread count increases
38
+ if (count > prevUnreadCountRef.current && prevUnreadCountRef.current >= 0) {
39
+ const diff = count - prevUnreadCountRef.current
40
+ if (diff > 0 && Notification.permission === 'granted' && typeof document !== 'undefined' && document.hidden) {
41
+ const latestTicket = docs
42
+ .filter((t: { updatedAt?: string; lastClientReadAt?: string }) =>
43
+ t.updatedAt && (!t.lastClientReadAt || new Date(t.lastClientReadAt) < new Date(t.updatedAt)),
44
+ )
45
+ .sort((a: { updatedAt: string }, b: { updatedAt: string }) =>
46
+ new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
47
+ )[0]
48
+
49
+ if (latestTicket) {
50
+ showNewMessageNotification(
51
+ latestTicket.ticketNumber || `#${latestTicket.id}`,
52
+ latestTicket.subject || 'Nouveau message',
53
+ )
54
+ }
55
+ }
56
+ }
57
+
58
+ prevUnreadCountRef.current = count
59
+ setUnreadCount(count)
60
+ } catch { /* ignore */ }
61
+ }, [])
62
+
63
+ useEffect(() => {
64
+ const stored = localStorage.getItem(DARK_MODE_KEY)
65
+ if (stored === 'true') {
66
+ setDarkMode(true)
67
+ document.documentElement.setAttribute('data-theme', 'dark')
68
+ }
69
+
70
+ // Check notification permission
71
+ if ('Notification' in window) {
72
+ setNotificationPermission(Notification.permission)
73
+ } else {
74
+ setNotificationPermission('unsupported')
75
+ }
76
+
77
+ // Check impersonation cookie
78
+ try {
79
+ const match = document.cookie.split('; ').find((c) => c.startsWith('impersonating='))
80
+ if (match) {
81
+ const value = decodeURIComponent(match.split('=').slice(1).join('='))
82
+ setImpersonation(JSON.parse(value))
83
+ }
84
+ } catch { /* ignore */ }
85
+
86
+ // Initial fetch + polling for unread count
87
+ fetchUnread()
88
+ const interval = setInterval(fetchUnread, 30000)
89
+ return () => clearInterval(interval)
90
+ }, [fetchUnread])
91
+
92
+ // Close user menu on outside click
93
+ useEffect(() => {
94
+ function handleClickOutside(e: MouseEvent) {
95
+ if (userMenuRef.current && !userMenuRef.current.contains(e.target as Node)) {
96
+ setUserMenuOpen(false)
97
+ }
98
+ }
99
+ document.addEventListener('mousedown', handleClickOutside)
100
+ return () => document.removeEventListener('mousedown', handleClickOutside)
101
+ }, [])
102
+
103
+ const toggleDarkMode = useCallback(() => {
104
+ const next = !darkMode
105
+ setDarkMode(next)
106
+ localStorage.setItem(DARK_MODE_KEY, String(next))
107
+ document.documentElement.setAttribute('data-theme', next ? 'dark' : 'light')
108
+ }, [darkMode])
109
+
110
+ const requestNotificationPermission = useCallback(async () => {
111
+ if (!('Notification' in window)) return
112
+
113
+ const permission = await Notification.requestPermission()
114
+ setNotificationPermission(permission)
115
+
116
+ if (permission === 'granted') {
117
+ new Notification('Notifications activees', {
118
+ body: 'Vous recevrez une notification quand le support repond.',
119
+ icon: '/favicon.ico',
120
+ })
121
+ }
122
+ }, [])
123
+
124
+ const handleLogout = async () => {
125
+ await fetch('/api/support-clients/logout', {
126
+ method: 'POST',
127
+ credentials: 'include',
128
+ })
129
+ router.push('/support/login')
130
+ router.refresh()
131
+ }
132
+
133
+ const navLinks = [
134
+ { href: '/support/dashboard', label: 'Mes tickets', icon: InboxIcon },
135
+ { href: '/support/tickets/new', label: 'Nouveau ticket', icon: PlusIcon },
136
+ { href: '/support/faq', label: 'FAQ', icon: HelpIcon },
137
+ ]
138
+
139
+ const isActive = (href: string) => pathname === href || pathname.startsWith(href + '/')
140
+
141
+ return (
142
+ <>
143
+ {impersonation && (
144
+ <div className="sticky top-0 z-50 flex items-center justify-center gap-3 bg-red-600 px-4 py-2 text-sm font-semibold text-white">
145
+ <span>Mode impersonation — {impersonation.adminEmail} → {impersonation.clientCompany}</span>
146
+ {/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
147
+ <a
148
+ href="/api/admin/stop-impersonation"
149
+ className="rounded-md bg-white/20 px-3 py-1 text-xs font-bold text-white backdrop-blur-sm transition-colors hover:bg-white hover:text-red-600"
150
+ >
151
+ Quitter
152
+ </a>
153
+ </div>
154
+ )}
155
+
156
+ <header className="sticky top-0 z-40 border-b border-slate-200 bg-white/80 backdrop-blur-xl dark:border-slate-800 dark:bg-slate-950/80">
157
+ <div className="mx-auto flex h-16 w-full max-w-screen-2xl items-center justify-between px-4 sm:px-6 lg:px-8">
158
+ {/* Logo */}
159
+ <div className="flex items-center gap-8">
160
+ <Link href="/support/dashboard" className="flex items-center gap-2.5">
161
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600 text-white">
162
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
163
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
164
+ </svg>
165
+ </div>
166
+ <div className="flex items-baseline gap-1.5">
167
+ <span className="text-base font-bold text-slate-900 dark:text-white">Support</span>
168
+ </div>
169
+ </Link>
170
+
171
+ {/* Desktop nav */}
172
+ <nav className="hidden items-center gap-1 md:flex">
173
+ {navLinks.map((link) => {
174
+ const active = isActive(link.href)
175
+ return (
176
+ <Link
177
+ key={link.href}
178
+ href={link.href}
179
+ className={`relative flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
180
+ active
181
+ ? 'bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-400'
182
+ : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white'
183
+ }`}
184
+ >
185
+ <link.icon className="h-4 w-4" />
186
+ {link.label}
187
+ {link.href === '/support/dashboard' && unreadCount > 0 && (
188
+ <span className="ml-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-blue-600 px-1.5 text-[11px] font-bold text-white">
189
+ {unreadCount}
190
+ </span>
191
+ )}
192
+ </Link>
193
+ )
194
+ })}
195
+ </nav>
196
+ </div>
197
+
198
+ {/* Right side actions */}
199
+ <div className="flex items-center gap-2">
200
+ {/* Notification bell */}
201
+ {notificationPermission !== 'unsupported' && (
202
+ <button
203
+ onClick={requestNotificationPermission}
204
+ className={`relative rounded-lg p-2 transition-colors ${
205
+ notificationPermission === 'granted'
206
+ ? 'text-blue-600 dark:text-blue-400'
207
+ : 'text-slate-400 hover:bg-slate-100 hover:text-slate-600 dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-300'
208
+ }`}
209
+ title={
210
+ notificationPermission === 'granted'
211
+ ? 'Notifications activees'
212
+ : notificationPermission === 'denied'
213
+ ? 'Notifications bloquees (modifiez dans les parametres du navigateur)'
214
+ : 'Activer les notifications'
215
+ }
216
+ >
217
+ {notificationPermission === 'granted' ? (
218
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-[18px] w-[18px]">
219
+ <path d="M12 2C10.343 2 9 3.343 9 5v.26A5.001 5.001 0 0 0 7 10v4l-2 2v1h14v-1l-2-2v-4a5.001 5.001 0 0 0-2-4.74V5c0-1.657-1.343-3-3-3zM10 20a2 2 0 0 0 4 0h-4z" />
220
+ </svg>
221
+ ) : (
222
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-[18px] w-[18px]">
223
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
224
+ <path d="M13.73 21a2 2 0 0 1-3.46 0" />
225
+ </svg>
226
+ )}
227
+ </button>
228
+ )}
229
+
230
+ {/* Dark mode toggle */}
231
+ <button
232
+ onClick={toggleDarkMode}
233
+ className="rounded-lg p-2 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600 dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-300"
234
+ title={darkMode ? 'Mode clair' : 'Mode sombre'}
235
+ >
236
+ {darkMode ? (
237
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-[18px] w-[18px]">
238
+ <circle cx="12" cy="12" r="5" />
239
+ <line x1="12" y1="1" x2="12" y2="3" />
240
+ <line x1="12" y1="21" x2="12" y2="23" />
241
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
242
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
243
+ <line x1="1" y1="12" x2="3" y2="12" />
244
+ <line x1="21" y1="12" x2="23" y2="12" />
245
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
246
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
247
+ </svg>
248
+ ) : (
249
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-[18px] w-[18px]">
250
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
251
+ </svg>
252
+ )}
253
+ </button>
254
+
255
+ {/* Separator */}
256
+ <div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
257
+
258
+ {/* User menu dropdown */}
259
+ <div className="relative" ref={userMenuRef}>
260
+ <button
261
+ onClick={() => setUserMenuOpen(!userMenuOpen)}
262
+ className="flex items-center gap-2.5 rounded-lg px-2 py-1.5 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800"
263
+ >
264
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-blue-500 to-blue-600 text-xs font-bold text-white">
265
+ {(user.firstName?.[0] || '').toUpperCase()}{(user.lastName?.[0] || '').toUpperCase()}
266
+ </div>
267
+ <div className="hidden text-left sm:block">
268
+ <p className="text-sm font-medium text-slate-900 dark:text-white">{user.company}</p>
269
+ <p className="text-xs text-slate-500 dark:text-slate-400">{user.firstName} {user.lastName}</p>
270
+ </div>
271
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="hidden h-4 w-4 text-slate-400 sm:block">
272
+ <polyline points="6 9 12 15 18 9" />
273
+ </svg>
274
+ </button>
275
+
276
+ {userMenuOpen && (
277
+ <div className="absolute right-0 top-full z-50 mt-1.5 w-56 overflow-hidden rounded-xl border border-slate-200 bg-white shadow-lg shadow-slate-200/50 dark:border-slate-700 dark:bg-slate-900 dark:shadow-slate-950/50">
278
+ <div className="border-b border-slate-100 px-4 py-3 dark:border-slate-800">
279
+ <p className="text-sm font-medium text-slate-900 dark:text-white">{user.company}</p>
280
+ <p className="text-xs text-slate-500 dark:text-slate-400">{user.email}</p>
281
+ </div>
282
+ <div className="py-1.5">
283
+ <Link
284
+ href="/support/profile"
285
+ onClick={() => setUserMenuOpen(false)}
286
+ className="flex items-center gap-2.5 px-4 py-2 text-sm text-slate-700 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800"
287
+ >
288
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4 text-slate-400">
289
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
290
+ <circle cx="12" cy="7" r="4" />
291
+ </svg>
292
+ Mon profil
293
+ </Link>
294
+ <button
295
+ onClick={() => { setUserMenuOpen(false); handleLogout() }}
296
+ className="flex w-full items-center gap-2.5 px-4 py-2 text-sm text-red-600 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/30"
297
+ >
298
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
299
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
300
+ <polyline points="16 17 21 12 16 7" />
301
+ <line x1="21" y1="12" x2="9" y2="12" />
302
+ </svg>
303
+ Deconnexion
304
+ </button>
305
+ </div>
306
+ </div>
307
+ )}
308
+ </div>
309
+
310
+ {/* Mobile menu toggle */}
311
+ <button
312
+ onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
313
+ className="rounded-lg p-2 text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800 md:hidden"
314
+ >
315
+ {mobileMenuOpen ? (
316
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5">
317
+ <line x1="18" y1="6" x2="6" y2="18" />
318
+ <line x1="6" y1="6" x2="18" y2="18" />
319
+ </svg>
320
+ ) : (
321
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5">
322
+ <line x1="3" y1="12" x2="21" y2="12" />
323
+ <line x1="3" y1="6" x2="21" y2="6" />
324
+ <line x1="3" y1="18" x2="21" y2="18" />
325
+ </svg>
326
+ )}
327
+ </button>
328
+ </div>
329
+ </div>
330
+
331
+ {/* Mobile nav */}
332
+ {mobileMenuOpen && (
333
+ <nav className="border-t border-slate-200 bg-white px-4 py-3 dark:border-slate-800 dark:bg-slate-950 md:hidden">
334
+ <div className="space-y-1">
335
+ {navLinks.map((link) => {
336
+ const active = isActive(link.href)
337
+ return (
338
+ <Link
339
+ key={link.href}
340
+ href={link.href}
341
+ onClick={() => setMobileMenuOpen(false)}
342
+ className={`flex items-center gap-2.5 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
343
+ active
344
+ ? 'bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-400'
345
+ : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800'
346
+ }`}
347
+ >
348
+ <link.icon className="h-4 w-4" />
349
+ {link.label}
350
+ {link.href === '/support/dashboard' && unreadCount > 0 && (
351
+ <span className="ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-blue-600 px-1.5 text-[11px] font-bold text-white">
352
+ {unreadCount}
353
+ </span>
354
+ )}
355
+ </Link>
356
+ )
357
+ })}
358
+ </div>
359
+ </nav>
360
+ )}
361
+ </header>
362
+ </>
363
+ )
364
+ }
365
+
366
+ // Icon components
367
+ function InboxIcon({ className }: { className?: string }) {
368
+ return (
369
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
370
+ <polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
371
+ <path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
372
+ </svg>
373
+ )
374
+ }
375
+
376
+ function PlusIcon({ className }: { className?: string }) {
377
+ return (
378
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
379
+ <line x1="12" y1="5" x2="12" y2="19" />
380
+ <line x1="5" y1="12" x2="19" y2="12" />
381
+ </svg>
382
+ )
383
+ }
384
+
385
+ function HelpIcon({ className }: { className?: string }) {
386
+ return (
387
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
388
+ <circle cx="12" cy="12" r="10" />
389
+ <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
390
+ <line x1="12" y1="17" x2="12.01" y2="17" />
391
+ </svg>
392
+ )
393
+ }
394
+
395
+ // Show a browser notification for a new message
396
+ function showNewMessageNotification(ticketNumber: string, preview: string) {
397
+ if (typeof window === 'undefined' || !('Notification' in window)) return
398
+ if (Notification.permission !== 'granted') return
399
+
400
+ try {
401
+ new Notification(`Nouveau message - ${ticketNumber}`, {
402
+ body: preview.slice(0, 100),
403
+ icon: '/favicon.ico',
404
+ tag: `ticket-${ticketNumber}`,
405
+ })
406
+ } catch (err) {
407
+ console.warn('[SupportHeader] Notification failed:', err)
408
+ }
409
+ }