@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,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
|
+
}
|