@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,124 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useCallback } from 'react'
4
+ import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
5
+ import s from '../../styles/EmailTracking.module.scss'
6
+
7
+ interface EmailLog { id: number; status: 'success' | 'ignored' | 'error'; action?: string; subject?: string; recipientEmail?: string; ticketNumber?: string; errorMessage?: string; httpStatus?: number; processingTimeMs?: number; createdAt: string }
8
+ interface Stats { total: number; success: number; errors: number; ignored: number; successRate: number; avgProcessingTime: number }
9
+ type StatusTab = 'all' | 'success' | 'error' | 'ignored'
10
+ type DateRange = 7 | 30 | 90
11
+
12
+ const STATUS_CFG: Record<string, { key: string; bg: string; color: string }> = {
13
+ success: { key: 'emailTracking.status.success', bg: '#dcfce7', color: '#16a34a' },
14
+ error: { key: 'emailTracking.status.error', bg: '#fef2f2', color: '#dc2626' },
15
+ ignored: { key: 'emailTracking.status.ignored', bg: '#f3f4f6', color: '#6b7280' },
16
+ }
17
+
18
+ function fmtDate(d: string): string { return new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' }) }
19
+
20
+ export const EmailTrackingClient: React.FC = () => {
21
+ const { t } = useTranslation()
22
+ const [logs, setLogs] = useState<EmailLog[]>([])
23
+ const [stats, setStats] = useState<Stats | null>(null)
24
+ const [loading, setLoading] = useState(true)
25
+ const [tab, setTab] = useState<StatusTab>('all')
26
+ const [dateRange, setDateRange] = useState<DateRange>(7)
27
+ const [search, setSearch] = useState('')
28
+ const [expandedRow, setExpandedRow] = useState<number | null>(null)
29
+ const [page, setPage] = useState(1)
30
+ const [hasMore, setHasMore] = useState(false)
31
+ const [totalDocs, setTotalDocs] = useState(0)
32
+
33
+ const fetchStats = useCallback(async () => { try { const r = await fetch(`/api/support/email-stats?days=${dateRange}`); if (r.ok) setStats(await r.json()) } catch (err) { console.warn('[support] fetchStats error:', err) } }, [dateRange])
34
+ const fetchLogs = useCallback(async () => {
35
+ const cutoff = new Date(Date.now() - dateRange * 86400000).toISOString()
36
+ const where = [`where[createdAt][greater_than]=${cutoff}`]
37
+ if (tab !== 'all') where.push(`where[status][equals]=${tab}`)
38
+ if (search.trim()) { where.push(`where[or][0][recipientEmail][contains]=${encodeURIComponent(search)}`, `where[or][1][subject][contains]=${encodeURIComponent(search)}`) }
39
+ try { const r = await fetch(`/api/email-logs?${where.join('&')}&sort=-createdAt&limit=30&page=${page}&depth=0`); if (r.ok) { const d = await r.json(); setLogs(d.docs); setHasMore(d.hasNextPage); setTotalDocs(d.totalDocs) } } catch (err) { console.warn('[support] fetchLogs error:', err) }
40
+ setLoading(false)
41
+ }, [dateRange, tab, search, page])
42
+
43
+ useEffect(() => { setLoading(true); setPage(1) }, [dateRange, tab, search])
44
+ useEffect(() => { fetchStats(); fetchLogs() }, [fetchStats, fetchLogs])
45
+
46
+ const S: Record<string, React.CSSProperties> = {
47
+ page: { padding: '20px 30px', maxWidth: 1100, margin: '0 auto' },
48
+ statGrid: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 20 },
49
+ statCard: { padding: '12px 16px', borderRadius: 8, border: '1px solid var(--theme-elevation-150)' },
50
+ statLabel: { fontSize: 11, color: 'var(--theme-elevation-500)' },
51
+ statValue: { fontSize: 22, fontWeight: 700, color: 'var(--theme-text)' },
52
+ filters: { display: 'flex', gap: 12, alignItems: 'center', marginBottom: 16, flexWrap: 'wrap' as const },
53
+ tab: { padding: '4px 10px', borderRadius: 6, border: 'none', background: 'none', cursor: 'pointer', fontSize: 12, color: 'var(--theme-elevation-500)' },
54
+ tabActive: { background: 'var(--theme-elevation-100)', fontWeight: 700, color: 'var(--theme-text)' },
55
+ dateBtn: { padding: '4px 8px', borderRadius: 4, border: '1px solid var(--theme-elevation-200)', fontSize: 11, cursor: 'pointer', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' },
56
+ dateBtnActive: { background: '#2563eb', color: '#fff', borderColor: '#2563eb' },
57
+ table: { width: '100%', borderCollapse: 'collapse' as const, fontSize: 13 },
58
+ th: { textAlign: 'left' as const, padding: '6px 8px', borderBottom: '1px solid var(--theme-elevation-200)', fontSize: 11, color: 'var(--theme-elevation-500)' },
59
+ td: { padding: '6px 8px', borderBottom: '1px solid var(--theme-elevation-100)' },
60
+ badge: { padding: '2px 6px', borderRadius: 4, fontSize: 11, fontWeight: 600 },
61
+ btn: { padding: '4px 10px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 11, cursor: 'pointer', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' },
62
+ }
63
+
64
+ return (
65
+ <div style={S.page}>
66
+ <h1 style={{ fontSize: 22, fontWeight: 700, marginBottom: 16, color: 'var(--theme-text)' }}>{t('emailTracking.title')}</h1>
67
+
68
+ <div style={S.statGrid}>
69
+ <div style={S.statCard}><div style={S.statLabel}>{t('emailTracking.stats.emailsSent')}</div><div style={S.statValue}>{stats?.total ?? '--'}</div></div>
70
+ <div style={S.statCard}><div style={S.statLabel}>{t('emailTracking.stats.successRate')}</div><div style={S.statValue}>{stats ? `${stats.successRate}%` : '--'}</div></div>
71
+ <div style={S.statCard}><div style={S.statLabel}>{t('emailTracking.stats.errors')}</div><div style={S.statValue}>{stats?.errors ?? '--'}</div></div>
72
+ <div style={S.statCard}><div style={S.statLabel}>{t('emailTracking.stats.avgTime')}</div><div style={S.statValue}>{stats ? `${stats.avgProcessingTime}ms` : '--'}</div></div>
73
+ </div>
74
+
75
+ <div style={S.filters}>
76
+ <div style={{ display: 'flex', gap: 4 }}>
77
+ {(['all', 'success', 'error', 'ignored'] as StatusTab[]).map((st) => (
78
+ <button key={st} style={{ ...S.tab, ...(tab === st ? S.tabActive : {}) }} onClick={() => setTab(st)}>{t(`emailTracking.tabs.${st === 'error' ? 'errors' : st}`)}</button>
79
+ ))}
80
+ </div>
81
+ <div style={{ display: 'flex', gap: 4 }}>
82
+ {([7, 30, 90] as DateRange[]).map((v) => (
83
+ <button key={v} style={{ ...S.dateBtn, ...(dateRange === v ? S.dateBtnActive : {}) }} onClick={() => setDateRange(v)}>{v}j</button>
84
+ ))}
85
+ </div>
86
+ <input type="text" placeholder={t('emailTracking.searchPlaceholder')} value={search} onChange={(e) => setSearch(e.target.value)} style={{ padding: '6px 10px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' }} />
87
+ </div>
88
+
89
+ {loading ? <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t('common.loading')}</div>
90
+ : logs.length === 0 ? <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t('emailTracking.noLogs')}</div>
91
+ : (
92
+ <table style={S.table}>
93
+ <thead><tr><th style={S.th}>{t('emailTracking.tableHeaders.date')}</th><th style={S.th}>{t('emailTracking.tableHeaders.recipient')}</th><th style={S.th}>{t('emailTracking.tableHeaders.subject')}</th><th style={S.th}>{t('emailTracking.tableHeaders.ticket')}</th><th style={S.th}>{t('emailTracking.tableHeaders.status')}</th><th style={S.th}>{t('emailTracking.tableHeaders.action')}</th><th style={{ ...S.th, textAlign: 'right' }}>{t('emailTracking.tableHeaders.time')}</th></tr></thead>
94
+ <tbody>
95
+ {logs.map((log) => (
96
+ <React.Fragment key={log.id}>
97
+ <tr onClick={() => log.status === 'error' && log.errorMessage ? setExpandedRow(expandedRow === log.id ? null : log.id) : null} style={{ cursor: log.status === 'error' ? 'pointer' : 'default' }}>
98
+ <td style={{ ...S.td, fontSize: 12, whiteSpace: 'nowrap' }}>{fmtDate(log.createdAt)}</td>
99
+ <td style={{ ...S.td, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{log.recipientEmail || '--'}</td>
100
+ <td style={{ ...S.td, maxWidth: 250, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{log.subject || '--'}</td>
101
+ <td style={S.td}>{log.ticketNumber || '--'}</td>
102
+ <td style={S.td}><span style={{ ...S.badge, background: STATUS_CFG[log.status]?.bg, color: STATUS_CFG[log.status]?.color }}>{STATUS_CFG[log.status]?.key ? t(STATUS_CFG[log.status].key) : log.status}</span></td>
103
+ <td style={{ ...S.td, fontSize: 12 }}>{log.action || '--'}</td>
104
+ <td style={{ ...S.td, textAlign: 'right' }}>{log.processingTimeMs != null ? <span style={{ fontFamily: 'monospace', fontSize: 12, color: log.processingTimeMs > 2000 ? '#dc2626' : '#16a34a' }}>{log.processingTimeMs}ms</span> : '--'}</td>
105
+ </tr>
106
+ {expandedRow === log.id && log.errorMessage && (
107
+ <tr><td colSpan={7}><div style={{ padding: '8px 12px', background: '#fef2f2', borderRadius: 6, fontSize: 12, color: '#dc2626' }}>Erreur{log.httpStatus ? ` (HTTP ${log.httpStatus})` : ''}: {log.errorMessage}</div></td></tr>
108
+ )}
109
+ </React.Fragment>
110
+ ))}
111
+ </tbody>
112
+ </table>
113
+ )}
114
+
115
+ {totalDocs > 0 && (
116
+ <div style={{ display: 'flex', justifyContent: 'center', gap: 12, marginTop: 16, alignItems: 'center' }}>
117
+ <button style={S.btn} onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>{t('common.previous')}</button>
118
+ <span style={{ fontSize: 12, color: 'var(--theme-elevation-500)' }}>{t('common.page')} {page} -- {totalDocs} {t('common.results')}</span>
119
+ <button style={S.btn} onClick={() => setPage((p) => p + 1)} disabled={!hasMore}>{t('common.next')}</button>
120
+ </div>
121
+ )}
122
+ </div>
123
+ )
124
+ }
@@ -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 { EmailTrackingClient } from './client'
7
+
8
+ export const EmailTrackingView: 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="EmailTrackingView">
27
+ <EmailTrackingClient />
28
+ </AdminErrorBoundary>
29
+ </DefaultTemplate>
30
+ )
31
+ }
32
+
33
+ export default EmailTrackingView
@@ -0,0 +1,133 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useCallback, useRef } from 'react'
4
+ import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
5
+ import s from '../../styles/ImportConversation.module.scss'
6
+
7
+ interface PreviewMessage { from: 'client' | 'admin'; name: string; date: string; preview: string }
8
+ interface PreviewData { client: { email: string; name: string; company: string }; subject: string; messageCount: number; messages: PreviewMessage[]; parseMethod: string }
9
+ interface ImportResult { ticketNumber: string; ticketId: number; clientEmail: string; clientName: string; clientCompany: string; isNewClient: boolean; messagesImported: number }
10
+
11
+ export function ImportConversationClient() {
12
+ const { t } = useTranslation()
13
+ const [markdown, setMarkdown] = useState('')
14
+ const [fileName, setFileName] = useState('')
15
+ const [isDragOver, setIsDragOver] = useState(false)
16
+ const [preview, setPreview] = useState<PreviewData | null>(null)
17
+ const [result, setResult] = useState<ImportResult | null>(null)
18
+ const [error, setError] = useState('')
19
+ const [loading, setLoading] = useState(false)
20
+ const fileRef = useRef<HTMLInputElement>(null)
21
+
22
+ const handleFile = useCallback((file: File) => {
23
+ if (!file.name.endsWith('.md') && !file.name.endsWith('.txt')) { setError(t('import.formatError')); return }
24
+ if (file.size > 512_000) { setError(t('import.sizeError')); return }
25
+ setError(''); setFileName(file.name); setResult(null); setPreview(null)
26
+ const reader = new FileReader()
27
+ reader.onload = (e) => { setMarkdown(e.target?.result as string) }
28
+ reader.readAsText(file)
29
+ }, [])
30
+
31
+ const onDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); const file = e.dataTransfer.files[0]; if (file) handleFile(file) }, [handleFile])
32
+ const onFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (file) handleFile(file) }, [handleFile])
33
+
34
+ const doPreview = useCallback(async () => {
35
+ if (!markdown) return; setLoading(true); setError(''); setPreview(null)
36
+ try { const res = await fetch('/api/support/import-conversation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ markdown, previewOnly: true }) }); const data = await res.json(); if (!res.ok) { setError(data.error || 'Erreur'); return }; setPreview(data) } catch { setError('Erreur reseau') } finally { setLoading(false) }
37
+ }, [markdown])
38
+
39
+ const doImport = useCallback(async () => {
40
+ if (!markdown) return; setLoading(true); setError('')
41
+ try { const res = await fetch('/api/support/import-conversation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ markdown }) }); const data = await res.json(); if (!res.ok) { setError(data.error || 'Erreur'); return }; setResult(data); setPreview(null) } catch { setError('Erreur reseau') } finally { setLoading(false) }
42
+ }, [markdown])
43
+
44
+ const reset = useCallback(() => { setMarkdown(''); setFileName(''); setPreview(null); setResult(null); setError(''); if (fileRef.current) fileRef.current.value = '' }, [])
45
+
46
+ const S: Record<string, React.CSSProperties> = {
47
+ page: { padding: '20px 30px', maxWidth: 720, margin: '0 auto' },
48
+ dropzone: { border: '2px dashed var(--theme-elevation-300)', borderRadius: 12, padding: 40, textAlign: 'center' as const, cursor: 'pointer', transition: 'all 150ms' },
49
+ dropzoneDragOver: { border: '2px dashed #2563eb', borderRadius: 12, padding: 40, textAlign: 'center' as const, cursor: 'pointer', background: '#eff6ff' },
50
+ dropzoneHasFile: { border: '2px solid #22c55e', borderRadius: 12, padding: 40, textAlign: 'center' as const, cursor: 'pointer', background: '#f0fdf4' },
51
+ btn: { padding: '8px 16px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, cursor: 'pointer', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)', fontWeight: 600 },
52
+ btnPrimary: { padding: '8px 16px', borderRadius: 8, border: 'none', fontSize: 13, cursor: 'pointer', background: '#2563eb', color: '#fff', fontWeight: 600 },
53
+ btnGreen: { padding: '8px 16px', borderRadius: 8, border: 'none', fontSize: 13, cursor: 'pointer', background: '#16a34a', color: '#fff', fontWeight: 600 },
54
+ btnAmber: { padding: '8px 16px', borderRadius: 8, border: 'none', fontSize: 13, cursor: 'pointer', background: '#d97706', color: '#fff', fontWeight: 600 },
55
+ section: { padding: 16, borderRadius: 10, border: '1px solid var(--theme-elevation-150)', marginBottom: 12 },
56
+ infoRow: { display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 13 },
57
+ infoLabel: { color: 'var(--theme-elevation-500)' },
58
+ infoValue: { fontWeight: 600, color: 'var(--theme-text)' },
59
+ msgAdmin: { padding: '8px 12px', borderRadius: 8, background: '#dbeafe', marginBottom: 6 },
60
+ msgClient: { padding: '8px 12px', borderRadius: 8, background: 'var(--theme-elevation-50)', marginBottom: 6 },
61
+ resultSuccess: { padding: 20, borderRadius: 10, border: '2px solid #22c55e', background: '#f0fdf4' },
62
+ resultError: { padding: 16, borderRadius: 10, border: '1px solid #fecaca', background: '#fef2f2', color: '#dc2626', marginBottom: 12 },
63
+ }
64
+
65
+ return (
66
+ <div style={S.page}>
67
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
68
+ <h1 style={{ fontSize: 22, fontWeight: 700, margin: 0 }}>{t('import.title')}</h1>
69
+ {result && <button onClick={reset} style={S.btnPrimary}>{t('import.newImport')}</button>}
70
+ </div>
71
+
72
+ {!result && (
73
+ <div style={isDragOver ? S.dropzoneDragOver : markdown ? S.dropzoneHasFile : S.dropzone} onDragOver={(e) => { e.preventDefault(); setIsDragOver(true) }} onDragLeave={() => setIsDragOver(false)} onDrop={onDrop} onClick={() => fileRef.current?.click()}>
74
+ <div style={{ fontSize: 24, marginBottom: 8 }}>&#8593;</div>
75
+ <p style={{ fontSize: 14, color: 'var(--theme-text)' }}>{markdown ? t('import.dropzoneLoaded') : t('import.dropzoneText')}</p>
76
+ {!markdown && <p style={{ fontSize: 12, color: 'var(--theme-elevation-400)', marginTop: 8 }}>{t('import.acceptedFormats')}</p>}
77
+ {fileName && <p style={{ fontSize: 12, fontWeight: 600, marginTop: 8 }}>{fileName}</p>}
78
+ <input ref={fileRef} type="file" accept=".md,.txt" onChange={onFileChange} style={{ display: 'none' }} />
79
+ </div>
80
+ )}
81
+
82
+ {error && <div style={S.resultError}><strong>{t('common.error')}</strong><p style={{ margin: '4px 0 0' }}>{error}</p></div>}
83
+
84
+ {markdown && !preview && !result && (
85
+ <div style={{ display: 'flex', justifyContent: 'center', marginTop: 16 }}>
86
+ <button onClick={doPreview} disabled={loading} style={S.btnAmber}>{loading ? t('import.analyzing') : t('import.preview')}</button>
87
+ </div>
88
+ )}
89
+
90
+ {preview && (
91
+ <>
92
+ <div style={S.section}>
93
+ <div style={{ fontWeight: 700, marginBottom: 8 }}>{t('import.client')}</div>
94
+ <div style={S.infoRow}><span style={S.infoLabel}>{t('import.name')}</span><span style={S.infoValue}>{preview.client.name}</span></div>
95
+ <div style={S.infoRow}><span style={S.infoLabel}>{t('import.email')}</span><span style={S.infoValue}>{preview.client.email}</span></div>
96
+ <div style={S.infoRow}><span style={S.infoLabel}>{t('import.company')}</span><span style={S.infoValue}>{preview.client.company}</span></div>
97
+ <div style={S.infoRow}><span style={S.infoLabel}>{t('import.parsing')}</span><span style={S.infoValue}>{preview.parseMethod === 'structured' ? t('import.parsingRegex') : t('import.parsingAi')}</span></div>
98
+ </div>
99
+
100
+ <div style={S.section}>
101
+ <div style={{ fontWeight: 700, marginBottom: 8, display: 'flex', justifyContent: 'space-between' }}>
102
+ <span>{preview.subject}</span><span style={{ color: 'var(--theme-elevation-500)', fontSize: 12 }}>{preview.messageCount} {t('import.messages')}</span>
103
+ </div>
104
+ {preview.messages.map((msg, i) => (
105
+ <div key={i} style={msg.from === 'admin' ? S.msgAdmin : S.msgClient}>
106
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 4 }}>
107
+ <span style={{ fontWeight: 600 }}>{msg.from === 'admin' ? '>> ' : '<< '}{msg.name}</span>
108
+ <span style={{ color: 'var(--theme-elevation-400)' }}>{msg.date}</span>
109
+ </div>
110
+ <div style={{ fontSize: 13 }}>{msg.preview}</div>
111
+ </div>
112
+ ))}
113
+ </div>
114
+
115
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 12 }}>
116
+ <button onClick={() => setPreview(null)} style={S.btn}>{t('common.cancel')}</button>
117
+ <button onClick={doImport} disabled={loading} style={S.btnGreen}>{loading ? t('import.importing') : t('import.importButton', { count: String(preview.messageCount) })}</button>
118
+ </div>
119
+ </>
120
+ )}
121
+
122
+ {result && (
123
+ <div style={S.resultSuccess}>
124
+ <div style={{ fontWeight: 700, fontSize: 16, marginBottom: 12, color: '#166534' }}>{t('import.resultTitle')}</div>
125
+ <div style={S.infoRow}><span style={S.infoLabel}>{t('import.resultTicket')}</span><span style={S.infoValue}><a href={`/admin/support/ticket?id=${result.ticketId}`} style={{ color: '#2563eb' }}>{result.ticketNumber}</a></span></div>
126
+ <div style={S.infoRow}><span style={S.infoLabel}>{t('import.resultClient')}</span><span style={S.infoValue}>{result.clientName} ({result.clientEmail}){result.isNewClient && <span style={{ marginLeft: 8, padding: '1px 6px', borderRadius: 4, background: '#dbeafe', color: '#1e40af', fontSize: 10 }}>{t('import.resultNew')}</span>}</span></div>
127
+ <div style={S.infoRow}><span style={S.infoLabel}>{t('import.resultCompany')}</span><span style={S.infoValue}>{result.clientCompany}</span></div>
128
+ <div style={S.infoRow}><span style={S.infoLabel}>{t('import.resultMessages')}</span><span style={S.infoValue}>{t('import.resultImported', { count: String(result.messagesImported) })}</span></div>
129
+ </div>
130
+ )}
131
+ </div>
132
+ )
133
+ }
@@ -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 { ImportConversationClient } from './client'
7
+
8
+ export const ImportConversationView: 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="ImportConversationView">
27
+ <ImportConversationClient />
28
+ </AdminErrorBoundary>
29
+ </DefaultTemplate>
30
+ )
31
+ }
32
+
33
+ export default ImportConversationView
@@ -0,0 +1,151 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useCallback } from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
6
+ import s from '../../styles/Logs.module.scss'
7
+
8
+ type LogType = 'email' | 'auth'
9
+
10
+ interface LogEntry {
11
+ id: number
12
+ status?: string
13
+ action?: string
14
+ senderEmail?: string
15
+ subject?: string
16
+ recipientEmail?: string
17
+ errorMessage?: string
18
+ httpStatus?: number
19
+ processingTimeMs?: number
20
+ email?: string
21
+ success?: boolean
22
+ ip?: string
23
+ userAgent?: string
24
+ createdAt: string
25
+ }
26
+
27
+ function fmtDate(d: string): string {
28
+ return new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
29
+ }
30
+
31
+ export const LogsClient: React.FC = () => {
32
+ const { t } = useTranslation()
33
+ const searchParams = useSearchParams()
34
+ const [logType, setLogType] = useState<LogType>(() => {
35
+ const t = searchParams.get('type')
36
+ return t === 'auth' ? 'auth' : 'email'
37
+ })
38
+ const [logs, setLogs] = useState<LogEntry[]>([])
39
+ const [loading, setLoading] = useState(true)
40
+ const [page, setPage] = useState(1)
41
+ const [hasMore, setHasMore] = useState(false)
42
+ const [totalDocs, setTotalDocs] = useState(0)
43
+ const [purgeResult, setPurgeResult] = useState<string | null>(null)
44
+
45
+ const collection = logType === 'email' ? 'email-logs' : 'auth-logs'
46
+
47
+ const fetchLogs = useCallback(async () => {
48
+ try {
49
+ const res = await fetch(`/api/${collection}?sort=-createdAt&limit=30&page=${page}&depth=0`, { credentials: 'include' })
50
+ if (res.ok) {
51
+ const d = await res.json()
52
+ setLogs(d.docs || [])
53
+ setHasMore(d.hasNextPage)
54
+ setTotalDocs(d.totalDocs)
55
+ }
56
+ } catch { /* silent */ }
57
+ setLoading(false)
58
+ }, [collection, page])
59
+
60
+ useEffect(() => { setLoading(true); setPage(1) }, [logType])
61
+ useEffect(() => { fetchLogs() }, [fetchLogs])
62
+
63
+ const handlePurge = async (days: number) => {
64
+ const label = days === 0 ? 'TOUS les logs' : `les logs de plus de ${days} jours`
65
+ if (!window.confirm(`Supprimer ${label} (${collection}) ? Cette action est irreversible.`)) return
66
+ try {
67
+ const res = await fetch(`/api/support/purge-logs?collection=${collection}&days=${days}`, { method: 'DELETE', credentials: 'include' })
68
+ if (res.ok) { const d = await res.json(); setPurgeResult(`${d.purged} log(s) supprime(s)`); setTimeout(() => setPurgeResult(null), 5000); fetchLogs() }
69
+ } catch { /* silent */ }
70
+ }
71
+
72
+ const S: Record<string, React.CSSProperties> = {
73
+ page: { padding: '20px 30px', maxWidth: 1100, margin: '0 auto' },
74
+ header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
75
+ title: { fontSize: 22, fontWeight: 700, margin: 0, color: 'var(--theme-text)' },
76
+ tabsRow: { display: 'flex', gap: 4, marginBottom: 12 },
77
+ tab: { padding: '6px 12px', borderRadius: 6, border: 'none', background: 'none', cursor: 'pointer', fontSize: 13, color: 'var(--theme-elevation-500)', fontWeight: 500 },
78
+ tabActive: { background: 'var(--theme-elevation-100)', color: 'var(--theme-text)', fontWeight: 700 },
79
+ purgeBtn: { padding: '4px 10px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 11, background: 'var(--theme-elevation-0)', cursor: 'pointer', color: 'var(--theme-text)' },
80
+ table: { width: '100%', borderCollapse: 'collapse' as const, fontSize: 13 },
81
+ th: { textAlign: 'left' as const, padding: '8px', borderBottom: '1px solid var(--theme-elevation-200)', fontSize: 11, color: 'var(--theme-elevation-500)' },
82
+ td: { padding: '8px', borderBottom: '1px solid var(--theme-elevation-100)' },
83
+ badge: { padding: '2px 8px', borderRadius: 4, fontSize: 11, fontWeight: 600 },
84
+ mono: { fontFamily: 'monospace', fontSize: 12 },
85
+ }
86
+
87
+ return (
88
+ <div style={S.page}>
89
+ <div style={S.header}>
90
+ <h1 style={S.title}>{logType === 'email' ? t('logs.title') : t('logs.titleAuth')}</h1>
91
+ <div style={{ display: 'flex', gap: 6 }}>
92
+ <button style={S.purgeBtn} onClick={() => handlePurge(7)}>{t('logs.purge7')}</button>
93
+ <button style={S.purgeBtn} onClick={() => handlePurge(30)}>{t('logs.purge30')}</button>
94
+ <button style={{ ...S.purgeBtn, color: '#dc2626', borderColor: '#dc2626' }} onClick={() => handlePurge(0)}>{t('logs.purgeAll')}</button>
95
+ </div>
96
+ </div>
97
+
98
+ <div style={S.tabsRow}>
99
+ <button style={{ ...S.tab, ...(logType === 'email' ? S.tabActive : {}) }} onClick={() => setLogType('email')}>{t('logs.tabs.email')} ({logType === 'email' ? totalDocs : '...'})</button>
100
+ <button style={{ ...S.tab, ...(logType === 'auth' ? S.tabActive : {}) }} onClick={() => setLogType('auth')}>{t('logs.tabs.auth')} ({logType === 'auth' ? totalDocs : '...'})</button>
101
+ </div>
102
+
103
+ {purgeResult && <div style={{ padding: '8px 14px', borderRadius: 6, background: '#dcfce7', color: '#166534', fontSize: 13, marginBottom: 12 }}>{purgeResult}</div>}
104
+
105
+ {loading ? (
106
+ <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t('common.loading')}</div>
107
+ ) : logs.length === 0 ? (
108
+ <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t('logs.noLogs')}</div>
109
+ ) : logType === 'email' ? (
110
+ <table style={S.table}>
111
+ <thead><tr><th style={S.th}>{t('logs.tableHeaders.date')}</th><th style={S.th}>{t('logs.tableHeaders.status')}</th><th style={S.th}>{t('logs.tableHeaders.recipient')}</th><th style={S.th}>{t('logs.tableHeaders.subject')}</th><th style={S.th}>{t('logs.tableHeaders.action')}</th><th style={{ ...S.th, textAlign: 'right' }}>{t('logs.tableHeaders.time')}</th></tr></thead>
112
+ <tbody>
113
+ {logs.map((log) => (
114
+ <tr key={log.id}>
115
+ <td style={{ ...S.td, ...S.mono }}>{fmtDate(log.createdAt)}</td>
116
+ <td style={S.td}><span style={{ ...S.badge, background: log.status === 'success' ? '#dcfce7' : log.status === 'error' ? '#fef2f2' : '#f3f4f6', color: log.status === 'success' ? '#16a34a' : log.status === 'error' ? '#dc2626' : '#6b7280' }}>{log.status === 'success' ? t('logs.statusSuccess') : log.status === 'error' ? t('logs.statusError') : t('logs.statusIgnored')}</span></td>
117
+ <td style={{ ...S.td, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.recipientEmail}>{log.recipientEmail || '--'}</td>
118
+ <td style={{ ...S.td, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.subject}>{log.subject || '--'}</td>
119
+ <td style={{ ...S.td, fontSize: 12, color: 'var(--theme-elevation-500)' }}>{log.action || '--'}</td>
120
+ <td style={{ ...S.td, textAlign: 'right' }}>{log.processingTimeMs != null ? <span style={{ ...S.mono, color: log.processingTimeMs > 2000 ? '#dc2626' : '#16a34a' }}>{log.processingTimeMs}ms</span> : '--'}</td>
121
+ </tr>
122
+ ))}
123
+ </tbody>
124
+ </table>
125
+ ) : (
126
+ <table style={S.table}>
127
+ <thead><tr><th style={S.th}>{t('logs.tableHeaders.date')}</th><th style={S.th}>{t('logs.tableHeaders.status')}</th><th style={S.th}>{t('logs.tableHeaders.email')}</th><th style={S.th}>{t('logs.tableHeaders.ip')}</th><th style={S.th}>{t('logs.tableHeaders.userAgent')}</th></tr></thead>
128
+ <tbody>
129
+ {logs.map((log) => (
130
+ <tr key={log.id}>
131
+ <td style={{ ...S.td, ...S.mono }}>{fmtDate(log.createdAt)}</td>
132
+ <td style={S.td}><span style={{ ...S.badge, background: log.success ? '#dcfce7' : '#fef2f2', color: log.success ? '#16a34a' : '#dc2626' }}>{log.success ? t('logs.statusSuccess') : t('logs.statusFailed')}</span></td>
133
+ <td style={{ ...S.td, ...S.mono }}>{log.email || '--'}</td>
134
+ <td style={{ ...S.td, ...S.mono }}>{log.ip || '--'}</td>
135
+ <td style={{ ...S.td, fontSize: 11, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{log.userAgent || '--'}</td>
136
+ </tr>
137
+ ))}
138
+ </tbody>
139
+ </table>
140
+ )}
141
+
142
+ {totalDocs > 0 && (
143
+ <div style={{ display: 'flex', justifyContent: 'center', gap: 12, marginTop: 16, alignItems: 'center' }}>
144
+ <button style={S.purgeBtn} onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>{t('common.previous')}</button>
145
+ <span style={{ fontSize: 12, color: 'var(--theme-elevation-500)' }}>{t('common.page')} {page} -- {totalDocs} {t('common.results')}</span>
146
+ <button style={S.purgeBtn} onClick={() => setPage((p) => p + 1)} disabled={!hasMore}>{t('common.next')}</button>
147
+ </div>
148
+ )}
149
+ </div>
150
+ )
151
+ }
@@ -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 { LogsClient } from './client'
7
+
8
+ export const LogsView: 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="LogsView">
24
+ <LogsClient />
25
+ </AdminErrorBoundary>
26
+ </DefaultTemplate>
27
+ )
28
+ }
29
+
30
+ export default LogsView