@consilioweb/payload-support 0.8.2 → 0.9.5
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/dist/components/RichTextEditor/index.cjs +233 -0
- package/dist/components/RichTextEditor/index.js +232 -0
- package/dist/components/TicketConversation/components/CodeBlock.cjs +24 -7
- package/dist/components/TicketConversation/components/CodeBlock.js +23 -9
- package/dist/index.cjs +19 -1
- package/dist/index.js +19 -1
- package/dist/views/BillingView/client.cjs +260 -103
- package/dist/views/BillingView/client.js +259 -103
- package/dist/views/ChatView/client.cjs +184 -137
- package/dist/views/ChatView/client.js +180 -137
- package/dist/views/CrmView/client.cjs +270 -122
- package/dist/views/CrmView/client.js +266 -122
- package/dist/views/EmailTrackingView/client.cjs +80 -69
- package/dist/views/EmailTrackingView/client.js +80 -70
- package/dist/views/ImportConversationView/client.cjs +127 -94
- package/dist/views/ImportConversationView/client.js +123 -94
- package/dist/views/LogsView/client.cjs +56 -58
- package/dist/views/LogsView/client.js +52 -58
- package/dist/views/NewTicketView/client.cjs +39 -55
- package/dist/views/NewTicketView/client.js +38 -55
- package/dist/views/PendingEmailsView/client.cjs +399 -102
- package/dist/views/PendingEmailsView/client.js +396 -103
- package/dist/views/SupportDashboardView/client.cjs +276 -137
- package/dist/views/SupportDashboardView/client.js +275 -137
- package/dist/views/TicketDetailView/client.cjs +487 -204
- package/dist/views/TicketDetailView/client.js +486 -204
- package/dist/views/TicketInboxView/client.cjs +62 -65
- package/dist/views/TicketInboxView/client.js +62 -66
- package/dist/views/TicketingSettingsView/client.cjs +10 -8
- package/dist/views/TicketingSettingsView/client.js +10 -8
- package/dist/views/TimeDashboardView/client.cjs +70 -59
- package/dist/views/TimeDashboardView/client.js +69 -59
- package/package.json +7 -3
- package/src/components/RichTextEditor/index.tsx +261 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +53 -14
- package/src/plugin.ts +2 -0
- package/src/utils/emailTemplate.ts +37 -0
- package/src/views/BillingView/client.tsx +362 -69
- package/src/views/ChatView/client.tsx +225 -140
- package/src/views/CrmView/client.tsx +447 -189
- package/src/views/EmailTrackingView/client.tsx +111 -71
- package/src/views/ImportConversationView/client.tsx +255 -70
- package/src/views/LogsView/client.tsx +85 -50
- package/src/views/NewTicketView/client.tsx +37 -53
- package/src/views/PendingEmailsView/client.tsx +512 -92
- package/src/views/SupportDashboardView/client.tsx +294 -134
- package/src/views/TicketDetailView/client.tsx +486 -213
- package/src/views/TicketInboxView/client.tsx +52 -61
- package/src/views/TicketingSettingsView/client.tsx +10 -9
- package/src/views/TimeDashboardView/client.tsx +184 -69
|
@@ -4,18 +4,41 @@ import React, { useState, useEffect, useCallback } from 'react'
|
|
|
4
4
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
5
5
|
import s from '../../styles/EmailTracking.module.scss'
|
|
6
6
|
|
|
7
|
-
interface EmailLog {
|
|
8
|
-
|
|
7
|
+
interface EmailLog {
|
|
8
|
+
id: number
|
|
9
|
+
status: 'success' | 'ignored' | 'error'
|
|
10
|
+
action?: string
|
|
11
|
+
senderEmail?: string
|
|
12
|
+
subject?: string
|
|
13
|
+
recipientEmail?: string
|
|
14
|
+
ticketNumber?: string
|
|
15
|
+
errorMessage?: string
|
|
16
|
+
httpStatus?: number
|
|
17
|
+
processingTimeMs?: number
|
|
18
|
+
createdAt: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Stats {
|
|
22
|
+
total: number
|
|
23
|
+
success: number
|
|
24
|
+
errors: number
|
|
25
|
+
ignored: number
|
|
26
|
+
successRate: number
|
|
27
|
+
avgProcessingTime: number
|
|
28
|
+
}
|
|
29
|
+
|
|
9
30
|
type StatusTab = 'all' | 'success' | 'error' | 'ignored'
|
|
10
31
|
type DateRange = 7 | 30 | 90
|
|
11
32
|
|
|
12
|
-
const STATUS_CFG: Record<string, {
|
|
13
|
-
success: {
|
|
14
|
-
error: {
|
|
15
|
-
ignored: {
|
|
33
|
+
const STATUS_CFG: Record<string, { label: string; bg: string; color: string }> = {
|
|
34
|
+
success: { label: 'Succès', bg: '#dcfce7', color: '#16a34a' },
|
|
35
|
+
error: { label: 'Erreur', bg: '#fef2f2', color: '#dc2626' },
|
|
36
|
+
ignored: { label: 'Ignoré', bg: '#f3f4f6', color: '#6b7280' },
|
|
16
37
|
}
|
|
17
38
|
|
|
18
|
-
function fmtDate(d: string): string {
|
|
39
|
+
function fmtDate(d: string): string {
|
|
40
|
+
return new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
41
|
+
}
|
|
19
42
|
|
|
20
43
|
export const EmailTrackingClient: React.FC = () => {
|
|
21
44
|
const { t } = useTranslation()
|
|
@@ -29,94 +52,111 @@ export const EmailTrackingClient: React.FC = () => {
|
|
|
29
52
|
const [page, setPage] = useState(1)
|
|
30
53
|
const [hasMore, setHasMore] = useState(false)
|
|
31
54
|
const [totalDocs, setTotalDocs] = useState(0)
|
|
55
|
+
const LIMIT = 30
|
|
56
|
+
|
|
57
|
+
const fetchStats = useCallback(async () => {
|
|
58
|
+
try { const r = await fetch(`/api/support/email-stats?days=${dateRange}`); if (r.ok) setStats(await r.json()) } catch {}
|
|
59
|
+
}, [dateRange])
|
|
32
60
|
|
|
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
61
|
const fetchLogs = useCallback(async () => {
|
|
35
62
|
const cutoff = new Date(Date.now() - dateRange * 86400000).toISOString()
|
|
36
63
|
const where = [`where[createdAt][greater_than]=${cutoff}`]
|
|
37
64
|
if (tab !== 'all') where.push(`where[status][equals]=${tab}`)
|
|
38
|
-
if (search.trim()) {
|
|
39
|
-
|
|
65
|
+
if (search.trim()) {
|
|
66
|
+
where.push(`where[or][0][recipientEmail][contains]=${encodeURIComponent(search)}`, `where[or][1][subject][contains]=${encodeURIComponent(search)}`)
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const r = await fetch(`/api/email-logs?${where.join('&')}&sort=-createdAt&limit=${LIMIT}&page=${page}&depth=0`)
|
|
70
|
+
if (r.ok) { const d = await r.json(); setLogs(d.docs); setHasMore(d.hasNextPage); setTotalDocs(d.totalDocs) }
|
|
71
|
+
} catch {}
|
|
40
72
|
setLoading(false)
|
|
41
73
|
}, [dateRange, tab, search, page])
|
|
42
74
|
|
|
43
75
|
useEffect(() => { setLoading(true); setPage(1) }, [dateRange, tab, search])
|
|
44
76
|
useEffect(() => { fetchStats(); fetchLogs() }, [fetchStats, fetchLogs])
|
|
45
77
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
}
|
|
78
|
+
const tabsList: Array<{ key: StatusTab; label: string; count?: number }> = [
|
|
79
|
+
{ key: 'all', label: t('emailTracking.tabs.all'), count: stats?.total },
|
|
80
|
+
{ key: 'success', label: t('emailTracking.tabs.success'), count: stats?.success },
|
|
81
|
+
{ key: 'error', label: t('emailTracking.tabs.errors'), count: stats?.errors },
|
|
82
|
+
{ key: 'ignored', label: t('emailTracking.tabs.ignored'), count: stats?.ignored },
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if (loading && !stats) return <div className={s.loading}>{t('common.loading')}</div>
|
|
63
86
|
|
|
64
87
|
return (
|
|
65
|
-
<div
|
|
66
|
-
<h1
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<div
|
|
71
|
-
<div
|
|
72
|
-
<div
|
|
88
|
+
<div className={s.page}>
|
|
89
|
+
<div className={s.header}><h1 className={s.title}>{t('emailTracking.title')}</h1></div>
|
|
90
|
+
|
|
91
|
+
{/* Stats */}
|
|
92
|
+
<div className={s.statGrid}>
|
|
93
|
+
<div className={s.statCard}><div className={s.statLabel}>{t('emailTracking.stats.emailsSent')}</div><div className={s.statValue}>{stats?.total ?? '—'}</div></div>
|
|
94
|
+
<div className={s.statCard}><div className={s.statLabel}>{t('emailTracking.stats.successRate')}</div><div className={s.statValue}>{stats ? `${stats.successRate}` : '—'}<span className={s.statUnit}>%</span></div></div>
|
|
95
|
+
<div className={s.statCard}><div className={s.statLabel}>{t('emailTracking.stats.errors')}</div><div className={s.statValue}>{stats?.errors ?? '—'}</div></div>
|
|
96
|
+
<div className={s.statCard}><div className={s.statLabel}>{t('emailTracking.stats.avgTime')}</div><div className={s.statValue}>{stats ? stats.avgProcessingTime : '—'}<span className={s.statUnit}>ms</span></div></div>
|
|
73
97
|
</div>
|
|
74
98
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
{/* Filters */}
|
|
100
|
+
<div className={s.filters}>
|
|
101
|
+
<div className={s.tabs}>
|
|
102
|
+
{tabsList.map((t) => (
|
|
103
|
+
<button key={t.key} className={`${s.tab} ${tab === t.key ? s.tabActive : ''}`} onClick={() => setTab(t.key)}>
|
|
104
|
+
{t.label} {typeof t.count === 'number' && <span style={{ marginLeft: 4, opacity: 0.6 }}>({t.count})</span>}
|
|
105
|
+
</button>
|
|
79
106
|
))}
|
|
80
107
|
</div>
|
|
81
|
-
<div
|
|
108
|
+
<div className={s.dateRangeGroup}>
|
|
82
109
|
{([7, 30, 90] as DateRange[]).map((v) => (
|
|
83
|
-
<button key={v}
|
|
110
|
+
<button key={v} className={`${s.dateRangeBtn} ${dateRange === v ? s.dateRangeActive : ''}`} onClick={() => setDateRange(v)}>{v}j</button>
|
|
84
111
|
))}
|
|
85
112
|
</div>
|
|
86
|
-
<input type="text" placeholder={t('emailTracking.searchPlaceholder')} value={search} onChange={(e) => setSearch(e.target.value)}
|
|
113
|
+
<input type="text" className={s.searchInput} placeholder={t('emailTracking.searchPlaceholder')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
87
114
|
</div>
|
|
88
115
|
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
</
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
{/* Table */}
|
|
117
|
+
{logs.length === 0 ? (
|
|
118
|
+
<div className={s.empty}>{t('emailTracking.noLogs')}</div>
|
|
119
|
+
) : (
|
|
120
|
+
<table className={s.table}>
|
|
121
|
+
<thead>
|
|
122
|
+
<tr><th>{t('emailTracking.tableHeaders.date')}</th><th>{t('emailTracking.tableHeaders.recipient')}</th><th>{t('emailTracking.tableHeaders.subject')}</th><th>{t('emailTracking.tableHeaders.ticket')}</th><th>{t('emailTracking.tableHeaders.status')}</th><th>{t('emailTracking.tableHeaders.action')}</th><th style={{ textAlign: 'right' }}>{t('emailTracking.tableHeaders.time')}</th></tr>
|
|
123
|
+
</thead>
|
|
124
|
+
<tbody>
|
|
125
|
+
{logs.map((log) => (
|
|
126
|
+
<React.Fragment key={log.id}>
|
|
127
|
+
<tr onClick={() => log.status === 'error' && log.errorMessage ? setExpandedRow(expandedRow === log.id ? null : log.id) : null} style={{ cursor: log.status === 'error' && log.errorMessage ? 'pointer' : 'default' }}>
|
|
128
|
+
<td style={{ fontSize: 12, whiteSpace: 'nowrap' }}>{fmtDate(log.createdAt)}</td>
|
|
129
|
+
<td style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.recipientEmail}>{log.recipientEmail || '—'}</td>
|
|
130
|
+
<td style={{ maxWidth: 250, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.subject}>{log.subject || '—'}</td>
|
|
131
|
+
<td>{log.ticketNumber ? <span className={s.ticketLink}>{log.ticketNumber}</span> : '—'}</td>
|
|
132
|
+
<td><span className={s.statusBadge} style={{ background: STATUS_CFG[log.status]?.bg, color: STATUS_CFG[log.status]?.color }}>{STATUS_CFG[log.status]?.label || log.status}</span></td>
|
|
133
|
+
<td style={{ fontSize: 12 }}>{log.action || '—'}</td>
|
|
134
|
+
<td style={{ textAlign: 'right' }}>
|
|
135
|
+
{log.processingTimeMs != null ? (
|
|
136
|
+
<span className={`${s.processingTime} ${log.processingTimeMs > 2000 ? s.timeSlow : log.processingTimeMs > 500 ? s.timeMedium : s.timeFast}`}>{log.processingTimeMs}ms</span>
|
|
137
|
+
) : '—'}
|
|
138
|
+
{log.status === 'error' && log.errorMessage && (
|
|
139
|
+
<button className={s.expandBtn} onClick={(e) => { e.stopPropagation(); setExpandedRow(expandedRow === log.id ? null : log.id) }}>
|
|
140
|
+
{expandedRow === log.id ? '▴' : '▾'}
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
</td>
|
|
144
|
+
</tr>
|
|
145
|
+
{expandedRow === log.id && log.errorMessage && (
|
|
146
|
+
<tr><td colSpan={7}><div className={s.errorDetail}>Erreur{log.httpStatus ? ` (HTTP ${log.httpStatus})` : ''}: {log.errorMessage}</div></td></tr>
|
|
147
|
+
)}
|
|
148
|
+
</React.Fragment>
|
|
149
|
+
))}
|
|
150
|
+
</tbody>
|
|
151
|
+
</table>
|
|
152
|
+
)}
|
|
114
153
|
|
|
154
|
+
{/* Pagination */}
|
|
115
155
|
{totalDocs > 0 && (
|
|
116
|
-
<div
|
|
117
|
-
<button
|
|
118
|
-
<span style={{ fontSize: 12, color: 'var(--theme-elevation-500)' }}>{t('common.page')} {page}
|
|
119
|
-
<button
|
|
156
|
+
<div className={s.pagination}>
|
|
157
|
+
<button className={s.pageBtn} onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>{t('common.previous')}</button>
|
|
158
|
+
<span style={{ fontSize: 12, color: 'var(--theme-elevation-500)', alignSelf: 'center' }}>{t('common.page')} {page} — {totalDocs} {t('common.results')}</span>
|
|
159
|
+
<button className={s.pageBtn} onClick={() => setPage((p) => p + 1)} disabled={!hasMore}>{t('common.next')}</button>
|
|
120
160
|
</div>
|
|
121
161
|
)}
|
|
122
162
|
</div>
|
|
@@ -2,11 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useCallback, useRef } from 'react'
|
|
4
4
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
5
|
-
import
|
|
5
|
+
import styles from '../../styles/ImportConversation.module.scss'
|
|
6
6
|
|
|
7
|
-
interface PreviewMessage {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
interface PreviewMessage {
|
|
8
|
+
from: 'client' | 'admin'
|
|
9
|
+
name: string
|
|
10
|
+
date: string
|
|
11
|
+
preview: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PreviewData {
|
|
15
|
+
client: { email: string; name: string; company: string }
|
|
16
|
+
subject: string
|
|
17
|
+
messageCount: number
|
|
18
|
+
messages: PreviewMessage[]
|
|
19
|
+
parseMethod: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ImportResult {
|
|
23
|
+
ticketNumber: string
|
|
24
|
+
ticketId: number
|
|
25
|
+
clientEmail: string
|
|
26
|
+
clientName: string
|
|
27
|
+
clientCompany: string
|
|
28
|
+
isNewClient: boolean
|
|
29
|
+
messagesImported: number
|
|
30
|
+
}
|
|
10
31
|
|
|
11
32
|
export function ImportConversationClient() {
|
|
12
33
|
const { t } = useTranslation()
|
|
@@ -20,112 +41,276 @@ export function ImportConversationClient() {
|
|
|
20
41
|
const fileRef = useRef<HTMLInputElement>(null)
|
|
21
42
|
|
|
22
43
|
const handleFile = useCallback((file: File) => {
|
|
23
|
-
if (!file.name.endsWith('.md') && !file.name.endsWith('.txt')) {
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
if (!file.name.endsWith('.md') && !file.name.endsWith('.txt')) {
|
|
45
|
+
setError(t('import.formatError'))
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (file.size > 512_000) {
|
|
49
|
+
setError(t('import.sizeError'))
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setError('')
|
|
54
|
+
setFileName(file.name)
|
|
55
|
+
setResult(null)
|
|
56
|
+
setPreview(null)
|
|
57
|
+
|
|
26
58
|
const reader = new FileReader()
|
|
27
|
-
reader.onload = (e) => {
|
|
59
|
+
reader.onload = (e) => {
|
|
60
|
+
const content = e.target?.result as string
|
|
61
|
+
setMarkdown(content)
|
|
62
|
+
}
|
|
28
63
|
reader.readAsText(file)
|
|
29
64
|
}, [])
|
|
30
65
|
|
|
31
|
-
const onDrop = useCallback(
|
|
32
|
-
|
|
66
|
+
const onDrop = useCallback(
|
|
67
|
+
(e: React.DragEvent) => {
|
|
68
|
+
e.preventDefault()
|
|
69
|
+
setIsDragOver(false)
|
|
70
|
+
const file = e.dataTransfer.files[0]
|
|
71
|
+
if (file) handleFile(file)
|
|
72
|
+
},
|
|
73
|
+
[handleFile],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const onFileChange = useCallback(
|
|
77
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
78
|
+
const file = e.target.files?.[0]
|
|
79
|
+
if (file) handleFile(file)
|
|
80
|
+
},
|
|
81
|
+
[handleFile],
|
|
82
|
+
)
|
|
33
83
|
|
|
34
84
|
const doPreview = useCallback(async () => {
|
|
35
|
-
if (!markdown) return
|
|
36
|
-
|
|
85
|
+
if (!markdown) return
|
|
86
|
+
setLoading(true)
|
|
87
|
+
setError('')
|
|
88
|
+
setPreview(null)
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch('/api/support/import-conversation', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ markdown, previewOnly: true }),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const data = await res.json()
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
setError(data.error || 'Erreur lors de l\'analyse')
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setPreview(data)
|
|
104
|
+
} catch {
|
|
105
|
+
setError('Erreur reseau')
|
|
106
|
+
} finally {
|
|
107
|
+
setLoading(false)
|
|
108
|
+
}
|
|
37
109
|
}, [markdown])
|
|
38
110
|
|
|
39
111
|
const doImport = useCallback(async () => {
|
|
40
|
-
if (!markdown) return
|
|
41
|
-
|
|
112
|
+
if (!markdown) return
|
|
113
|
+
setLoading(true)
|
|
114
|
+
setError('')
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetch('/api/support/import-conversation', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({ markdown }),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const data = await res.json()
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
setError(data.error || 'Erreur lors de l\'import')
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setResult(data)
|
|
130
|
+
setPreview(null)
|
|
131
|
+
} catch {
|
|
132
|
+
setError('Erreur reseau')
|
|
133
|
+
} finally {
|
|
134
|
+
setLoading(false)
|
|
135
|
+
}
|
|
42
136
|
}, [markdown])
|
|
43
137
|
|
|
44
|
-
const reset = useCallback(() => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
}
|
|
138
|
+
const reset = useCallback(() => {
|
|
139
|
+
setMarkdown('')
|
|
140
|
+
setFileName('')
|
|
141
|
+
setPreview(null)
|
|
142
|
+
setResult(null)
|
|
143
|
+
setError('')
|
|
144
|
+
if (fileRef.current) fileRef.current.value = ''
|
|
145
|
+
}, [])
|
|
146
|
+
|
|
147
|
+
const dropzoneClass = isDragOver
|
|
148
|
+
? styles.dropzoneDragOver
|
|
149
|
+
: markdown
|
|
150
|
+
? styles.dropzoneHasFile
|
|
151
|
+
: styles.dropzone
|
|
64
152
|
|
|
65
153
|
return (
|
|
66
|
-
<div
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
154
|
+
<div className={styles.page}>
|
|
155
|
+
{/* Header */}
|
|
156
|
+
<div className={styles.header}>
|
|
157
|
+
<div>
|
|
158
|
+
<h1 className={styles.title}>{t('import.title')}</h1>
|
|
159
|
+
</div>
|
|
160
|
+
{result && (
|
|
161
|
+
<button onClick={reset} className={styles.btnPrimary}>
|
|
162
|
+
{t('import.newImport')}
|
|
163
|
+
</button>
|
|
164
|
+
)}
|
|
70
165
|
</div>
|
|
71
166
|
|
|
167
|
+
{/* Drop zone */}
|
|
72
168
|
{!result && (
|
|
73
|
-
<div
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
169
|
+
<div
|
|
170
|
+
className={dropzoneClass}
|
|
171
|
+
onDragOver={(e) => {
|
|
172
|
+
e.preventDefault()
|
|
173
|
+
setIsDragOver(true)
|
|
174
|
+
}}
|
|
175
|
+
onDragLeave={() => setIsDragOver(false)}
|
|
176
|
+
onDrop={onDrop}
|
|
177
|
+
onClick={() => fileRef.current?.click()}
|
|
178
|
+
>
|
|
179
|
+
<div className={styles.dropzoneIcon}>↑</div>
|
|
180
|
+
<p className={styles.dropzoneText}>
|
|
181
|
+
{markdown
|
|
182
|
+
? t('import.dropzoneLoaded')
|
|
183
|
+
: t('import.dropzoneText')}
|
|
184
|
+
</p>
|
|
185
|
+
{!markdown && (
|
|
186
|
+
<p style={{ fontSize: 12, color: 'var(--theme-elevation-400)', marginTop: 8 }}>
|
|
187
|
+
{t('import.acceptedFormats')}
|
|
188
|
+
</p>
|
|
189
|
+
)}
|
|
190
|
+
{fileName && <p className={styles.fileName}>{fileName}</p>}
|
|
191
|
+
<input
|
|
192
|
+
ref={fileRef}
|
|
193
|
+
type="file"
|
|
194
|
+
accept=".md,.txt"
|
|
195
|
+
onChange={onFileChange}
|
|
196
|
+
style={{ display: 'none' }}
|
|
197
|
+
/>
|
|
79
198
|
</div>
|
|
80
199
|
)}
|
|
81
200
|
|
|
82
|
-
{
|
|
201
|
+
{/* Error */}
|
|
202
|
+
{error && (
|
|
203
|
+
<div className={styles.resultError}>
|
|
204
|
+
<div className={styles.resultTitleError}>
|
|
205
|
+
⚠ Erreur
|
|
206
|
+
</div>
|
|
207
|
+
<p className={styles.errorText}>{error}</p>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
83
210
|
|
|
211
|
+
{/* Actions */}
|
|
84
212
|
{markdown && !preview && !result && (
|
|
85
|
-
<div
|
|
86
|
-
<button onClick={doPreview} disabled={loading}
|
|
213
|
+
<div className={styles.btnRow}>
|
|
214
|
+
<button onClick={doPreview} disabled={loading} className={styles.btnAmber}>
|
|
215
|
+
<span className={styles.btnContent}>
|
|
216
|
+
👁 {loading ? t('import.analyzing') : t('import.preview')}
|
|
217
|
+
</span>
|
|
218
|
+
</button>
|
|
87
219
|
</div>
|
|
88
220
|
)}
|
|
89
221
|
|
|
222
|
+
{/* Preview */}
|
|
90
223
|
{preview && (
|
|
91
224
|
<>
|
|
92
|
-
<div
|
|
93
|
-
<div
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<div
|
|
97
|
-
|
|
225
|
+
<div className={styles.section}>
|
|
226
|
+
<div className={styles.sectionTitle}>
|
|
227
|
+
👤 {t('import.client')}
|
|
228
|
+
</div>
|
|
229
|
+
<div className={styles.infoRow}>
|
|
230
|
+
<span className={styles.infoLabel}>{t('import.name')}</span>
|
|
231
|
+
<span className={styles.infoValue}>{preview.client.name}</span>
|
|
232
|
+
</div>
|
|
233
|
+
<div className={styles.infoRow}>
|
|
234
|
+
<span className={styles.infoLabel}>{t('import.email')}</span>
|
|
235
|
+
<span className={styles.infoValue}>{preview.client.email}</span>
|
|
236
|
+
</div>
|
|
237
|
+
<div className={styles.infoRow}>
|
|
238
|
+
<span className={styles.infoLabel}>{t('import.company')}</span>
|
|
239
|
+
<span className={styles.infoValue}>{preview.client.company}</span>
|
|
240
|
+
</div>
|
|
241
|
+
<div className={styles.infoRow}>
|
|
242
|
+
<span className={styles.infoLabel}>{t('import.parsing')}</span>
|
|
243
|
+
<span className={styles.infoValue}>
|
|
244
|
+
<span className={styles.badgeInfo}>
|
|
245
|
+
{preview.parseMethod === 'structured' ? t('import.parsingRegex') : t('import.parsingAi')}
|
|
246
|
+
</span>
|
|
247
|
+
</span>
|
|
248
|
+
</div>
|
|
98
249
|
</div>
|
|
99
250
|
|
|
100
|
-
<div
|
|
101
|
-
<div
|
|
102
|
-
|
|
251
|
+
<div className={styles.section}>
|
|
252
|
+
<div className={styles.sectionTitle}>
|
|
253
|
+
💬 {preview.subject}
|
|
254
|
+
<span className={styles.sectionTitleRight}>
|
|
255
|
+
{preview.messageCount} {t('import.messages')}
|
|
256
|
+
</span>
|
|
103
257
|
</div>
|
|
104
258
|
{preview.messages.map((msg, i) => (
|
|
105
|
-
<div key={i}
|
|
106
|
-
<div
|
|
107
|
-
<span
|
|
108
|
-
|
|
259
|
+
<div key={i} className={msg.from === 'admin' ? styles.msgAdmin : styles.msgClient}>
|
|
260
|
+
<div className={styles.msgHeader}>
|
|
261
|
+
<span className={styles.msgAuthor}>
|
|
262
|
+
{msg.from === 'admin' ? '>> ' : '<< '}
|
|
263
|
+
{msg.name}
|
|
264
|
+
</span>
|
|
265
|
+
<span className={styles.msgDate}>{msg.date}</span>
|
|
109
266
|
</div>
|
|
110
|
-
<div
|
|
267
|
+
<div className={styles.msgPreview}>{msg.preview}</div>
|
|
111
268
|
</div>
|
|
112
269
|
))}
|
|
113
270
|
</div>
|
|
114
271
|
|
|
115
|
-
<div
|
|
116
|
-
<button onClick={() => setPreview(null)}
|
|
117
|
-
|
|
272
|
+
<div className={styles.btnRow}>
|
|
273
|
+
<button onClick={() => setPreview(null)} className={styles.btnMuted}>
|
|
274
|
+
{t('common.cancel')}
|
|
275
|
+
</button>
|
|
276
|
+
<button onClick={doImport} disabled={loading} className={styles.btnGreen}>
|
|
277
|
+
<span className={styles.btnContent}>
|
|
278
|
+
➜ {loading ? t('import.importing') : t('import.importButton', { count: String(preview.messageCount) })}
|
|
279
|
+
</span>
|
|
280
|
+
</button>
|
|
118
281
|
</div>
|
|
119
282
|
</>
|
|
120
283
|
)}
|
|
121
284
|
|
|
285
|
+
{/* Result */}
|
|
122
286
|
{result && (
|
|
123
|
-
<div
|
|
124
|
-
<div
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<div
|
|
128
|
-
|
|
287
|
+
<div className={styles.resultSuccess}>
|
|
288
|
+
<div className={styles.resultTitleSuccess}>
|
|
289
|
+
✓ {t('import.resultTitle')}
|
|
290
|
+
</div>
|
|
291
|
+
<div className={styles.infoRow}>
|
|
292
|
+
<span className={styles.infoLabel}>{t('import.resultTicket')}</span>
|
|
293
|
+
<span className={styles.infoValue}>
|
|
294
|
+
<a href={`/admin/support/ticket?id=${result.ticketId}`} className={styles.link}>
|
|
295
|
+
{result.ticketNumber}
|
|
296
|
+
</a>
|
|
297
|
+
</span>
|
|
298
|
+
</div>
|
|
299
|
+
<div className={styles.infoRow}>
|
|
300
|
+
<span className={styles.infoLabel}>{t('import.resultClient')}</span>
|
|
301
|
+
<span className={styles.infoValue}>
|
|
302
|
+
{result.clientName} ({result.clientEmail})
|
|
303
|
+
{result.isNewClient && <span className={styles.badgeNew} style={{ marginLeft: 8 }}>{t('import.resultNew')}</span>}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
<div className={styles.infoRow}>
|
|
307
|
+
<span className={styles.infoLabel}>{t('import.resultCompany')}</span>
|
|
308
|
+
<span className={styles.infoValue}>{result.clientCompany}</span>
|
|
309
|
+
</div>
|
|
310
|
+
<div className={styles.infoRow}>
|
|
311
|
+
<span className={styles.infoLabel}>{t('import.resultMessages')}</span>
|
|
312
|
+
<span className={styles.infoValue}>{t('import.resultImported', { count: String(result.messagesImported) })}</span>
|
|
313
|
+
</div>
|
|
129
314
|
</div>
|
|
130
315
|
)}
|
|
131
316
|
</div>
|