@consilioweb/payload-support 0.8.2 → 0.9.4
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 +6 -2
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
|
4
4
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
5
|
-
import
|
|
5
|
+
import styles from '../../styles/ChatView.module.scss'
|
|
6
6
|
|
|
7
7
|
interface ChatSession {
|
|
8
8
|
session: string
|
|
@@ -32,65 +32,58 @@ export const ChatViewClient: React.FC = () => {
|
|
|
32
32
|
const [sending, setSending] = useState(false)
|
|
33
33
|
const [showClosed, setShowClosed] = useState(false)
|
|
34
34
|
const [loading, setLoading] = useState(true)
|
|
35
|
-
const [cannedResponses, setCannedResponses] = useState<{ id: string | number; title: string; body: string }[]>([])
|
|
35
|
+
const [cannedResponses, setCannedResponses] = useState<{ id: string | number; title: string; body: string; category?: string }[]>([])
|
|
36
36
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
37
37
|
const lastFetchRef = useRef<string | null>(null)
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
38
|
+
const sessionsPollInterval = useRef(5000)
|
|
39
|
+
const sessionsPollTimeout = useRef<NodeJS.Timeout>(undefined)
|
|
40
|
+
const messagesPollInterval = useRef(3000)
|
|
41
|
+
const messagesPollTimeout = useRef<NodeJS.Timeout>(undefined)
|
|
41
42
|
|
|
43
|
+
// Fetch sessions list
|
|
44
|
+
const [sessionExpired, setSessionExpired] = useState(false)
|
|
42
45
|
const fetchSessions = useCallback(async () => {
|
|
46
|
+
let hadChanges = false
|
|
43
47
|
try {
|
|
44
48
|
const res = await fetch('/api/support/admin-chat')
|
|
45
49
|
if (res.status === 401 || res.status === 403) { setSessionExpired(true); return }
|
|
46
50
|
if (res.ok) {
|
|
47
51
|
const data = await res.json()
|
|
48
|
-
setSessions(
|
|
52
|
+
setSessions((prev) => {
|
|
53
|
+
const newActive = data.active || []
|
|
54
|
+
const newClosed = data.closed || []
|
|
55
|
+
if (JSON.stringify(prev.active) !== JSON.stringify(newActive) || JSON.stringify(prev.closed) !== JSON.stringify(newClosed)) {
|
|
56
|
+
hadChanges = true
|
|
57
|
+
return { active: newActive, closed: newClosed }
|
|
58
|
+
}
|
|
59
|
+
return prev
|
|
60
|
+
})
|
|
49
61
|
}
|
|
50
62
|
} catch { /* ignore */ }
|
|
51
63
|
setLoading(false)
|
|
64
|
+
if (hadChanges) {
|
|
65
|
+
sessionsPollInterval.current = 5000
|
|
66
|
+
} else {
|
|
67
|
+
sessionsPollInterval.current = Math.min(sessionsPollInterval.current + 2000, 15000)
|
|
68
|
+
}
|
|
52
69
|
}, [])
|
|
53
70
|
|
|
54
|
-
// SSE for session list with polling fallback
|
|
55
71
|
useEffect(() => {
|
|
56
|
-
if (sessionExpired) return
|
|
57
|
-
|
|
58
|
-
// Always fetch once for initial data
|
|
59
72
|
fetchSessions()
|
|
73
|
+
if (sessionExpired) return
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const parsed = JSON.parse(event.data)
|
|
68
|
-
if (parsed.type === 'sessions' && parsed.data) {
|
|
69
|
-
setSessions({ active: parsed.data.active || [], closed: parsed.data.closed || [] })
|
|
70
|
-
setLoading(false)
|
|
71
|
-
}
|
|
72
|
-
} catch { /* ignore parse errors */ }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
es.onerror = () => {
|
|
76
|
-
// SSE failed, fall back to polling
|
|
77
|
-
es.close()
|
|
78
|
-
sessionsESRef.current = null
|
|
79
|
-
const iv = setInterval(fetchSessions, 5000)
|
|
80
|
-
return () => clearInterval(iv)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return () => {
|
|
84
|
-
es.close()
|
|
85
|
-
sessionsESRef.current = null
|
|
86
|
-
}
|
|
75
|
+
const schedulePoll = () => {
|
|
76
|
+
sessionsPollTimeout.current = setTimeout(async () => {
|
|
77
|
+
await fetchSessions()
|
|
78
|
+
schedulePoll()
|
|
79
|
+
}, sessionsPollInterval.current)
|
|
87
80
|
}
|
|
81
|
+
schedulePoll()
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
const iv = setInterval(fetchSessions, 5000)
|
|
91
|
-
return () => clearInterval(iv)
|
|
83
|
+
return () => clearTimeout(sessionsPollTimeout.current)
|
|
92
84
|
}, [fetchSessions, sessionExpired])
|
|
93
85
|
|
|
86
|
+
// Fetch canned responses
|
|
94
87
|
useEffect(() => {
|
|
95
88
|
fetch('/api/canned-responses?sort=sortOrder&limit=50&depth=0', { credentials: 'include' })
|
|
96
89
|
.then((res) => res.ok ? res.json() : null)
|
|
@@ -98,11 +91,12 @@ export const ChatViewClient: React.FC = () => {
|
|
|
98
91
|
.catch(() => {})
|
|
99
92
|
}, [])
|
|
100
93
|
|
|
101
|
-
//
|
|
94
|
+
// Fetch messages for selected session (polling)
|
|
102
95
|
useEffect(() => {
|
|
103
96
|
if (!selectedSession) return
|
|
104
97
|
|
|
105
98
|
const fetchMessages = async () => {
|
|
99
|
+
let hadNewMessages = false
|
|
106
100
|
try {
|
|
107
101
|
const after = lastFetchRef.current || ''
|
|
108
102
|
const url = `/api/support/admin-chat?session=${selectedSession}${after ? `&after=${after}` : ''}`
|
|
@@ -110,73 +104,65 @@ export const ChatViewClient: React.FC = () => {
|
|
|
110
104
|
if (res.ok) {
|
|
111
105
|
const data = await res.json()
|
|
112
106
|
if (!lastFetchRef.current) {
|
|
107
|
+
// Initial load
|
|
113
108
|
setMessages(data.messages || [])
|
|
109
|
+
hadNewMessages = (data.messages?.length || 0) > 0
|
|
114
110
|
} else if (data.messages?.length > 0) {
|
|
115
111
|
setMessages((prev) => {
|
|
116
112
|
const ids = new Set(prev.map((m) => m.id))
|
|
117
113
|
const newMsgs = data.messages.filter((m: ChatMessage) => !ids.has(m.id))
|
|
118
114
|
return newMsgs.length > 0 ? [...prev, ...newMsgs] : prev
|
|
119
115
|
})
|
|
116
|
+
hadNewMessages = true
|
|
120
117
|
}
|
|
121
118
|
if (data.messages?.length > 0) {
|
|
122
119
|
lastFetchRef.current = data.messages[data.messages.length - 1].createdAt
|
|
123
120
|
}
|
|
124
121
|
}
|
|
125
122
|
} catch { /* ignore */ }
|
|
123
|
+
if (hadNewMessages) {
|
|
124
|
+
messagesPollInterval.current = 3000
|
|
125
|
+
} else {
|
|
126
|
+
messagesPollInterval.current = Math.min(messagesPollInterval.current + 1000, 10000)
|
|
127
|
+
}
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
// Always load initial messages via REST
|
|
129
130
|
lastFetchRef.current = null
|
|
131
|
+
messagesPollInterval.current = 3000
|
|
130
132
|
fetchMessages()
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
es.onmessage = (event) => {
|
|
138
|
-
try {
|
|
139
|
-
const parsed = JSON.parse(event.data)
|
|
140
|
-
if (parsed.type === 'messages' && parsed.data?.length > 0) {
|
|
141
|
-
setMessages((prev) => {
|
|
142
|
-
const ids = new Set(prev.map((m) => m.id))
|
|
143
|
-
const newMsgs = parsed.data.filter((m: ChatMessage) => !ids.has(m.id))
|
|
144
|
-
return newMsgs.length > 0 ? [...prev, ...newMsgs] : prev
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
} catch { /* ignore parse errors */ }
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
es.onerror = () => {
|
|
151
|
-
// SSE failed, fall back to polling
|
|
152
|
-
es.close()
|
|
153
|
-
messagesESRef.current = null
|
|
154
|
-
const iv = setInterval(fetchMessages, 3000)
|
|
155
|
-
// Store interval for cleanup — use a local ref
|
|
156
|
-
;(fetchMessages as any)._fallbackIv = iv
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return () => {
|
|
160
|
-
es.close()
|
|
161
|
-
messagesESRef.current = null
|
|
162
|
-
if ((fetchMessages as any)._fallbackIv) clearInterval((fetchMessages as any)._fallbackIv)
|
|
163
|
-
}
|
|
134
|
+
const schedulePoll = () => {
|
|
135
|
+
messagesPollTimeout.current = setTimeout(async () => {
|
|
136
|
+
await fetchMessages()
|
|
137
|
+
schedulePoll()
|
|
138
|
+
}, messagesPollInterval.current)
|
|
164
139
|
}
|
|
140
|
+
schedulePoll()
|
|
165
141
|
|
|
166
|
-
|
|
167
|
-
const iv = setInterval(fetchMessages, 3000)
|
|
168
|
-
return () => clearInterval(iv)
|
|
142
|
+
return () => clearTimeout(messagesPollTimeout.current)
|
|
169
143
|
}, [selectedSession])
|
|
170
144
|
|
|
171
|
-
useEffect(() => {
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
147
|
+
}, [messages])
|
|
172
148
|
|
|
173
149
|
const sendMessage = async (e: React.FormEvent) => {
|
|
174
150
|
e.preventDefault()
|
|
175
151
|
if (!input.trim() || !selectedSession || sending) return
|
|
152
|
+
|
|
176
153
|
setSending(true)
|
|
177
154
|
try {
|
|
178
|
-
const res = await fetch('/api/support/admin-chat', {
|
|
179
|
-
|
|
155
|
+
const res = await fetch('/api/support/admin-chat', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'Content-Type': 'application/json' },
|
|
158
|
+
body: JSON.stringify({ action: 'send', session: selectedSession, message: input.trim() }),
|
|
159
|
+
})
|
|
160
|
+
if (res.ok) {
|
|
161
|
+
const data = await res.json()
|
|
162
|
+
setMessages((prev) => [...prev, data.message])
|
|
163
|
+
lastFetchRef.current = data.message.createdAt
|
|
164
|
+
setInput('')
|
|
165
|
+
}
|
|
180
166
|
} catch { /* ignore */ }
|
|
181
167
|
setSending(false)
|
|
182
168
|
}
|
|
@@ -184,7 +170,11 @@ export const ChatViewClient: React.FC = () => {
|
|
|
184
170
|
const closeSession = async () => {
|
|
185
171
|
if (!selectedSession) return
|
|
186
172
|
try {
|
|
187
|
-
await fetch('/api/support/admin-chat', {
|
|
173
|
+
await fetch('/api/support/admin-chat', {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: { 'Content-Type': 'application/json' },
|
|
176
|
+
body: JSON.stringify({ action: 'close', session: selectedSession }),
|
|
177
|
+
})
|
|
188
178
|
setSelectedSession(null)
|
|
189
179
|
fetchSessions()
|
|
190
180
|
} catch { /* ignore */ }
|
|
@@ -197,92 +187,187 @@ export const ChatViewClient: React.FC = () => {
|
|
|
197
187
|
return client.email || `Client #${client.id}`
|
|
198
188
|
}
|
|
199
189
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
page: { padding: '20px 30px', maxWidth: 1200, margin: '0 auto' },
|
|
204
|
-
container: { display: 'grid', gridTemplateColumns: '320px 1fr', gap: 16, minHeight: 'calc(100vh - 300px)' },
|
|
205
|
-
sidebar: { borderRight: '1px solid var(--theme-elevation-200)' },
|
|
206
|
-
sessionItem: { display: 'block', width: '100%', padding: '10px 14px', border: 'none', background: 'none', cursor: 'pointer', textAlign: 'left' as const, borderBottom: '1px solid var(--theme-elevation-100)', fontSize: 13 },
|
|
207
|
-
sessionActive: { background: 'var(--theme-elevation-50)' },
|
|
208
|
-
chatPanel: { display: 'flex', flexDirection: 'column' as const },
|
|
209
|
-
messagesArea: { flex: 1, overflowY: 'auto' as const, padding: '12px 0' },
|
|
210
|
-
bubble: { maxWidth: '70%', padding: '8px 12px', borderRadius: 10, marginBottom: 8, fontSize: 14 },
|
|
211
|
-
bubbleAgent: { background: '#dbeafe', color: '#1e3a5f', marginLeft: 'auto' },
|
|
212
|
-
bubbleClient: { background: 'var(--theme-elevation-100)', color: 'var(--theme-text)' },
|
|
213
|
-
bubbleSystem: { margin: '4px auto', padding: '4px 12px', fontSize: 11, color: '#6b7280', textAlign: 'center' as const },
|
|
214
|
-
composer: { borderTop: '1px solid var(--theme-elevation-200)', padding: '8px 0' },
|
|
215
|
-
composerInput: { flex: 1, padding: '8px 12px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' },
|
|
216
|
-
sendBtn: { padding: '8px 16px', borderRadius: 8, background: '#2563eb', color: '#fff', border: 'none', fontWeight: 600, cursor: 'pointer', fontSize: 13 },
|
|
217
|
-
tabsRow: { display: 'flex', gap: 4, padding: '8px 14px', borderBottom: '1px solid var(--theme-elevation-200)' },
|
|
218
|
-
tab: { padding: '4px 10px', borderRadius: 6, border: 'none', background: 'none', cursor: 'pointer', fontSize: 12, color: 'var(--theme-elevation-500)' },
|
|
219
|
-
tabActive: { background: 'var(--theme-elevation-100)', fontWeight: 700, color: 'var(--theme-text)' },
|
|
190
|
+
const getClientCompany = (client: ChatSession['client']): string => {
|
|
191
|
+
if (typeof client === 'number') return ''
|
|
192
|
+
return client.company || ''
|
|
220
193
|
}
|
|
221
194
|
|
|
195
|
+
const displayedSessions = showClosed ? sessions.closed : sessions.active
|
|
196
|
+
|
|
222
197
|
return (
|
|
223
|
-
<div
|
|
224
|
-
<div
|
|
225
|
-
<
|
|
226
|
-
|
|
198
|
+
<div className={styles.page}>
|
|
199
|
+
<div className={styles.header}>
|
|
200
|
+
<div>
|
|
201
|
+
<h1 className={styles.title}>{t('chat.title')}</h1>
|
|
202
|
+
<p className={styles.subtitle}>
|
|
203
|
+
{sessions.active.length !== 1 ? t('chat.sessionCountPlural', { count: String(sessions.active.length) }) : t('chat.sessionCount', { count: String(sessions.active.length) })}
|
|
204
|
+
</p>
|
|
205
|
+
</div>
|
|
227
206
|
</div>
|
|
228
207
|
|
|
229
|
-
<div
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
208
|
+
<div className={styles.container}>
|
|
209
|
+
{/* Sessions sidebar */}
|
|
210
|
+
<div className={styles.sidebar}>
|
|
211
|
+
{/* Tabs */}
|
|
212
|
+
<div className={styles.tabs}>
|
|
213
|
+
<button
|
|
214
|
+
onClick={() => setShowClosed(false)}
|
|
215
|
+
className={`${styles.tab} ${!showClosed ? styles.tabActive : ''}`}
|
|
216
|
+
>
|
|
217
|
+
{t('chat.tabs.active')} ({sessions.active.length})
|
|
218
|
+
</button>
|
|
219
|
+
<button
|
|
220
|
+
onClick={() => setShowClosed(true)}
|
|
221
|
+
className={`${styles.tab} ${showClosed ? styles.tabActive : ''}`}
|
|
222
|
+
>
|
|
223
|
+
{t('chat.tabs.closed')} ({sessions.closed.length})
|
|
224
|
+
</button>
|
|
234
225
|
</div>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
226
|
+
|
|
227
|
+
{/* Session list */}
|
|
228
|
+
<div className={styles.sessionList}>
|
|
229
|
+
{loading ? (
|
|
230
|
+
<div className={styles.loadingState}>
|
|
231
|
+
<div className={styles.emptyState}>{t('common.loading')}</div>
|
|
232
|
+
</div>
|
|
233
|
+
) : displayedSessions.length === 0 ? (
|
|
234
|
+
<div className={styles.emptyState}>
|
|
235
|
+
{showClosed ? t('chat.noSessionClosed') : t('chat.noSessionActive')}
|
|
236
|
+
</div>
|
|
237
|
+
) : displayedSessions.map((s) => (
|
|
238
|
+
<button
|
|
239
|
+
key={s.session}
|
|
240
|
+
onClick={() => setSelectedSession(s.session)}
|
|
241
|
+
className={`${styles.sessionItem} ${selectedSession === s.session ? styles.sessionItemActive : ''}`}
|
|
242
|
+
>
|
|
243
|
+
<div className={styles.sessionHeader}>
|
|
244
|
+
<span className={styles.sessionName}>{getClientName(s.client)}</span>
|
|
245
|
+
{s.unreadCount > 0 && (
|
|
246
|
+
<span className={styles.unreadBadge}>{s.unreadCount}</span>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
{getClientCompany(s.client) && (
|
|
250
|
+
<div className={styles.sessionCompany}>{getClientCompany(s.client)}</div>
|
|
251
|
+
)}
|
|
252
|
+
<div className={styles.sessionPreview}>
|
|
253
|
+
{s.lastMessage.startsWith('Note:') ? (
|
|
254
|
+
<span className={styles.sessionRating}>{s.lastMessage.match(/[★☆]+/)?.[0] || '⭐'}</span>
|
|
255
|
+
) : s.lastMessage}
|
|
256
|
+
</div>
|
|
257
|
+
<div className={styles.sessionMeta}>
|
|
258
|
+
{new Date(s.lastMessageAt).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
|
|
259
|
+
{' · '}{s.messageCount} {t('chat.msg')}
|
|
260
|
+
</div>
|
|
261
|
+
</button>
|
|
262
|
+
))}
|
|
248
263
|
</div>
|
|
249
264
|
</div>
|
|
250
265
|
|
|
251
|
-
|
|
266
|
+
{/* Chat area */}
|
|
267
|
+
<div className={styles.chatPanel}>
|
|
252
268
|
{!selectedSession ? (
|
|
253
|
-
<div
|
|
269
|
+
<div className={styles.chatEmpty}>
|
|
270
|
+
{t('chat.selectSession')}
|
|
271
|
+
</div>
|
|
254
272
|
) : (
|
|
255
273
|
<>
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
<
|
|
274
|
+
{/* Chat header */}
|
|
275
|
+
<div className={styles.chatHeader}>
|
|
276
|
+
<span className={styles.chatSessionId}>{selectedSession}</span>
|
|
277
|
+
<button onClick={closeSession} className={styles.closeBtn}>
|
|
278
|
+
{t('chat.closeChat')}
|
|
279
|
+
</button>
|
|
259
280
|
</div>
|
|
260
|
-
|
|
281
|
+
|
|
282
|
+
{/* Messages */}
|
|
283
|
+
<div className={styles.messagesArea}>
|
|
261
284
|
{messages.map((msg) => (
|
|
262
|
-
<div
|
|
285
|
+
<div
|
|
286
|
+
key={msg.id}
|
|
287
|
+
className={`${styles.messageRow} ${
|
|
288
|
+
msg.senderType === 'agent'
|
|
289
|
+
? styles.messageRowAgent
|
|
290
|
+
: msg.senderType === 'system'
|
|
291
|
+
? styles.messageRowSystem
|
|
292
|
+
: styles.messageRowClient
|
|
293
|
+
}`}
|
|
294
|
+
>
|
|
263
295
|
{msg.senderType === 'system' ? (
|
|
264
|
-
|
|
296
|
+
msg.message.startsWith('Note:') || msg.message.startsWith('Commentaire:') ? (
|
|
297
|
+
<div className={styles.bubbleRating}>
|
|
298
|
+
{msg.message.includes('★') && (
|
|
299
|
+
<div className={styles.ratingStars}>
|
|
300
|
+
{msg.message.match(/[★☆]+/)?.[0] || ''}
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
<div className={styles.ratingComment}>
|
|
304
|
+
{msg.message.includes('—')
|
|
305
|
+
? msg.message.split('—').slice(1).join('—').trim()
|
|
306
|
+
: msg.message.replace(/Note:\s*[★☆]+\s*\(\d\/5\)\s*/, '').replace('Commentaire: ', '')}
|
|
307
|
+
</div>
|
|
308
|
+
<div className={styles.ratingMeta}>
|
|
309
|
+
{t('chat.clientReview')} · {new Date(msg.createdAt).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
) : (
|
|
313
|
+
<div className={styles.bubbleSystem}>
|
|
314
|
+
{msg.message}
|
|
315
|
+
</div>
|
|
316
|
+
)
|
|
265
317
|
) : (
|
|
266
|
-
<div
|
|
267
|
-
<div
|
|
268
|
-
|
|
269
|
-
|
|
318
|
+
<div className={`${styles.bubble} ${msg.senderType === 'agent' ? styles.bubbleAgent : styles.bubbleClient}`}>
|
|
319
|
+
<div className={styles.bubbleSender}>
|
|
320
|
+
{msg.senderType === 'agent'
|
|
321
|
+
? (msg.agent ? `${(msg.agent as { firstName?: string }).firstName || t('chat.agent')}` : t('chat.you'))
|
|
322
|
+
: t('chat.clientLabel')}
|
|
323
|
+
</div>
|
|
324
|
+
<div className={styles.bubbleBody}>
|
|
325
|
+
{msg.message}
|
|
326
|
+
</div>
|
|
327
|
+
<div className={styles.bubbleTime}>
|
|
328
|
+
{new Date(msg.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
|
329
|
+
</div>
|
|
270
330
|
</div>
|
|
271
331
|
)}
|
|
272
332
|
</div>
|
|
273
333
|
))}
|
|
274
334
|
<div ref={messagesEndRef} />
|
|
275
335
|
</div>
|
|
276
|
-
|
|
336
|
+
|
|
337
|
+
{/* Reply input */}
|
|
338
|
+
<form onSubmit={sendMessage} className={styles.composer}>
|
|
277
339
|
{cannedResponses.length > 0 && (
|
|
278
|
-
<select
|
|
340
|
+
<select
|
|
341
|
+
onChange={(e) => {
|
|
342
|
+
const cr = cannedResponses.find((c) => String(c.id) === e.target.value)
|
|
343
|
+
if (cr) setInput(cr.body)
|
|
344
|
+
e.target.value = ''
|
|
345
|
+
}}
|
|
346
|
+
className={styles.cannedSelect}
|
|
347
|
+
>
|
|
279
348
|
<option value="">{t('chat.quickReply')}</option>
|
|
280
|
-
{cannedResponses.map((cr) =>
|
|
349
|
+
{cannedResponses.map((cr) => (
|
|
350
|
+
<option key={cr.id} value={String(cr.id)}>{cr.title}</option>
|
|
351
|
+
))}
|
|
281
352
|
</select>
|
|
282
353
|
)}
|
|
283
|
-
<div
|
|
284
|
-
<input
|
|
285
|
-
|
|
354
|
+
<div className={styles.composerRow}>
|
|
355
|
+
<input
|
|
356
|
+
type="text"
|
|
357
|
+
value={input}
|
|
358
|
+
onChange={(e) => setInput(e.target.value)}
|
|
359
|
+
placeholder={t('chat.inputPlaceholder')}
|
|
360
|
+
maxLength={2000}
|
|
361
|
+
className={styles.composerInput}
|
|
362
|
+
autoFocus
|
|
363
|
+
/>
|
|
364
|
+
<button
|
|
365
|
+
type="submit"
|
|
366
|
+
disabled={!input.trim() || sending}
|
|
367
|
+
className={styles.sendBtn}
|
|
368
|
+
>
|
|
369
|
+
{t('chat.sendButton')}
|
|
370
|
+
</button>
|
|
286
371
|
</div>
|
|
287
372
|
</form>
|
|
288
373
|
</>
|