@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,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 }}>↑</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
|