@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,650 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState, useMemo, useEffect } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
|
|
6
|
+
interface TicketData {
|
|
7
|
+
id: number | string
|
|
8
|
+
ticketNumber: string | null | undefined
|
|
9
|
+
subject: string
|
|
10
|
+
status: string | null | undefined
|
|
11
|
+
priority: string | null | undefined
|
|
12
|
+
category: string | null | undefined
|
|
13
|
+
projectName: string | null
|
|
14
|
+
updatedAt: string | null | undefined
|
|
15
|
+
createdAt: string | null | undefined
|
|
16
|
+
hasNewMessage: boolean
|
|
17
|
+
messageCount: number
|
|
18
|
+
totalTimeMinutes: number | null | undefined
|
|
19
|
+
lastMessagePreview: string | null | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const statusConfig: Record<string, { label: string; dot: string; bg: string }> = {
|
|
23
|
+
open: { label: 'Ouvert', dot: 'bg-emerald-500', bg: 'bg-emerald-50 text-emerald-700 ring-emerald-600/20 dark:bg-emerald-950/40 dark:text-emerald-400 dark:ring-emerald-400/20' },
|
|
24
|
+
waiting_client: { label: 'En attente', dot: 'bg-amber-500', bg: 'bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-950/40 dark:text-amber-400 dark:ring-amber-400/20' },
|
|
25
|
+
resolved: { label: 'Resolu', dot: 'bg-slate-400', bg: 'bg-slate-100 text-slate-600 ring-slate-500/20 dark:bg-slate-800 dark:text-slate-400 dark:ring-slate-400/20' },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const priorityConfig: Record<string, { label: string; color: string }> = {
|
|
29
|
+
low: { label: 'Basse', color: 'text-slate-400 dark:text-slate-500' },
|
|
30
|
+
normal: { label: 'Normale', color: 'text-blue-600 dark:text-blue-400' },
|
|
31
|
+
high: { label: 'Haute', color: 'text-orange-600 dark:text-orange-400' },
|
|
32
|
+
urgent: { label: 'Urgente', color: 'text-red-600 dark:text-red-400' },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const categoryLabels: Record<string, string> = {
|
|
36
|
+
bug: 'Bug',
|
|
37
|
+
content: 'Contenu',
|
|
38
|
+
feature: 'Fonctionnalité',
|
|
39
|
+
question: 'Question',
|
|
40
|
+
hosting: 'Hébergement',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Tab = 'active' | 'archived'
|
|
44
|
+
type SortBy = 'updatedAt' | 'createdAt' | 'priority'
|
|
45
|
+
|
|
46
|
+
const PAGE_SIZE = 20
|
|
47
|
+
|
|
48
|
+
function timeAgo(dateStr: string): string {
|
|
49
|
+
const now = new Date()
|
|
50
|
+
const date = new Date(dateStr)
|
|
51
|
+
const diffMs = now.getTime() - date.getTime()
|
|
52
|
+
const diffMin = Math.floor(diffMs / 60000)
|
|
53
|
+
const diffH = Math.floor(diffMin / 60)
|
|
54
|
+
const diffD = Math.floor(diffH / 24)
|
|
55
|
+
|
|
56
|
+
if (diffMin < 1) return "A l'instant"
|
|
57
|
+
if (diffMin < 60) return `il y a ${diffMin}min`
|
|
58
|
+
if (diffH < 24) return `il y a ${diffH}h`
|
|
59
|
+
if (diffD < 7) return `il y a ${diffD}j`
|
|
60
|
+
return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function DashboardClient({ tickets }: { tickets: TicketData[] }) {
|
|
64
|
+
const [tab, setTab] = useState<Tab>('active')
|
|
65
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
66
|
+
const [filterStatus, setFilterStatus] = useState('')
|
|
67
|
+
const [filterCategory, setFilterCategory] = useState('')
|
|
68
|
+
const [filterProject, setFilterProject] = useState('')
|
|
69
|
+
const [sortBy, setSortBy] = useState<SortBy>('updatedAt')
|
|
70
|
+
const [showFilters, setShowFilters] = useState(false)
|
|
71
|
+
const [currentPage, setCurrentPage] = useState(1)
|
|
72
|
+
|
|
73
|
+
// Global search across messages
|
|
74
|
+
const [searchMatchedIds, setSearchMatchedIds] = useState<Set<number | string>>(new Set())
|
|
75
|
+
const [messageSearchLoading, setMessageSearchLoading] = useState(false)
|
|
76
|
+
const [messageSearchCount, setMessageSearchCount] = useState(0)
|
|
77
|
+
|
|
78
|
+
// Extract unique projects
|
|
79
|
+
const projects = useMemo(() => {
|
|
80
|
+
const names = new Set<string>()
|
|
81
|
+
tickets.forEach((t) => { if (t.projectName) names.add(t.projectName) })
|
|
82
|
+
return Array.from(names).sort()
|
|
83
|
+
}, [tickets])
|
|
84
|
+
|
|
85
|
+
// Search in ticket messages when query >= 3 chars
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (searchQuery.length < 3) {
|
|
88
|
+
setSearchMatchedIds(new Set())
|
|
89
|
+
setMessageSearchCount(0)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setMessageSearchLoading(true)
|
|
94
|
+
const timer = setTimeout(async () => {
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(
|
|
97
|
+
`/api/ticket-messages?where[body][like]=${encodeURIComponent(searchQuery)}&limit=50&depth=1`,
|
|
98
|
+
{ credentials: 'include' },
|
|
99
|
+
)
|
|
100
|
+
if (res.ok) {
|
|
101
|
+
const data = await res.json()
|
|
102
|
+
const docs = data.docs || []
|
|
103
|
+
const matchedIds = new Set<number | string>(
|
|
104
|
+
docs.map((m: { ticket: { id: number | string } | number | string }) =>
|
|
105
|
+
typeof m.ticket === 'object' ? m.ticket.id : m.ticket,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
setSearchMatchedIds(matchedIds)
|
|
109
|
+
setMessageSearchCount(matchedIds.size)
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.warn('[DashboardClient] Message search failed:', err)
|
|
113
|
+
} finally {
|
|
114
|
+
setMessageSearchLoading(false)
|
|
115
|
+
}
|
|
116
|
+
}, 500)
|
|
117
|
+
|
|
118
|
+
return () => {
|
|
119
|
+
clearTimeout(timer)
|
|
120
|
+
setMessageSearchLoading(false)
|
|
121
|
+
}
|
|
122
|
+
}, [searchQuery])
|
|
123
|
+
|
|
124
|
+
// Split tickets by tab
|
|
125
|
+
const archivedStatuses = ['resolved']
|
|
126
|
+
const activeTickets = tickets.filter((t) => !archivedStatuses.includes(t.status || ''))
|
|
127
|
+
const archivedTickets = tickets.filter((t) => archivedStatuses.includes(t.status || ''))
|
|
128
|
+
|
|
129
|
+
const baseTickets = tab === 'active' ? activeTickets : archivedTickets
|
|
130
|
+
|
|
131
|
+
// Apply search + filters
|
|
132
|
+
const filtered = useMemo(() => {
|
|
133
|
+
const query = searchQuery.toLowerCase().trim()
|
|
134
|
+
return baseTickets.filter((t) => {
|
|
135
|
+
if (query) {
|
|
136
|
+
const matchSubject = t.subject.toLowerCase().includes(query)
|
|
137
|
+
const matchNumber = (t.ticketNumber || '').toLowerCase().includes(query)
|
|
138
|
+
const matchMessages = searchMatchedIds.has(t.id)
|
|
139
|
+
if (!matchSubject && !matchNumber && !matchMessages) return false
|
|
140
|
+
}
|
|
141
|
+
if (filterStatus && t.status !== filterStatus) return false
|
|
142
|
+
if (filterCategory && t.category !== filterCategory) return false
|
|
143
|
+
if (filterProject && t.projectName !== filterProject) return false
|
|
144
|
+
return true
|
|
145
|
+
})
|
|
146
|
+
}, [baseTickets, searchQuery, filterStatus, filterCategory, filterProject, searchMatchedIds])
|
|
147
|
+
|
|
148
|
+
// Apply sort
|
|
149
|
+
const sorted = useMemo(() => {
|
|
150
|
+
const priorityOrder: Record<string, number> = { urgent: 0, high: 1, normal: 2, low: 3 }
|
|
151
|
+
return [...filtered].sort((a, b) => {
|
|
152
|
+
if (sortBy === 'priority') {
|
|
153
|
+
return (priorityOrder[a.priority || 'normal'] || 2) - (priorityOrder[b.priority || 'normal'] || 2)
|
|
154
|
+
}
|
|
155
|
+
const dateA = new Date(a[sortBy] || 0).getTime()
|
|
156
|
+
const dateB = new Date(b[sortBy] || 0).getTime()
|
|
157
|
+
return dateB - dateA
|
|
158
|
+
})
|
|
159
|
+
}, [filtered, sortBy])
|
|
160
|
+
|
|
161
|
+
// Pagination
|
|
162
|
+
const totalPages = Math.max(1, Math.ceil(sorted.length / PAGE_SIZE))
|
|
163
|
+
const safePage = Math.min(currentPage, totalPages)
|
|
164
|
+
const paginatedTickets = sorted.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE)
|
|
165
|
+
|
|
166
|
+
const resetPage = () => setCurrentPage(1)
|
|
167
|
+
|
|
168
|
+
// Stats
|
|
169
|
+
const stats = useMemo(() => {
|
|
170
|
+
return {
|
|
171
|
+
total: tickets.length,
|
|
172
|
+
active: activeTickets.length,
|
|
173
|
+
archived: archivedTickets.length,
|
|
174
|
+
newMessages: tickets.filter((t) => t.hasNewMessage).length,
|
|
175
|
+
}
|
|
176
|
+
}, [tickets, activeTickets, archivedTickets])
|
|
177
|
+
|
|
178
|
+
const activeFilterCount = [filterStatus, filterCategory, filterProject].filter(Boolean).length
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div className="space-y-6">
|
|
182
|
+
{/* Page header */}
|
|
183
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
184
|
+
<div>
|
|
185
|
+
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">Mes tickets</h1>
|
|
186
|
+
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
|
187
|
+
Suivez et gerez vos demandes de support
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
<Link
|
|
191
|
+
href="/support/tickets/new"
|
|
192
|
+
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:bg-blue-700 hover:shadow-md active:scale-[0.98]"
|
|
193
|
+
>
|
|
194
|
+
<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">
|
|
195
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
196
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
197
|
+
</svg>
|
|
198
|
+
Nouveau ticket
|
|
199
|
+
</Link>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Stats row */}
|
|
203
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
204
|
+
<div className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
|
205
|
+
<div className="flex items-center gap-2">
|
|
206
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 dark:bg-blue-950/40">
|
|
207
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-4 w-4 text-blue-600 dark:text-blue-400">
|
|
208
|
+
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
|
209
|
+
<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" />
|
|
210
|
+
</svg>
|
|
211
|
+
</div>
|
|
212
|
+
<div>
|
|
213
|
+
<p className="text-xl font-bold text-slate-900 dark:text-white">{stats.active}</p>
|
|
214
|
+
<p className="text-xs text-slate-500 dark:text-slate-400">Actifs</p>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{stats.newMessages > 0 && (
|
|
220
|
+
<div className="rounded-xl border border-blue-200 bg-blue-50/50 p-4 dark:border-blue-800 dark:bg-blue-950/30">
|
|
221
|
+
<div className="flex items-center gap-2">
|
|
222
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/50">
|
|
223
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-4 w-4 text-blue-600 dark:text-blue-400">
|
|
224
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
225
|
+
</svg>
|
|
226
|
+
</div>
|
|
227
|
+
<div>
|
|
228
|
+
<p className="text-xl font-bold text-blue-700 dark:text-blue-300">{stats.newMessages}</p>
|
|
229
|
+
<p className="text-xs text-blue-600 dark:text-blue-400">Non lus</p>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
<div className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
|
236
|
+
<div className="flex items-center gap-2">
|
|
237
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100 dark:bg-slate-800">
|
|
238
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-4 w-4 text-slate-500 dark:text-slate-400">
|
|
239
|
+
<rect x="3" y="3" width="7" height="7" />
|
|
240
|
+
<rect x="14" y="3" width="7" height="7" />
|
|
241
|
+
<rect x="14" y="14" width="7" height="7" />
|
|
242
|
+
<rect x="3" y="14" width="7" height="7" />
|
|
243
|
+
</svg>
|
|
244
|
+
</div>
|
|
245
|
+
<div>
|
|
246
|
+
<p className="text-xl font-bold text-slate-900 dark:text-white">{stats.total}</p>
|
|
247
|
+
<p className="text-xs text-slate-500 dark:text-slate-400">Total</p>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
|
253
|
+
<div className="flex items-center gap-2">
|
|
254
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100 dark:bg-slate-800">
|
|
255
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-4 w-4 text-slate-400 dark:text-slate-500">
|
|
256
|
+
<polyline points="20 6 9 17 4 12" />
|
|
257
|
+
</svg>
|
|
258
|
+
</div>
|
|
259
|
+
<div>
|
|
260
|
+
<p className="text-xl font-bold text-slate-900 dark:text-white">{stats.archived}</p>
|
|
261
|
+
<p className="text-xs text-slate-500 dark:text-slate-400">Archives</p>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Response time banner */}
|
|
268
|
+
<div className="flex items-center gap-3 rounded-lg border border-blue-100 bg-blue-50/50 px-4 py-3 dark:border-blue-900/50 dark:bg-blue-950/20">
|
|
269
|
+
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/50">
|
|
270
|
+
<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-blue-600 dark:text-blue-400">
|
|
271
|
+
<circle cx="12" cy="12" r="10" />
|
|
272
|
+
<polyline points="12 6 12 12 16 14" />
|
|
273
|
+
</svg>
|
|
274
|
+
</div>
|
|
275
|
+
<p className="text-sm text-blue-700 dark:text-blue-300">
|
|
276
|
+
Notre temps de reponse moyen est de <strong>moins de 2h</strong> en jours ouvres.
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
{/* Main content card */}
|
|
281
|
+
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
|
|
282
|
+
{/* Tabs + Search bar */}
|
|
283
|
+
<div className="border-b border-slate-200 dark:border-slate-800">
|
|
284
|
+
<div className="flex items-center justify-between px-4 sm:px-5">
|
|
285
|
+
<div className="flex gap-0">
|
|
286
|
+
<button
|
|
287
|
+
onClick={() => { setTab('active'); setFilterStatus(''); resetPage() }}
|
|
288
|
+
className={`relative cursor-pointer px-4 py-3.5 text-sm font-medium transition-colors ${
|
|
289
|
+
tab === 'active'
|
|
290
|
+
? 'text-blue-600 dark:text-blue-400'
|
|
291
|
+
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300'
|
|
292
|
+
}`}
|
|
293
|
+
>
|
|
294
|
+
Actifs
|
|
295
|
+
<span className={`ml-1.5 rounded-full px-1.5 py-0.5 text-xs font-semibold ${
|
|
296
|
+
tab === 'active'
|
|
297
|
+
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
|
|
298
|
+
: 'bg-slate-100 text-slate-500 dark:bg-slate-800 dark:text-slate-400'
|
|
299
|
+
}`}>
|
|
300
|
+
{activeTickets.length}
|
|
301
|
+
</span>
|
|
302
|
+
{tab === 'active' && (
|
|
303
|
+
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-blue-600 dark:bg-blue-400" />
|
|
304
|
+
)}
|
|
305
|
+
</button>
|
|
306
|
+
<button
|
|
307
|
+
onClick={() => { setTab('archived'); setFilterStatus(''); resetPage() }}
|
|
308
|
+
className={`relative cursor-pointer px-4 py-3.5 text-sm font-medium transition-colors ${
|
|
309
|
+
tab === 'archived'
|
|
310
|
+
? 'text-blue-600 dark:text-blue-400'
|
|
311
|
+
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300'
|
|
312
|
+
}`}
|
|
313
|
+
>
|
|
314
|
+
Archives
|
|
315
|
+
<span className={`ml-1.5 rounded-full px-1.5 py-0.5 text-xs font-semibold ${
|
|
316
|
+
tab === 'archived'
|
|
317
|
+
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
|
|
318
|
+
: 'bg-slate-100 text-slate-500 dark:bg-slate-800 dark:text-slate-400'
|
|
319
|
+
}`}>
|
|
320
|
+
{archivedTickets.length}
|
|
321
|
+
</span>
|
|
322
|
+
{tab === 'archived' && (
|
|
323
|
+
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-blue-600 dark:bg-blue-400" />
|
|
324
|
+
)}
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
{/* Search + Sort + Filter bar */}
|
|
331
|
+
<div className="flex flex-wrap items-center gap-2 border-b border-slate-100 px-4 py-3 dark:border-slate-800 sm:px-5">
|
|
332
|
+
<div className="relative flex-1 sm:max-w-xs">
|
|
333
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 dark:text-slate-500">
|
|
334
|
+
<circle cx="11" cy="11" r="8" />
|
|
335
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
336
|
+
</svg>
|
|
337
|
+
<input
|
|
338
|
+
type="text"
|
|
339
|
+
value={searchQuery}
|
|
340
|
+
onChange={(e) => { setSearchQuery(e.target.value); resetPage() }}
|
|
341
|
+
placeholder="Rechercher..."
|
|
342
|
+
className="w-full rounded-lg border border-slate-200 bg-slate-50 py-2 pl-9 pr-3 text-sm text-slate-900 placeholder-slate-400 outline-none transition-colors focus:border-blue-300 focus:bg-white focus:ring-2 focus:ring-blue-100 dark:border-slate-700 dark:bg-slate-800 dark:text-white dark:placeholder-slate-500 dark:focus:border-blue-600 dark:focus:ring-blue-900/30"
|
|
343
|
+
/>
|
|
344
|
+
{messageSearchLoading && (
|
|
345
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
346
|
+
<div className="h-4 w-4 animate-spin rounded-full border-2 border-slate-300 border-t-blue-600 dark:border-slate-600 dark:border-t-blue-400" />
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<select
|
|
352
|
+
value={sortBy}
|
|
353
|
+
onChange={(e) => setSortBy(e.target.value as SortBy)}
|
|
354
|
+
className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-600 outline-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300"
|
|
355
|
+
>
|
|
356
|
+
<option value="updatedAt">Derniere activite</option>
|
|
357
|
+
<option value="createdAt">Date de creation</option>
|
|
358
|
+
<option value="priority">Priorite</option>
|
|
359
|
+
</select>
|
|
360
|
+
|
|
361
|
+
<button
|
|
362
|
+
type="button"
|
|
363
|
+
onClick={() => setShowFilters(!showFilters)}
|
|
364
|
+
className={`flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition-colors ${
|
|
365
|
+
showFilters || activeFilterCount > 0
|
|
366
|
+
? 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950/30 dark:text-blue-400'
|
|
367
|
+
: 'border-slate-200 text-slate-600 hover:bg-slate-50 dark:border-slate-700 dark:text-slate-400 dark:hover:bg-slate-800'
|
|
368
|
+
}`}
|
|
369
|
+
>
|
|
370
|
+
<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">
|
|
371
|
+
<line x1="4" y1="21" x2="4" y2="14" />
|
|
372
|
+
<line x1="4" y1="10" x2="4" y2="3" />
|
|
373
|
+
<line x1="12" y1="21" x2="12" y2="12" />
|
|
374
|
+
<line x1="12" y1="8" x2="12" y2="3" />
|
|
375
|
+
<line x1="20" y1="21" x2="20" y2="16" />
|
|
376
|
+
<line x1="20" y1="12" x2="20" y2="3" />
|
|
377
|
+
<line x1="1" y1="14" x2="7" y2="14" />
|
|
378
|
+
<line x1="9" y1="8" x2="15" y2="8" />
|
|
379
|
+
<line x1="17" y1="16" x2="23" y2="16" />
|
|
380
|
+
</svg>
|
|
381
|
+
Filtres
|
|
382
|
+
{activeFilterCount > 0 && (
|
|
383
|
+
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-blue-600 px-1 text-[11px] font-bold text-white">
|
|
384
|
+
{activeFilterCount}
|
|
385
|
+
</span>
|
|
386
|
+
)}
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
{/* Message search indicator */}
|
|
391
|
+
{searchQuery.length >= 3 && !messageSearchLoading && messageSearchCount > 0 && (
|
|
392
|
+
<div className="border-b border-slate-100 px-5 py-2 dark:border-slate-800">
|
|
393
|
+
<p className="text-xs text-slate-500 dark:text-slate-400">
|
|
394
|
+
{messageSearchCount} ticket{messageSearchCount > 1 ? 's' : ''} trouve{messageSearchCount > 1 ? 's' : ''} via les messages
|
|
395
|
+
</p>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{/* Collapsible filters */}
|
|
400
|
+
{showFilters && (
|
|
401
|
+
<div className="flex flex-wrap items-center gap-2 border-b border-slate-100 bg-slate-50/50 px-5 py-3 dark:border-slate-800 dark:bg-slate-800/30">
|
|
402
|
+
{tab === 'active' && (
|
|
403
|
+
<select
|
|
404
|
+
value={filterStatus}
|
|
405
|
+
onChange={(e) => { setFilterStatus(e.target.value); resetPage() }}
|
|
406
|
+
className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-600 outline-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300"
|
|
407
|
+
>
|
|
408
|
+
<option value="">Tous les statuts</option>
|
|
409
|
+
<option value="open">Ouvert</option>
|
|
410
|
+
<option value="waiting_client">En attente</option>
|
|
411
|
+
</select>
|
|
412
|
+
)}
|
|
413
|
+
<select
|
|
414
|
+
value={filterCategory}
|
|
415
|
+
onChange={(e) => { setFilterCategory(e.target.value); resetPage() }}
|
|
416
|
+
className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-600 outline-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300"
|
|
417
|
+
>
|
|
418
|
+
<option value="">Toutes categories</option>
|
|
419
|
+
{Object.entries(categoryLabels).map(([k, v]) => (
|
|
420
|
+
<option key={k} value={k}>{v}</option>
|
|
421
|
+
))}
|
|
422
|
+
</select>
|
|
423
|
+
{projects.length > 0 && (
|
|
424
|
+
<select
|
|
425
|
+
value={filterProject}
|
|
426
|
+
onChange={(e) => { setFilterProject(e.target.value); resetPage() }}
|
|
427
|
+
className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-600 outline-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300"
|
|
428
|
+
>
|
|
429
|
+
<option value="">Tous les projets</option>
|
|
430
|
+
{projects.map((p) => (
|
|
431
|
+
<option key={p} value={p}>{p}</option>
|
|
432
|
+
))}
|
|
433
|
+
</select>
|
|
434
|
+
)}
|
|
435
|
+
{activeFilterCount > 0 && (
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
onClick={() => { setFilterStatus(''); setFilterCategory(''); setFilterProject(''); resetPage() }}
|
|
439
|
+
className="text-sm text-slate-500 underline decoration-slate-300 underline-offset-2 transition-colors hover:text-slate-700 dark:text-slate-400 dark:decoration-slate-600 dark:hover:text-slate-300"
|
|
440
|
+
>
|
|
441
|
+
Effacer
|
|
442
|
+
</button>
|
|
443
|
+
)}
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
446
|
+
|
|
447
|
+
{/* Results count when searching */}
|
|
448
|
+
{searchQuery && (
|
|
449
|
+
<div className="border-b border-slate-100 px-5 py-2 dark:border-slate-800">
|
|
450
|
+
<p className="text-xs text-slate-500 dark:text-slate-400">
|
|
451
|
+
{sorted.length} resultat{sorted.length !== 1 ? 's' : ''} pour « {searchQuery} »
|
|
452
|
+
</p>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{/* Ticket list (inbox style) */}
|
|
457
|
+
{paginatedTickets.length === 0 ? (
|
|
458
|
+
<div className="px-5 py-16 text-center">
|
|
459
|
+
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-slate-100 dark:bg-slate-800">
|
|
460
|
+
{searchQuery ? (
|
|
461
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6 text-slate-400 dark:text-slate-500">
|
|
462
|
+
<circle cx="11" cy="11" r="8" />
|
|
463
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
464
|
+
</svg>
|
|
465
|
+
) : (
|
|
466
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6 text-slate-400 dark:text-slate-500">
|
|
467
|
+
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
|
468
|
+
<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" />
|
|
469
|
+
</svg>
|
|
470
|
+
)}
|
|
471
|
+
</div>
|
|
472
|
+
<h3 className="text-base font-semibold text-slate-700 dark:text-slate-300">
|
|
473
|
+
{searchQuery ? 'Aucun resultat' : tab === 'archived' ? 'Aucun ticket archive' : 'Aucun ticket'}
|
|
474
|
+
</h3>
|
|
475
|
+
<p className="mt-1 text-sm text-slate-400 dark:text-slate-500">
|
|
476
|
+
{searchQuery
|
|
477
|
+
? `Aucun ticket ne correspond a "${searchQuery}"`
|
|
478
|
+
: tab === 'archived'
|
|
479
|
+
? 'Vos tickets resolus apparaitront ici'
|
|
480
|
+
: "Vous n'avez pas encore de demande de support"}
|
|
481
|
+
</p>
|
|
482
|
+
{!searchQuery && tab === 'active' && (
|
|
483
|
+
<Link
|
|
484
|
+
href="/support/tickets/new"
|
|
485
|
+
className="mt-5 inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:bg-blue-700"
|
|
486
|
+
>
|
|
487
|
+
<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">
|
|
488
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
489
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
490
|
+
</svg>
|
|
491
|
+
Nouveau ticket
|
|
492
|
+
</Link>
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
495
|
+
) : (
|
|
496
|
+
<div className="divide-y divide-slate-100 dark:divide-slate-800">
|
|
497
|
+
{paginatedTickets.map((ticket) => {
|
|
498
|
+
const status = statusConfig[ticket.status || 'open'] || statusConfig.open
|
|
499
|
+
const priority = priorityConfig[ticket.priority || 'normal']
|
|
500
|
+
const category = ticket.category ? categoryLabels[ticket.category] : null
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
<Link
|
|
504
|
+
key={ticket.id}
|
|
505
|
+
href={`/support/tickets/${ticket.id}`}
|
|
506
|
+
className={`group flex items-start gap-3 px-4 py-4 transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/50 sm:items-center sm:gap-4 sm:px-5 ${
|
|
507
|
+
ticket.hasNewMessage ? 'bg-blue-50/30 dark:bg-blue-950/10' : ''
|
|
508
|
+
}`}
|
|
509
|
+
>
|
|
510
|
+
{/* Status dot */}
|
|
511
|
+
<div className="mt-1.5 flex-shrink-0 sm:mt-0">
|
|
512
|
+
<div className={`h-2.5 w-2.5 rounded-full ${status.dot} ${
|
|
513
|
+
ticket.hasNewMessage ? 'ring-4 ring-blue-100 dark:ring-blue-900/30' : ''
|
|
514
|
+
}`} />
|
|
515
|
+
</div>
|
|
516
|
+
|
|
517
|
+
{/* Content */}
|
|
518
|
+
<div className="min-w-0 flex-1">
|
|
519
|
+
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2">
|
|
520
|
+
<span className="font-mono text-xs font-medium text-slate-400 dark:text-slate-500">
|
|
521
|
+
{ticket.ticketNumber}
|
|
522
|
+
</span>
|
|
523
|
+
<h3 className={`truncate text-sm ${
|
|
524
|
+
ticket.hasNewMessage
|
|
525
|
+
? 'font-semibold text-slate-900 dark:text-white'
|
|
526
|
+
: 'font-medium text-slate-700 dark:text-slate-300'
|
|
527
|
+
}`}>
|
|
528
|
+
{ticket.subject}
|
|
529
|
+
</h3>
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
{/* Preview + meta */}
|
|
533
|
+
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
534
|
+
{ticket.lastMessagePreview && (
|
|
535
|
+
<p className="max-w-md truncate text-xs text-slate-400 dark:text-slate-500">
|
|
536
|
+
{ticket.lastMessagePreview}
|
|
537
|
+
</p>
|
|
538
|
+
)}
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
{/* Tags row (mobile friendly) */}
|
|
542
|
+
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
|
543
|
+
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium ring-1 ring-inset ${status.bg}`}>
|
|
544
|
+
{status.label}
|
|
545
|
+
</span>
|
|
546
|
+
{ticket.hasNewMessage && (
|
|
547
|
+
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-0.5 text-[11px] font-medium text-blue-700 ring-1 ring-inset ring-blue-600/20 dark:bg-blue-950/40 dark:text-blue-400 dark:ring-blue-400/20">
|
|
548
|
+
Nouveau message
|
|
549
|
+
</span>
|
|
550
|
+
)}
|
|
551
|
+
{category && (
|
|
552
|
+
<span className="inline-flex items-center rounded-md bg-slate-50 px-2 py-0.5 text-[11px] font-medium text-slate-600 ring-1 ring-inset ring-slate-500/10 dark:bg-slate-800 dark:text-slate-400 dark:ring-slate-400/10">
|
|
553
|
+
{category}
|
|
554
|
+
</span>
|
|
555
|
+
)}
|
|
556
|
+
{ticket.projectName && (
|
|
557
|
+
<span className="inline-flex items-center rounded-md bg-cyan-50 px-2 py-0.5 text-[11px] font-medium text-cyan-700 ring-1 ring-inset ring-cyan-600/20 dark:bg-cyan-950/40 dark:text-cyan-400 dark:ring-cyan-400/20">
|
|
558
|
+
{ticket.projectName}
|
|
559
|
+
</span>
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
{/* Right side meta */}
|
|
565
|
+
<div className="flex flex-shrink-0 flex-col items-end gap-1.5">
|
|
566
|
+
<span className="text-xs text-slate-400 dark:text-slate-500">
|
|
567
|
+
{ticket.updatedAt ? timeAgo(ticket.updatedAt) : ''}
|
|
568
|
+
</span>
|
|
569
|
+
<div className="flex items-center gap-2">
|
|
570
|
+
{ticket.messageCount > 0 && (
|
|
571
|
+
<span className="flex items-center gap-1 text-xs text-slate-400 dark:text-slate-500">
|
|
572
|
+
<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-3 w-3">
|
|
573
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
574
|
+
</svg>
|
|
575
|
+
{ticket.messageCount}
|
|
576
|
+
</span>
|
|
577
|
+
)}
|
|
578
|
+
{priority && (
|
|
579
|
+
<span className={`text-xs font-medium ${priority.color}`}>
|
|
580
|
+
{priority.label}
|
|
581
|
+
</span>
|
|
582
|
+
)}
|
|
583
|
+
</div>
|
|
584
|
+
{/* Chevron */}
|
|
585
|
+
<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-300 transition-colors group-hover:text-slate-500 dark:text-slate-600 dark:group-hover:text-slate-400">
|
|
586
|
+
<polyline points="9 18 15 12 9 6" />
|
|
587
|
+
</svg>
|
|
588
|
+
</div>
|
|
589
|
+
</Link>
|
|
590
|
+
)
|
|
591
|
+
})}
|
|
592
|
+
</div>
|
|
593
|
+
)}
|
|
594
|
+
|
|
595
|
+
{/* Pagination */}
|
|
596
|
+
{totalPages > 1 && (
|
|
597
|
+
<div className="flex items-center justify-between border-t border-slate-100 px-5 py-3 dark:border-slate-800">
|
|
598
|
+
<p className="text-xs text-slate-500 dark:text-slate-400">
|
|
599
|
+
Page {safePage} sur {totalPages} ({sorted.length} tickets)
|
|
600
|
+
</p>
|
|
601
|
+
<div className="flex items-center gap-1.5">
|
|
602
|
+
<button
|
|
603
|
+
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
|
604
|
+
disabled={safePage <= 1}
|
|
605
|
+
className="rounded-lg border border-slate-200 px-2.5 py-1.5 text-sm text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-30 dark:border-slate-700 dark:text-slate-400 dark:hover:bg-slate-800"
|
|
606
|
+
>
|
|
607
|
+
<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">
|
|
608
|
+
<polyline points="15 18 9 12 15 6" />
|
|
609
|
+
</svg>
|
|
610
|
+
</button>
|
|
611
|
+
{Array.from({ length: totalPages }, (_, i) => i + 1)
|
|
612
|
+
.filter((p) => p === 1 || p === totalPages || Math.abs(p - safePage) <= 1)
|
|
613
|
+
.reduce<(number | 'ellipsis')[]>((acc, p, idx, arr) => {
|
|
614
|
+
if (idx > 0 && p - (arr[idx - 1] as number) > 1) acc.push('ellipsis')
|
|
615
|
+
acc.push(p)
|
|
616
|
+
return acc
|
|
617
|
+
}, [])
|
|
618
|
+
.map((item, idx) =>
|
|
619
|
+
item === 'ellipsis' ? (
|
|
620
|
+
<span key={`e-${idx}`} className="px-1 text-xs text-slate-400">...</span>
|
|
621
|
+
) : (
|
|
622
|
+
<button
|
|
623
|
+
key={item}
|
|
624
|
+
onClick={() => setCurrentPage(item)}
|
|
625
|
+
className={`rounded-lg px-2.5 py-1.5 text-sm font-medium transition-colors ${
|
|
626
|
+
safePage === item
|
|
627
|
+
? 'bg-blue-600 text-white'
|
|
628
|
+
: 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800'
|
|
629
|
+
}`}
|
|
630
|
+
>
|
|
631
|
+
{item}
|
|
632
|
+
</button>
|
|
633
|
+
),
|
|
634
|
+
)}
|
|
635
|
+
<button
|
|
636
|
+
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
|
637
|
+
disabled={safePage >= totalPages}
|
|
638
|
+
className="rounded-lg border border-slate-200 px-2.5 py-1.5 text-sm text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-30 dark:border-slate-700 dark:text-slate-400 dark:hover:bg-slate-800"
|
|
639
|
+
>
|
|
640
|
+
<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">
|
|
641
|
+
<polyline points="9 18 15 12 9 6" />
|
|
642
|
+
</svg>
|
|
643
|
+
</button>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
)}
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
)
|
|
650
|
+
}
|