@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,33 @@
1
+ import type { AdminViewServerProps } from 'payload'
2
+ import { DefaultTemplate } from '@payloadcms/next/templates'
3
+ import { redirect } from 'next/navigation'
4
+ import React from 'react'
5
+ import { AdminErrorBoundary } from '../shared/ErrorBoundary'
6
+ import { TicketDetailClient } from './client'
7
+
8
+ export const TicketDetailView: React.FC<AdminViewServerProps> = ({ initPageResult }) => {
9
+ const { req, visibleEntities } = initPageResult
10
+
11
+ if (!req.user) {
12
+ redirect('/admin/login')
13
+ }
14
+
15
+ return (
16
+ <DefaultTemplate
17
+ i18n={req.i18n}
18
+ locale={initPageResult.locale}
19
+ params={{}}
20
+ payload={req.payload}
21
+ permissions={initPageResult.permissions}
22
+ searchParams={{}}
23
+ user={req.user}
24
+ visibleEntities={visibleEntities}
25
+ >
26
+ <AdminErrorBoundary viewName="TicketDetailView">
27
+ <TicketDetailClient />
28
+ </AdminErrorBoundary>
29
+ </DefaultTemplate>
30
+ )
31
+ }
32
+
33
+ export default TicketDetailView
@@ -0,0 +1,313 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useCallback } from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
7
+ import s from '../../styles/TicketInbox.module.scss'
8
+
9
+ interface Ticket {
10
+ id: number
11
+ ticketNumber: string
12
+ subject: string
13
+ status: string
14
+ priority: string
15
+ category?: string
16
+ client?: { id: number; firstName?: string; lastName?: string; company?: string } | number
17
+ updatedAt: string
18
+ createdAt: string
19
+ lastClientMessageAt?: string
20
+ lastAdminReadAt?: string
21
+ }
22
+
23
+ type Tab = 'all' | 'open' | 'waiting_client' | 'resolved'
24
+
25
+ const STATUS_DOTS: Record<string, string> = {
26
+ open: '#22c55e',
27
+ waiting_client: '#eab308',
28
+ resolved: '#94a3b8',
29
+ }
30
+
31
+ const STATUS_LABEL_KEYS: Record<string, string> = {
32
+ open: 'inbox.tabs.open',
33
+ waiting_client: 'inbox.tabs.waiting',
34
+ resolved: 'inbox.tabs.resolved',
35
+ }
36
+
37
+ const PRIORITY_COLORS: Record<string, string> = {
38
+ urgent: '#ef4444',
39
+ high: '#f97316',
40
+ normal: 'transparent',
41
+ low: 'transparent',
42
+ }
43
+
44
+ const CATEGORY_LABEL_KEYS: Record<string, string> = {
45
+ bug: 'ticket.category.bug',
46
+ content: 'ticket.category.content',
47
+ feature: 'ticket.category.feature',
48
+ question: 'ticket.category.question',
49
+ hosting: 'ticket.category.hosting',
50
+ }
51
+
52
+ function relativeTime(dateStr: string): string {
53
+ const diff = Date.now() - new Date(dateStr).getTime()
54
+ const mins = Math.floor(diff / 60000)
55
+ if (mins < 1) return 'maintenant'
56
+ if (mins < 60) return `${mins}m`
57
+ const hours = Math.floor(mins / 60)
58
+ if (hours < 24) return `${hours}h`
59
+ const days = Math.floor(hours / 24)
60
+ if (days < 7) return `${days}j`
61
+ return new Date(dateStr).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })
62
+ }
63
+
64
+ export const TicketInboxClient: React.FC = () => {
65
+ const { t } = useTranslation()
66
+ const searchParams = useSearchParams()
67
+ const [tickets, setTickets] = useState<Ticket[]>([])
68
+ const [loading, setLoading] = useState(true)
69
+ const [tab, setTab] = useState<Tab>(() => {
70
+ const urlTab = searchParams.get('tab')
71
+ if (urlTab && ['all', 'open', 'waiting_client', 'resolved'].includes(urlTab)) return urlTab as Tab
72
+ return 'all'
73
+ })
74
+ const [search, setSearch] = useState('')
75
+ const [sort, setSort] = useState('-updatedAt')
76
+ const [counts, setCounts] = useState({ all: 0, open: 0, waiting: 0, resolved: 0 })
77
+ const [selectedIdx, setSelectedIdx] = useState(-1)
78
+ const [checkedIds, setCheckedIds] = useState<Set<number>>(new Set())
79
+ const [bulkAction, setBulkAction] = useState('')
80
+ const [bulkProcessing, setBulkProcessing] = useState(false)
81
+
82
+ const toggleCheck = (id: number) => {
83
+ setCheckedIds((prev) => {
84
+ const next = new Set(prev)
85
+ if (next.has(id)) next.delete(id); else next.add(id)
86
+ return next
87
+ })
88
+ }
89
+
90
+ const _toggleAll = () => {
91
+ if (checkedIds.size === tickets.length) setCheckedIds(new Set())
92
+ else setCheckedIds(new Set(tickets.map((tk) => tk.id)))
93
+ }
94
+
95
+ const handleBulkAction = async (action: string) => {
96
+ if (checkedIds.size === 0) return
97
+ setBulkProcessing(true)
98
+ try {
99
+ const res = await fetch('/api/support/bulk-action', {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ credentials: 'include',
103
+ body: JSON.stringify({ ticketIds: Array.from(checkedIds), action }),
104
+ })
105
+ if (res.ok) {
106
+ setCheckedIds(new Set())
107
+ setBulkAction('')
108
+ fetchTickets()
109
+ }
110
+ } catch { /* silent */ }
111
+ setBulkProcessing(false)
112
+ }
113
+
114
+ const fetchTickets = useCallback(async () => {
115
+ const params = [`limit=30`, `sort=${sort}`, `depth=1`, `select[id]=true`, `select[ticketNumber]=true`, `select[subject]=true`, `select[status]=true`, `select[priority]=true`, `select[category]=true`, `select[client]=true`, `select[updatedAt]=true`, `select[lastClientMessageAt]=true`, `select[lastAdminReadAt]=true`]
116
+ if (tab !== 'all') params.push(`where[status][equals]=${tab}`)
117
+ if (search.trim()) {
118
+ params.push(`where[or][0][subject][contains]=${encodeURIComponent(search)}`)
119
+ params.push(`where[or][1][ticketNumber][contains]=${encodeURIComponent(search)}`)
120
+ }
121
+ try {
122
+ const url = `/api/tickets?${params.join('&')}`
123
+ const res = await fetch(url, { credentials: 'include' })
124
+ if (res.ok) {
125
+ const d = await res.json()
126
+ setTickets(d.docs || [])
127
+ } else {
128
+ console.error('[inbox] Fetch failed:', res.status, await res.text().catch(() => ''))
129
+ }
130
+ } catch (err) {
131
+ console.error('[inbox] Fetch error:', err)
132
+ }
133
+ setLoading(false)
134
+ }, [tab, sort, search])
135
+
136
+ useEffect(() => { setLoading(true); fetchTickets() }, [fetchTickets])
137
+
138
+ // Fetch counts
139
+ useEffect(() => {
140
+ const fetchCounts = async () => {
141
+ try {
142
+ const [all, openRes, waiting, resolved] = await Promise.all([
143
+ fetch('/api/tickets?limit=0&depth=0', { credentials: 'include' }),
144
+ fetch('/api/tickets?limit=0&depth=0&where[status][equals]=open', { credentials: 'include' }),
145
+ fetch('/api/tickets?limit=0&depth=0&where[status][equals]=waiting_client', { credentials: 'include' }),
146
+ fetch('/api/tickets?limit=0&depth=0&where[status][equals]=resolved', { credentials: 'include' }),
147
+ ])
148
+ const [a, o, w, r] = await Promise.all([all.json(), openRes.json(), waiting.json(), resolved.json()])
149
+ setCounts({ all: a.totalDocs || 0, open: o.totalDocs || 0, waiting: w.totalDocs || 0, resolved: r.totalDocs || 0 })
150
+ } catch { /* silent */ }
151
+ }
152
+ fetchCounts()
153
+ }, [])
154
+
155
+ // Auto-refresh 60s
156
+ useEffect(() => {
157
+ const iv = setInterval(fetchTickets, 60000)
158
+ return () => clearInterval(iv)
159
+ }, [fetchTickets])
160
+
161
+ // Keyboard navigation
162
+ useEffect(() => {
163
+ const handleKey = (e: KeyboardEvent) => {
164
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
165
+ if (e.key === 'j' || e.key === 'ArrowDown') { e.preventDefault(); setSelectedIdx((p) => Math.min(p + 1, tickets.length - 1)) }
166
+ if (e.key === 'k' || e.key === 'ArrowUp') { e.preventDefault(); setSelectedIdx((p) => Math.max(p - 1, 0)) }
167
+ if (e.key === 'Enter' && selectedIdx >= 0 && tickets[selectedIdx]) {
168
+ window.location.href = `/admin/support/ticket?id=${tickets[selectedIdx].id}`
169
+ }
170
+ }
171
+ window.addEventListener('keydown', handleKey)
172
+ return () => window.removeEventListener('keydown', handleKey)
173
+ }, [tickets, selectedIdx])
174
+
175
+ const tabs: Array<{ key: Tab; label: string; count: number }> = [
176
+ { key: 'all', label: t('inbox.tabs.all'), count: counts.all },
177
+ { key: 'open', label: t('inbox.tabs.open'), count: counts.open },
178
+ { key: 'waiting_client', label: t('inbox.tabs.waiting'), count: counts.waiting },
179
+ { key: 'resolved', label: t('inbox.tabs.resolved'), count: counts.resolved },
180
+ ]
181
+
182
+ const S: Record<string, React.CSSProperties> = {
183
+ page: { padding: '20px 30px', maxWidth: 1100, margin: '0 auto' },
184
+ header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
185
+ title: { fontSize: 24, fontWeight: 700, margin: 0, color: 'var(--theme-text)' },
186
+ headerRight: { display: 'flex', gap: 10, alignItems: 'center' },
187
+ searchWrap: { position: 'relative' as const, display: 'flex', alignItems: 'center' },
188
+ searchInput: { padding: '6px 12px 6px 30px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, background: 'var(--theme-elevation-0)', color: 'var(--theme-text)', width: 200 },
189
+ newTicketBtn: { padding: '7px 14px', borderRadius: 8, background: '#2563eb', color: '#fff', fontSize: 13, fontWeight: 600, textDecoration: 'none', whiteSpace: 'nowrap' as const },
190
+ tabsRow: { display: 'flex', gap: 4, marginBottom: 12, borderBottom: '1px solid var(--theme-elevation-200)', paddingBottom: 8 },
191
+ tab: { padding: '6px 12px', borderRadius: 6, border: 'none', background: 'none', cursor: 'pointer', fontSize: 13, color: 'var(--theme-elevation-500)', fontWeight: 500 },
192
+ tabActive: { background: 'var(--theme-elevation-100)', color: 'var(--theme-text)', fontWeight: 700 },
193
+ sortRow: { display: 'flex', alignItems: 'center', marginBottom: 12 },
194
+ sortSelect: { padding: '4px 10px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, background: 'var(--theme-elevation-0)', color: 'var(--theme-text)', cursor: 'pointer' },
195
+ loading: { padding: 40, textAlign: 'center' as const, color: '#94a3b8' },
196
+ empty: { padding: 60, textAlign: 'center' as const, color: '#94a3b8' },
197
+ list: { display: 'flex', flexDirection: 'column' as const, gap: 1 },
198
+ row: { display: 'grid', gridTemplateColumns: '20px 10px 60px 70px 1fr 140px 80px 4px 50px 10px', gap: 8, alignItems: 'center', padding: '10px 12px', borderRadius: 8, textDecoration: 'none', color: 'var(--theme-text)', fontSize: 13, cursor: 'pointer', transition: 'background 100ms', background: 'var(--theme-elevation-0)' },
199
+ rowHover: { background: 'var(--theme-elevation-50)' },
200
+ statusDot: { width: 8, height: 8, borderRadius: '50%' },
201
+ priorityBar: { width: 4, height: 20, borderRadius: 2 },
202
+ unreadDot: { width: 8, height: 8, borderRadius: '50%', background: '#2563eb' },
203
+ keyboardHints: { display: 'flex', gap: 12, marginTop: 16, fontSize: 11, color: 'var(--theme-elevation-400)' },
204
+ }
205
+
206
+ return (
207
+ <div style={S.page}>
208
+ {/* Header */}
209
+ <div style={S.header}>
210
+ <h1 style={S.title}>{t('inbox.title')}</h1>
211
+ <div style={S.headerRight}>
212
+ <div style={S.searchWrap}>
213
+ <input
214
+ type="text"
215
+ style={S.searchInput}
216
+ placeholder={t('inbox.searchPlaceholder')}
217
+ value={search}
218
+ onChange={(e) => setSearch(e.target.value)}
219
+ />
220
+ </div>
221
+ <Link href="/admin/support/new-ticket" style={S.newTicketBtn}>{t('inbox.newTicketBtn')}</Link>
222
+ </div>
223
+ </div>
224
+
225
+ {/* Tabs */}
226
+ <div style={S.tabsRow}>
227
+ {tabs.map((tb) => (
228
+ <button
229
+ key={tb.key}
230
+ style={{ ...S.tab, ...(tab === tb.key ? S.tabActive : {}) }}
231
+ onClick={() => { setTab(tb.key); setSelectedIdx(-1) }}
232
+ >
233
+ {tb.label} <span style={{ opacity: 0.6 }}>({tb.count})</span>
234
+ </button>
235
+ ))}
236
+ </div>
237
+
238
+ {/* Sort + Bulk */}
239
+ <div style={S.sortRow}>
240
+ {checkedIds.size > 0 ? (
241
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center', flex: 1 }}>
242
+ <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--theme-text)' }}>{checkedIds.size > 1 ? t('inbox.selectedPlural', { count: String(checkedIds.size) }) : t('inbox.selected', { count: String(checkedIds.size) })}</span>
243
+ <button style={S.sortSelect} onClick={() => handleBulkAction('close')} disabled={bulkProcessing}>{t('inbox.closeAction')}</button>
244
+ <button style={S.sortSelect} onClick={() => handleBulkAction('reopen')} disabled={bulkProcessing}>{t('inbox.reopenAction')}</button>
245
+ <select style={S.sortSelect} value={bulkAction} onChange={(e) => { if (e.target.value) handleBulkAction(e.target.value); setBulkAction('') }}>
246
+ <option value="">{t('inbox.moreActions')}</option>
247
+ <option value="set_priority">{t('inbox.changePriority')}</option>
248
+ <option value="delete">{t('inbox.deleteAction')}</option>
249
+ </select>
250
+ <button style={{ ...S.sortSelect, marginLeft: 'auto' }} onClick={() => setCheckedIds(new Set())}>{t('inbox.deselect')}</button>
251
+ </div>
252
+ ) : (
253
+ <select style={S.sortSelect} value={sort} onChange={(e) => setSort(e.target.value)}>
254
+ <option value="-updatedAt">{t('inbox.sort.newest')}</option>
255
+ <option value="updatedAt">{t('inbox.sort.oldest')}</option>
256
+ <option value="-createdAt">{t('inbox.sort.created')}</option>
257
+ <option value="priority">{t('inbox.sort.priority')}</option>
258
+ </select>
259
+ )}
260
+ </div>
261
+
262
+ {/* List */}
263
+ {loading ? (
264
+ <div style={S.loading}>{t('common.loading')}</div>
265
+ ) : tickets.length === 0 ? (
266
+ <div style={S.empty}>{t('inbox.empty')}</div>
267
+ ) : (
268
+ <div style={S.list}>
269
+ {tickets.map((tk, idx) => {
270
+ const clientObj = typeof tk.client === 'object' ? tk.client : null
271
+ const clientName = clientObj ? `${clientObj.firstName || ''} ${clientObj.lastName || ''}`.trim() : ''
272
+ const clientCompany = clientObj?.company || ''
273
+ const displayClient = clientName ? `${clientName}${clientCompany ? `, ${clientCompany}` : ''}` : '--'
274
+ const isUnread = tk.lastClientMessageAt && (!tk.lastAdminReadAt || new Date(tk.lastClientMessageAt) > new Date(tk.lastAdminReadAt))
275
+ const priorityColor = PRIORITY_COLORS[tk.priority] || 'transparent'
276
+
277
+ return (
278
+ <a
279
+ key={tk.id}
280
+ href={`/admin/support/ticket?id=${tk.id}`}
281
+ style={{ ...S.row, ...(idx === selectedIdx ? { background: 'var(--theme-elevation-100)' } : {}) }}
282
+ onClick={(e) => { e.preventDefault(); window.location.href = `/admin/support/ticket?id=${tk.id}` }}
283
+ >
284
+ <input
285
+ type="checkbox"
286
+ checked={checkedIds.has(tk.id)}
287
+ onClick={(e) => e.stopPropagation()}
288
+ onChange={() => toggleCheck(tk.id)}
289
+ style={{ width: 14, height: 14, cursor: 'pointer', accentColor: '#2563eb' }}
290
+ />
291
+ <div style={{ ...S.statusDot, backgroundColor: STATUS_DOTS[tk.status] || '#94a3b8' }} />
292
+ <span style={{ fontSize: 11, color: 'var(--theme-elevation-500)' }}>{STATUS_LABEL_KEYS[tk.status] ? t(STATUS_LABEL_KEYS[tk.status]) : tk.status}</span>
293
+ <span style={{ fontWeight: 600, fontSize: 12, color: 'var(--theme-elevation-400)' }}>{tk.ticketNumber}</span>
294
+ <span style={{ fontWeight: 500 }}>{tk.subject}</span>
295
+ <span style={{ fontSize: 12, color: 'var(--theme-elevation-500)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{displayClient}</span>
296
+ {tk.category ? <span style={{ fontSize: 11, color: 'var(--theme-elevation-400)' }}>[{CATEGORY_LABEL_KEYS[tk.category] ? t(CATEGORY_LABEL_KEYS[tk.category]) : tk.category}]</span> : <span />}
297
+ <div style={{ ...S.priorityBar, backgroundColor: priorityColor }} />
298
+ <span style={{ fontSize: 11, color: 'var(--theme-elevation-400)', textAlign: 'right' }}>{relativeTime(tk.updatedAt)}</span>
299
+ {isUnread ? <div style={S.unreadDot} /> : <span />}
300
+ </a>
301
+ )
302
+ })}
303
+ </div>
304
+ )}
305
+
306
+ {/* Keyboard hints */}
307
+ <div style={S.keyboardHints}>
308
+ <span><kbd>&#8593;</kbd><kbd>&#8595;</kbd> {t('inbox.keyboardNavigate')}</span>
309
+ <span><kbd>&#8629;</kbd> {t('inbox.keyboardOpen')}</span>
310
+ </div>
311
+ </div>
312
+ )
313
+ }
@@ -0,0 +1,30 @@
1
+ import type { AdminViewServerProps } from 'payload'
2
+ import { DefaultTemplate } from '@payloadcms/next/templates'
3
+ import { redirect } from 'next/navigation'
4
+ import React from 'react'
5
+ import { AdminErrorBoundary } from '../shared/ErrorBoundary'
6
+ import { TicketInboxClient } from './client'
7
+
8
+ export const TicketInboxView: React.FC<AdminViewServerProps> = ({ initPageResult }) => {
9
+ const { req, visibleEntities } = initPageResult
10
+ if (!req.user) redirect('/admin/login')
11
+
12
+ return (
13
+ <DefaultTemplate
14
+ i18n={req.i18n}
15
+ locale={initPageResult.locale}
16
+ params={{}}
17
+ payload={req.payload}
18
+ permissions={initPageResult.permissions}
19
+ searchParams={{}}
20
+ user={req.user}
21
+ visibleEntities={visibleEntities}
22
+ >
23
+ <AdminErrorBoundary viewName="TicketInboxView">
24
+ <TicketInboxClient />
25
+ </AdminErrorBoundary>
26
+ </DefaultTemplate>
27
+ )
28
+ }
29
+
30
+ export default TicketInboxView