@consilioweb/payload-support 5.0.0 → 6.0.0
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/README.md +249 -22
- package/dist/components/RichTextEditor/index.js +1 -1
- package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
- package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
- package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
- package/dist/components/TicketConversation/components/QuickActions.js +10 -8
- package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
- package/dist/components/TicketConversation/index.d.ts +10 -0
- package/dist/components/TicketConversation/index.js +25 -7
- package/dist/index.cjs +306 -60
- package/dist/index.d.cts +107 -38
- package/dist/index.d.ts +107 -38
- package/dist/index.js +300 -59
- package/dist/styles/BillingView.module.scss +11 -11
- package/dist/styles/ChatView.module.scss +7 -7
- package/dist/styles/CommandPalette.module.scss +1 -1
- package/dist/styles/CrmView.module.scss +9 -9
- package/dist/styles/EmailTracking.module.scss +6 -6
- package/dist/styles/ImportConversation.module.scss +5 -5
- package/dist/styles/Logs.module.scss +5 -5
- package/dist/styles/NewTicket.module.scss +2 -2
- package/dist/styles/PendingEmails.module.scss +13 -13
- package/dist/styles/SupportDashboard.module.scss +24 -8
- package/dist/styles/TicketDetail.module.scss +20 -20
- package/dist/styles/TicketInbox.module.scss +6 -6
- package/dist/styles/TicketingSettings.module.scss +10 -10
- package/dist/styles/TimeDashboard.module.scss +6 -6
- package/dist/styles/_tokens.scss +3 -1
- package/dist/views/BillingView/client.js +15 -8
- package/dist/views/ChatView/client.js +2 -0
- package/dist/views/CrmView/client.js +2 -0
- package/dist/views/EmailTrackingView/client.js +23 -11
- package/dist/views/ImportConversationView/client.js +1 -0
- package/dist/views/LogsView/client.js +1 -1
- package/dist/views/NewTicketView/client.js +20 -13
- package/dist/views/PendingEmailsView/client.js +9 -3
- package/dist/views/SupportDashboardView/client.js +17 -19
- package/dist/views/TicketDetailView/client.js +23 -11
- package/dist/views/TicketInboxView/client.js +52 -26
- package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/dist/views/TicketingSettingsView/client.js +44 -26
- package/dist/views/TimeDashboardView/client.js +10 -7
- package/dist/views/shared/ErrorBoundary.d.ts +36 -1
- package/dist/views/shared/ErrorBoundary.js +59 -28
- package/dist/views/shared/adminTokens.d.ts +15 -5
- package/dist/views/shared/adminTokens.js +23 -7
- package/dist/views/shared/icons.d.ts +49 -0
- package/dist/views/shared/icons.js +92 -0
- package/dist/views/shared/locales/en.json +15 -1
- package/dist/views/shared/locales/fr.json +15 -1
- package/package.json +6 -2
- package/scripts/uninstall-data.mjs +178 -0
- package/scripts/uninstall.mjs +184 -0
- package/src/collections/Tickets.ts +7 -0
- package/src/collections/TimeEntries.ts +11 -3
- package/src/components/RichTextEditor/index.tsx +1 -1
- package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
- package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
- package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
- package/src/components/TicketConversation/index.tsx +41 -7
- package/src/endpoints/delete-account.ts +191 -47
- package/src/endpoints/export-data.ts +104 -4
- package/src/endpoints/purge-logs.ts +6 -15
- package/src/index.ts +2 -0
- package/src/plugin.ts +51 -1
- package/src/portal/LiveChat.tsx +1 -1
- package/src/portal/auth/profile/page.tsx +3 -2
- package/src/portal/icons.tsx +91 -0
- package/src/styles/BillingView.module.scss +11 -11
- package/src/styles/ChatView.module.scss +7 -7
- package/src/styles/CommandPalette.module.scss +1 -1
- package/src/styles/CrmView.module.scss +9 -9
- package/src/styles/EmailTracking.module.scss +6 -6
- package/src/styles/ImportConversation.module.scss +5 -5
- package/src/styles/Logs.module.scss +5 -5
- package/src/styles/NewTicket.module.scss +2 -2
- package/src/styles/PendingEmails.module.scss +13 -13
- package/src/styles/SupportDashboard.module.scss +24 -8
- package/src/styles/TicketDetail.module.scss +20 -20
- package/src/styles/TicketInbox.module.scss +6 -6
- package/src/styles/TicketingSettings.module.scss +10 -10
- package/src/styles/TimeDashboard.module.scss +6 -6
- package/src/styles/_tokens.scss +3 -1
- package/src/types.ts +20 -0
- package/src/utils/retention.ts +111 -0
- package/src/utils/slugs.ts +15 -0
- package/src/views/BillingView/client.tsx +17 -8
- package/src/views/ChatView/client.tsx +2 -0
- package/src/views/CrmView/client.tsx +2 -0
- package/src/views/EmailTrackingView/client.tsx +18 -7
- package/src/views/ImportConversationView/client.tsx +1 -0
- package/src/views/LogsView/client.tsx +1 -1
- package/src/views/NewTicketView/client.tsx +22 -13
- package/src/views/PendingEmailsView/client.tsx +9 -3
- package/src/views/SupportDashboardView/client.tsx +11 -5
- package/src/views/TicketDetailView/client.tsx +25 -11
- package/src/views/TicketInboxView/client.tsx +47 -7
- package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/src/views/TicketingSettingsView/client.tsx +201 -144
- package/src/views/TimeDashboardView/client.tsx +10 -7
- package/src/views/shared/ErrorBoundary.tsx +95 -31
- package/src/views/shared/adminTokens.ts +28 -11
- package/src/views/shared/icons.tsx +146 -0
- package/src/views/shared/locales/en.json +15 -1
- package/src/views/shared/locales/fr.json +15 -1
- package/src/types/lucide-react.d.ts +0 -42
|
@@ -287,7 +287,6 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
|
|
|
287
287
|
fontSize: '14px',
|
|
288
288
|
lineHeight: 1.6,
|
|
289
289
|
color: '#1f2937',
|
|
290
|
-
outline: 'none',
|
|
291
290
|
overflowY: 'auto',
|
|
292
291
|
}}
|
|
293
292
|
/>
|
|
@@ -300,6 +299,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
|
|
|
300
299
|
type="file"
|
|
301
300
|
accept="image/*"
|
|
302
301
|
multiple
|
|
302
|
+
aria-label="Inserer une image"
|
|
303
303
|
onChange={handleFileChange}
|
|
304
304
|
style={{ display: 'none' }}
|
|
305
305
|
/>
|
|
@@ -76,7 +76,7 @@ export const RewriteDropdown: React.FC<{
|
|
|
76
76
|
onMouseLeave={(e) => { (e.target as HTMLElement).style.background = 'transparent' }}
|
|
77
77
|
>
|
|
78
78
|
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--theme-text, #111)' }}>{style.label}</span>
|
|
79
|
-
<span style={{ fontSize: 10, color: 'var(--theme-elevation-
|
|
79
|
+
<span style={{ fontSize: 10, color: 'var(--theme-elevation-650, #6b7280)' }}>{style.desc}</span>
|
|
80
80
|
</button>
|
|
81
81
|
))}
|
|
82
82
|
</div>
|
|
@@ -39,6 +39,7 @@ export function AISummaryPanel({
|
|
|
39
39
|
return (
|
|
40
40
|
<div style={{ marginBottom: '14px' }}>
|
|
41
41
|
<button
|
|
42
|
+
type="button"
|
|
42
43
|
onClick={() => { setShowAiSummary(!showAiSummary); if (!showAiSummary && !aiSummary) handleAiGenerate() }}
|
|
43
44
|
style={{
|
|
44
45
|
...s.ghostBtn('#17807c', false),
|
|
@@ -61,6 +62,7 @@ export function AISummaryPanel({
|
|
|
61
62
|
</h4>
|
|
62
63
|
<div style={{ display: 'flex', gap: '6px' }}>
|
|
63
64
|
<button
|
|
65
|
+
type="button"
|
|
64
66
|
onClick={handleAiGenerate}
|
|
65
67
|
disabled={aiGenerating}
|
|
66
68
|
style={{ ...s.outlineBtn('#17807c', aiGenerating), fontSize: '11px', padding: '4px 10px' }}
|
|
@@ -69,6 +71,7 @@ export function AISummaryPanel({
|
|
|
69
71
|
</button>
|
|
70
72
|
{aiSummary && !aiGenerating && (
|
|
71
73
|
<button
|
|
74
|
+
type="button"
|
|
72
75
|
onClick={handleAiSave}
|
|
73
76
|
disabled={aiSaving || aiSaved}
|
|
74
77
|
style={{ ...s.btn(aiSaved ? '#16a34a' : '#2563eb', aiSaving), fontSize: '11px', padding: '4px 10px' }}
|
|
@@ -26,12 +26,13 @@ export function MergePanel({
|
|
|
26
26
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
27
27
|
<input
|
|
28
28
|
type="text"
|
|
29
|
+
aria-label="Numero du ticket cible"
|
|
29
30
|
value={mergeTarget}
|
|
30
31
|
onChange={(e) => { setMergeTarget(e.target.value); setMergeTargetInfo(null); setMergeError('') }}
|
|
31
32
|
placeholder="TK-0001"
|
|
32
33
|
style={{ ...s.input, width: '130px' }}
|
|
33
34
|
/>
|
|
34
|
-
<button onClick={handleMergeLookup} style={{ ...s.outlineBtn('#ec4899'), fontSize: '12px', padding: '6px 14px' }}>
|
|
35
|
+
<button type="button" onClick={handleMergeLookup} style={{ ...s.outlineBtn('#ec4899'), fontSize: '12px', padding: '6px 14px' }}>
|
|
35
36
|
Rechercher
|
|
36
37
|
</button>
|
|
37
38
|
{mergeError && <span style={{ fontSize: '12px', color: '#be185d', fontWeight: 600 }}>{mergeError}</span>}
|
|
@@ -41,7 +42,7 @@ export function MergePanel({
|
|
|
41
42
|
<span style={{ fontSize: '13px', fontWeight: 600 }}>
|
|
42
43
|
{mergeTargetInfo.ticketNumber} — {mergeTargetInfo.subject}
|
|
43
44
|
</span>
|
|
44
|
-
<button onClick={handleMerge} disabled={merging} style={{ ...s.btn('#ec4899', merging), color: '#fff', fontSize: '12px', padding: '6px 14px' }}>
|
|
45
|
+
<button type="button" onClick={handleMerge} disabled={merging} style={{ ...s.btn('#ec4899', merging), color: '#fff', fontSize: '12px', padding: '6px 14px' }}>
|
|
45
46
|
{merging ? 'Fusion...' : 'Confirmer la fusion'}
|
|
46
47
|
</button>
|
|
47
48
|
</div>
|
|
@@ -76,6 +77,7 @@ export function ExtMessagePanel({
|
|
|
76
77
|
<div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#eef2ff', border: '1px solid #c7d2fe', marginBottom: '14px' }}>
|
|
77
78
|
<h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '10px', color: '#312e81' }}>Ajouter un message reçu (email, SMS, WhatsApp...)</h4>
|
|
78
79
|
<textarea
|
|
80
|
+
aria-label="Contenu du message recu"
|
|
79
81
|
value={extMsgBody}
|
|
80
82
|
onChange={(e) => setExtMsgBody(e.target.value)}
|
|
81
83
|
placeholder="Coller le contenu du message reçu..."
|
|
@@ -83,17 +85,18 @@ export function ExtMessagePanel({
|
|
|
83
85
|
style={{ ...s.input, width: '100%', resize: 'vertical', marginBottom: '10px' }}
|
|
84
86
|
/>
|
|
85
87
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', marginBottom: '10px' }}>
|
|
86
|
-
<select value={extMsgAuthor} onChange={(e) => setExtMsgAuthor(e.target.value as 'client' | 'admin')} style={{ ...s.input, fontSize: '12px' }}>
|
|
88
|
+
<select aria-label="Auteur du message recu" value={extMsgAuthor} onChange={(e) => setExtMsgAuthor(e.target.value as 'client' | 'admin')} style={{ ...s.input, fontSize: '12px' }}>
|
|
87
89
|
<option value="client">Envoyé par le client</option>
|
|
88
90
|
<option value="admin">Envoyé par le support</option>
|
|
89
91
|
</select>
|
|
90
92
|
<input
|
|
91
93
|
type="datetime-local"
|
|
94
|
+
aria-label="Date du message recu"
|
|
92
95
|
value={extMsgDate}
|
|
93
96
|
onChange={(e) => setExtMsgDate(e.target.value)}
|
|
94
97
|
style={{ ...s.input, fontSize: '12px' }}
|
|
95
98
|
/>
|
|
96
|
-
<input ref={extFileInputRef} type="file" multiple onChange={handleExtFileChange} style={{ display: 'none' }} accept="image/*,.pdf,.doc,.docx,.txt,.zip" />
|
|
99
|
+
<input ref={extFileInputRef} type="file" multiple aria-label="Joindre des fichiers au message recu" onChange={handleExtFileChange} style={{ display: 'none' }} accept="image/*,.pdf,.doc,.docx,.txt,.zip" />
|
|
97
100
|
<button type="button" onClick={() => extFileInputRef.current?.click()} style={{ ...s.ghostBtn('#6b7280'), fontSize: '12px', padding: '6px 12px' }}>
|
|
98
101
|
+ PJ
|
|
99
102
|
</button>
|
|
@@ -108,7 +111,7 @@ export function ExtMessagePanel({
|
|
|
108
111
|
))}
|
|
109
112
|
</div>
|
|
110
113
|
)}
|
|
111
|
-
<button onClick={handleSendExtMsg} disabled={sendingExtMsg || !extMsgBody.trim()} style={s.btn('#1d2b4d', sendingExtMsg || !extMsgBody.trim())}>
|
|
114
|
+
<button type="button" onClick={handleSendExtMsg} disabled={sendingExtMsg || !extMsgBody.trim()} style={s.btn('#1d2b4d', sendingExtMsg || !extMsgBody.trim())}>
|
|
112
115
|
{sendingExtMsg ? 'Ajout...' : 'Ajouter (sans notification)'}
|
|
113
116
|
</button>
|
|
114
117
|
</div>
|
|
@@ -186,6 +189,7 @@ export function ReminderPanel({
|
|
|
186
189
|
)}
|
|
187
190
|
|
|
188
191
|
<button
|
|
192
|
+
type="button"
|
|
189
193
|
onClick={handleSendReminder}
|
|
190
194
|
disabled={reminderSending || !clientEmail}
|
|
191
195
|
style={{ ...s.btn(C.orange, reminderSending || !clientEmail), color: '#fff', fontSize: '13px', padding: '8px 18px' }}
|
|
@@ -207,11 +211,12 @@ export function SnoozePanel({ snoozeSaving, handleSnooze }: SnoozePanelProps) {
|
|
|
207
211
|
<div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#eef8f7', border: '1px solid #b8dcda', marginBottom: '14px' }}>
|
|
208
212
|
<h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '10px', color: '#5b21b6' }}>Snooze — masquer temporairement</h4>
|
|
209
213
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' }}>
|
|
210
|
-
<button onClick={() => handleSnooze(1)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>1 jour</button>
|
|
211
|
-
<button onClick={() => handleSnooze(3)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>3 jours</button>
|
|
212
|
-
<button onClick={() => handleSnooze(7)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>1 semaine</button>
|
|
214
|
+
<button type="button" onClick={() => handleSnooze(1)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>1 jour</button>
|
|
215
|
+
<button type="button" onClick={() => handleSnooze(3)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>3 jours</button>
|
|
216
|
+
<button type="button" onClick={() => handleSnooze(7)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>1 semaine</button>
|
|
213
217
|
<input
|
|
214
218
|
type="datetime-local"
|
|
219
|
+
aria-label="Snoozer jusqu'a une date precise"
|
|
215
220
|
onChange={(e) => { if (e.target.value) handleSnooze(null, e.target.value) }}
|
|
216
221
|
style={{ ...s.input, fontSize: '12px' }}
|
|
217
222
|
/>
|
|
@@ -96,6 +96,7 @@ export function ClientHistory({
|
|
|
96
96
|
{/* Notes */}
|
|
97
97
|
<h5 style={{ fontSize: '12px', fontWeight: 600, color: C.textSecondary, marginBottom: '8px' }}>Notes internes</h5>
|
|
98
98
|
<textarea
|
|
99
|
+
aria-label="Notes internes sur le client"
|
|
99
100
|
value={clientNotes}
|
|
100
101
|
onChange={(e) => onNotesChange(e.target.value)}
|
|
101
102
|
rows={3}
|
|
@@ -109,6 +109,7 @@ export function CodeBlockInserter({ onInsert, className, style }: CodeBlockInser
|
|
|
109
109
|
<input
|
|
110
110
|
ref={inputRef}
|
|
111
111
|
type="text"
|
|
112
|
+
aria-label="Rechercher un langage"
|
|
112
113
|
value={filter}
|
|
113
114
|
onChange={(e) => setFilter(e.target.value)}
|
|
114
115
|
onKeyDown={(e) => {
|
|
@@ -124,7 +125,6 @@ export function CodeBlockInserter({ onInsert, className, style }: CodeBlockInser
|
|
|
124
125
|
border: '1px solid #334155',
|
|
125
126
|
borderRadius: 6,
|
|
126
127
|
color: '#e2e8f0',
|
|
127
|
-
outline: 'none',
|
|
128
128
|
boxSizing: 'border-box',
|
|
129
129
|
}}
|
|
130
130
|
/>
|
|
@@ -48,6 +48,7 @@ export function QuickActions({
|
|
|
48
48
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '16px', alignItems: 'center' }}>
|
|
49
49
|
{statusTransitions.map((a) => (
|
|
50
50
|
<button
|
|
51
|
+
type="button"
|
|
51
52
|
key={a.status}
|
|
52
53
|
onClick={() => onStatusChange(a.status)}
|
|
53
54
|
disabled={statusUpdating}
|
|
@@ -57,22 +58,22 @@ export function QuickActions({
|
|
|
57
58
|
</button>
|
|
58
59
|
))}
|
|
59
60
|
{snoozeUntil && new Date(snoozeUntil) > new Date() && (
|
|
60
|
-
<button onClick={onCancelSnooze} disabled={snoozeSaving} style={{ ...s.ghostBtn('#17807c', snoozeSaving), fontSize: '12px', padding: '5px 10px' }}>
|
|
61
|
+
<button type="button" onClick={onCancelSnooze} disabled={snoozeSaving} style={{ ...s.ghostBtn('#17807c', snoozeSaving), fontSize: '12px', padding: '5px 10px' }}>
|
|
61
62
|
Annuler snooze
|
|
62
63
|
</button>
|
|
63
64
|
)}
|
|
64
65
|
<span style={{ borderLeft: `1px solid ${C.border}`, height: '20px', margin: '0 4px' }} />
|
|
65
|
-
<button onClick={onToggleMerge} style={s.ghostBtn('#be185d')}>Fusionner</button>
|
|
66
|
-
<button onClick={onToggleExtMsg} style={s.ghostBtn('#4f46e5')}>+ Message reçu</button>
|
|
67
|
-
<button onClick={onToggleSnooze} style={s.ghostBtn('#17807c')}>Snooze</button>
|
|
66
|
+
<button type="button" onClick={onToggleMerge} style={s.ghostBtn('#be185d')}>Fusionner</button>
|
|
67
|
+
<button type="button" onClick={onToggleExtMsg} style={s.ghostBtn('#4f46e5')}>+ Message reçu</button>
|
|
68
|
+
<button type="button" onClick={onToggleSnooze} style={s.ghostBtn('#17807c')}>Snooze</button>
|
|
68
69
|
{showReminderButton && (
|
|
69
|
-
<button onClick={onToggleReminder} style={s.ghostBtn('#ea580c')}>{'⏰'} Relancer</button>
|
|
70
|
+
<button type="button" onClick={onToggleReminder} style={s.ghostBtn('#ea580c')}>{'⏰'} Relancer</button>
|
|
70
71
|
)}
|
|
71
|
-
<button onClick={onNextTicket} style={s.ghostBtn('#16a34a')}>Ticket suivant</button>
|
|
72
|
+
<button type="button" onClick={onNextTicket} style={s.ghostBtn('#16a34a')}>Ticket suivant</button>
|
|
72
73
|
{scheduledClose && (
|
|
73
74
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '5px 10px', borderRadius: '6px', backgroundColor: '#fff7ed', border: '1px solid #fed7aa', fontSize: '12px', color: '#9a3412', fontWeight: 600 }}>
|
|
74
75
|
{'⏰'} Fermeture auto le {scheduledClose}
|
|
75
|
-
<button onClick={onCancelScheduledClose} disabled={cancelingClose} style={{ border: 'none', background: 'none', color: '#b91c1c', cursor: cancelingClose ? 'not-allowed' : 'pointer', fontWeight: 700, fontSize: '12px', textDecoration: 'underline', padding: 0, opacity: cancelingClose ? 0.5 : 1 }}>
|
|
76
|
+
<button type="button" onClick={onCancelScheduledClose} disabled={cancelingClose} style={{ border: 'none', background: 'none', color: '#b91c1c', cursor: cancelingClose ? 'not-allowed' : 'pointer', fontWeight: 700, fontSize: '12px', textDecoration: 'underline', padding: 0, opacity: cancelingClose ? 0.5 : 1 }}>
|
|
76
77
|
{cancelingClose ? '...' : 'Annuler'}
|
|
77
78
|
</button>
|
|
78
79
|
</span>
|
|
@@ -90,6 +91,7 @@ export function QuickActions({
|
|
|
90
91
|
Ticket suivant : <strong>{nextTicketInfo}</strong>
|
|
91
92
|
</span>
|
|
92
93
|
<button
|
|
94
|
+
type="button"
|
|
93
95
|
onClick={() => { window.location.href = `/admin/support/ticket?id=${nextTicketId}` }}
|
|
94
96
|
style={{ ...s.btn(C.statusResolved), color: C.white, fontSize: '12px', padding: '5px 14px' }}
|
|
95
97
|
>
|
|
@@ -99,7 +101,7 @@ export function QuickActions({
|
|
|
99
101
|
) : (
|
|
100
102
|
<span style={{ fontSize: '13px', color: '#166534', fontWeight: 700 }}>{nextTicketInfo}</span>
|
|
101
103
|
)}
|
|
102
|
-
<button onClick={onCloseNextTicket} style={{ border: 'none', background: 'none', color: C.textMuted, cursor: 'pointer', fontSize: '16px', fontWeight: 700, marginLeft: '8px' }}>×</button>
|
|
104
|
+
<button type="button" onClick={onCloseNextTicket} style={{ border: 'none', background: 'none', color: C.textMuted, cursor: 'pointer', fontSize: '16px', fontWeight: 700, marginLeft: '8px' }}>×</button>
|
|
103
105
|
</div>
|
|
104
106
|
)}
|
|
105
107
|
</>
|
|
@@ -71,6 +71,7 @@ export function TicketHeader({
|
|
|
71
71
|
)}
|
|
72
72
|
<span style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
|
|
73
73
|
<button
|
|
74
|
+
type="button"
|
|
74
75
|
onClick={() => onCopyLink('admin')}
|
|
75
76
|
title="Copier le lien admin"
|
|
76
77
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '13px', opacity: copiedLink === 'admin' ? 1 : 0.5, padding: '2px 4px' }}
|
|
@@ -78,6 +79,7 @@ export function TicketHeader({
|
|
|
78
79
|
{copiedLink === 'admin' ? '\u2705' : '\uD83D\uDCCB'}
|
|
79
80
|
</button>
|
|
80
81
|
<button
|
|
82
|
+
type="button"
|
|
81
83
|
onClick={() => onCopyLink('client')}
|
|
82
84
|
title="Copier le lien client"
|
|
83
85
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '13px', opacity: copiedLink === 'client' ? 1 : 0.5, padding: '2px 4px' }}
|
|
@@ -34,6 +34,11 @@ export function TimeTrackingPanel({
|
|
|
34
34
|
duration, setDuration, timeDescription, setTimeDescription,
|
|
35
35
|
handleAddTime, addingTime, timeSuccess,
|
|
36
36
|
}: TimeTrackingPanelProps) {
|
|
37
|
+
// The two `<label>`s of the manual entry row exist but were not associated
|
|
38
|
+
// with anything. useId, never a literal: the panel is one per ticket screen
|
|
39
|
+
// but the pattern has to hold if it is ever mounted twice.
|
|
40
|
+
const minutesId = React.useId()
|
|
41
|
+
const workDescriptionId = React.useId()
|
|
37
42
|
const totalH = Math.floor(totalMinutes / 60)
|
|
38
43
|
const totalM = totalMinutes % 60
|
|
39
44
|
|
|
@@ -63,6 +68,7 @@ export function TimeTrackingPanel({
|
|
|
63
68
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
64
69
|
{[-5, -1, 1, 5, 15, 30].map((m) => (
|
|
65
70
|
<button
|
|
71
|
+
type="button"
|
|
66
72
|
key={m}
|
|
67
73
|
onClick={() => setTimerSeconds((p) => Math.max(0, p + m * 60))}
|
|
68
74
|
style={{ width: Math.abs(m) >= 15 ? '32px' : '28px', height: '28px', borderRadius: '6px', border: '1px solid #e2e8f0', background: 'white', cursor: 'pointer', fontSize: Math.abs(m) >= 15 ? '12px' : '14px', fontWeight: 700, color: '#64748b' }}
|
|
@@ -75,28 +81,30 @@ export function TimeTrackingPanel({
|
|
|
75
81
|
)}
|
|
76
82
|
|
|
77
83
|
{!timerRunning && timerSeconds === 0 && (
|
|
78
|
-
<button onClick={() => handleTimerStart(true)} style={{ ...s.btn('#dc2626', false), fontSize: '12px', padding: '6px 16px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
84
|
+
<button type="button" onClick={() => handleTimerStart(true)} style={{ ...s.btn('#dc2626', false), fontSize: '12px', padding: '6px 16px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
79
85
|
▶ Démarrer
|
|
80
86
|
</button>
|
|
81
87
|
)}
|
|
82
88
|
{timerRunning && (
|
|
83
|
-
<button onClick={handleTimerStop} style={{ ...s.btn('#374151', false), fontSize: '12px', padding: '6px 16px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
89
|
+
<button type="button" onClick={handleTimerStop} style={{ ...s.btn('#374151', false), fontSize: '12px', padding: '6px 16px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
84
90
|
⏸ Pause
|
|
85
91
|
</button>
|
|
86
92
|
)}
|
|
87
93
|
{!timerRunning && timerSeconds > 0 && (
|
|
88
94
|
<>
|
|
89
|
-
<button onClick={() => handleTimerStart(false)} style={{ ...s.btn('#dc2626', false), fontSize: '12px', padding: '6px 14px' }}>
|
|
95
|
+
<button type="button" onClick={() => handleTimerStart(false)} style={{ ...s.btn('#dc2626', false), fontSize: '12px', padding: '6px 14px' }}>
|
|
90
96
|
▶ Reprendre
|
|
91
97
|
</button>
|
|
92
98
|
<input
|
|
93
99
|
type="text"
|
|
100
|
+
aria-label="Description du temps en cours"
|
|
94
101
|
value={timerDescription}
|
|
95
102
|
onChange={(e) => setTimerDescription(e.target.value)}
|
|
96
103
|
placeholder="Description..."
|
|
97
104
|
style={{ ...s.input, fontSize: '12px', flex: 1, minWidth: '120px' }}
|
|
98
105
|
/>
|
|
99
106
|
<button
|
|
107
|
+
type="button"
|
|
100
108
|
onClick={handleTimerSave}
|
|
101
109
|
disabled={addingTime || timerSeconds < 60}
|
|
102
110
|
style={{ ...s.btn('#16a34a', addingTime || timerSeconds < 60), fontSize: '12px', padding: '6px 14px' }}
|
|
@@ -104,7 +112,7 @@ export function TimeTrackingPanel({
|
|
|
104
112
|
>
|
|
105
113
|
💾 {Math.round(timerSeconds / 60)} min
|
|
106
114
|
</button>
|
|
107
|
-
<button onClick={handleTimerDiscard} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '14px', color: '#94a3b8', padding: '4px' }} title="Annuler">
|
|
115
|
+
<button type="button" onClick={handleTimerDiscard} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '14px', color: '#94a3b8', padding: '4px' }} title="Annuler">
|
|
108
116
|
✕
|
|
109
117
|
</button>
|
|
110
118
|
</>
|
|
@@ -119,14 +127,14 @@ export function TimeTrackingPanel({
|
|
|
119
127
|
{/* Manual time entry */}
|
|
120
128
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-end', flexWrap: 'wrap', marginBottom: '14px' }}>
|
|
121
129
|
<div>
|
|
122
|
-
<label style={{ display: 'block', fontSize: '11px', color: C.textSecondary, marginBottom: '3px', fontWeight: 600 }}>Min</label>
|
|
123
|
-
<input type="number" min="1" value={duration} onChange={(e) => setDuration(e.target.value)} placeholder="30" style={{ ...s.input, width: '80px' }} />
|
|
130
|
+
<label htmlFor={minutesId} style={{ display: 'block', fontSize: '11px', color: C.textSecondary, marginBottom: '3px', fontWeight: 600 }}>Min</label>
|
|
131
|
+
<input id={minutesId} type="number" min="1" value={duration} onChange={(e) => setDuration(e.target.value)} placeholder="30" style={{ ...s.input, width: '80px' }} />
|
|
124
132
|
</div>
|
|
125
133
|
<div style={{ flex: 1 }}>
|
|
126
|
-
<label style={{ display: 'block', fontSize: '11px', color: C.textSecondary, marginBottom: '3px', fontWeight: 600 }}>Description</label>
|
|
127
|
-
<input type="text" value={timeDescription} onChange={(e) => setTimeDescription(e.target.value)} placeholder="Travail effectué..." style={{ ...s.input, width: '100%' }} />
|
|
134
|
+
<label htmlFor={workDescriptionId} style={{ display: 'block', fontSize: '11px', color: C.textSecondary, marginBottom: '3px', fontWeight: 600 }}>Description</label>
|
|
135
|
+
<input id={workDescriptionId} type="text" value={timeDescription} onChange={(e) => setTimeDescription(e.target.value)} placeholder="Travail effectué..." style={{ ...s.input, width: '100%' }} />
|
|
128
136
|
</div>
|
|
129
|
-
<button onClick={handleAddTime} disabled={addingTime || !duration} style={s.btn(C.amber, addingTime || !duration)}>
|
|
137
|
+
<button type="button" onClick={handleAddTime} disabled={addingTime || !duration} style={s.btn(C.amber, addingTime || !duration)}>
|
|
130
138
|
{addingTime ? '...' : '+ Temps'}
|
|
131
139
|
</button>
|
|
132
140
|
{timeSuccess && <span style={{ fontSize: '12px', color: '#16a34a', fontWeight: 600 }}>{timeSuccess}</span>}
|
|
@@ -24,9 +24,10 @@ import { useDocumentIdFromUrl } from './hooks/useDocumentIdFromUrl'
|
|
|
24
24
|
import { useFeatures } from './hooks/useFeatures'
|
|
25
25
|
import { SkeletonText } from './SkeletonText'
|
|
26
26
|
import { RewriteDropdown } from './RewriteDropdown'
|
|
27
|
+
import { AdminErrorBoundary } from '../../views/shared/ErrorBoundary'
|
|
27
28
|
import '../../styles/theme.css'
|
|
28
29
|
|
|
29
|
-
const
|
|
30
|
+
const TicketConversationInner: React.FC = () => {
|
|
30
31
|
const { id } = useDocumentIdFromUrl()
|
|
31
32
|
const { features } = useFeatures()
|
|
32
33
|
const [messages, setMessages] = useState<Message[]>([])
|
|
@@ -416,6 +417,7 @@ const TicketConversation: React.FC = () => {
|
|
|
416
417
|
<div style={{ flex: 1 }}>
|
|
417
418
|
<input
|
|
418
419
|
type="text"
|
|
420
|
+
aria-label="Rechercher dans la conversation"
|
|
419
421
|
value={searchQuery}
|
|
420
422
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
421
423
|
placeholder="Rechercher..."
|
|
@@ -442,6 +444,7 @@ const TicketConversation: React.FC = () => {
|
|
|
442
444
|
<>
|
|
443
445
|
{showCollapse && hiddenCount > 0 && (
|
|
444
446
|
<button
|
|
447
|
+
type="button"
|
|
445
448
|
onClick={() => setMessagesCollapsed(false)}
|
|
446
449
|
style={{
|
|
447
450
|
background: 'none', border: `1px dashed ${C.border}`, borderRadius: '6px',
|
|
@@ -454,6 +457,7 @@ const TicketConversation: React.FC = () => {
|
|
|
454
457
|
)}
|
|
455
458
|
{!messagesCollapsed && filtered.length > 1 && !isSearching && (
|
|
456
459
|
<button
|
|
460
|
+
type="button"
|
|
457
461
|
onClick={() => setMessagesCollapsed(true)}
|
|
458
462
|
style={{
|
|
459
463
|
background: 'none', border: `1px dashed ${C.border}`, borderRadius: '6px',
|
|
@@ -501,6 +505,7 @@ const TicketConversation: React.FC = () => {
|
|
|
501
505
|
<span style={s.badge(C.emailBg, C.orange)}>Email</span>
|
|
502
506
|
) : (
|
|
503
507
|
<select
|
|
508
|
+
aria-label="Auteur du message"
|
|
504
509
|
value={msg.authorType}
|
|
505
510
|
onChange={(e) => handleToggleAuthor(msg.id, e.target.value === 'admin' ? 'client' : 'admin')}
|
|
506
511
|
disabled={togglingAuthor === msg.id}
|
|
@@ -537,6 +542,7 @@ const TicketConversation: React.FC = () => {
|
|
|
537
542
|
{/* Split button */}
|
|
538
543
|
{!msg.isInternal && !(msg as unknown as { deletedAt?: string }).deletedAt && (
|
|
539
544
|
<button
|
|
545
|
+
type="button"
|
|
540
546
|
onClick={() => handleSplitMessage(msg.id, ticketSubject)}
|
|
541
547
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '10px', color: '#6b7280', padding: 0 }}
|
|
542
548
|
title="Extraire en nouveau ticket"
|
|
@@ -579,16 +585,17 @@ const TicketConversation: React.FC = () => {
|
|
|
579
585
|
{editingMsg === msg.id ? (
|
|
580
586
|
<div style={{ marginTop: '6px' }}>
|
|
581
587
|
<textarea
|
|
588
|
+
aria-label="Editer le message"
|
|
582
589
|
value={editBody}
|
|
583
590
|
onChange={(e) => { setEditBody(e.target.value); setEditHtml(e.target.value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br />')) }}
|
|
584
591
|
rows={4}
|
|
585
592
|
style={{ ...s.input, width: '100%', resize: 'vertical', fontSize: '13px' }}
|
|
586
593
|
/>
|
|
587
594
|
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
|
|
588
|
-
<button onClick={() => handleEditSave(msg.id)} disabled={savingEdit || (!editBody.trim() && !editHtml)} style={{ ...s.btn(C.blue, savingEdit), fontSize: '11px', padding: '5px 12px' }}>
|
|
595
|
+
<button type="button" onClick={() => handleEditSave(msg.id)} disabled={savingEdit || (!editBody.trim() && !editHtml)} style={{ ...s.btn(C.blue, savingEdit), fontSize: '11px', padding: '5px 12px' }}>
|
|
589
596
|
{savingEdit ? '...' : 'Enregistrer'}
|
|
590
597
|
</button>
|
|
591
|
-
<button onClick={handleEditCancel} style={{ ...s.ghostBtn('#6b7280'), fontSize: '11px', padding: '5px 12px' }}>
|
|
598
|
+
<button type="button" onClick={handleEditCancel} style={{ ...s.ghostBtn('#6b7280'), fontSize: '11px', padding: '5px 12px' }}>
|
|
592
599
|
Annuler
|
|
593
600
|
</button>
|
|
594
601
|
</div>
|
|
@@ -775,6 +782,7 @@ const TicketConversation: React.FC = () => {
|
|
|
775
782
|
))}
|
|
776
783
|
{features.ai && (
|
|
777
784
|
<button
|
|
785
|
+
type="button"
|
|
778
786
|
onClick={handleAiSuggestReply}
|
|
779
787
|
disabled={aiReplying || messages.length === 0}
|
|
780
788
|
style={{ ...s.outlineBtn('#17807c', aiReplying || messages.length === 0), fontSize: '11px', padding: '3px 10px', borderRadius: '14px' }}
|
|
@@ -800,7 +808,7 @@ const TicketConversation: React.FC = () => {
|
|
|
800
808
|
/>
|
|
801
809
|
{features.canned && cannedResponses.length > 0 && (
|
|
802
810
|
<>
|
|
803
|
-
<select onChange={handleCannedSelect} style={{ ...s.input, fontSize: '11px', padding: '3px 8px', fontWeight: 600 }}>
|
|
811
|
+
<select aria-label="Reponse rapide" onChange={handleCannedSelect} style={{ ...s.input, fontSize: '11px', padding: '3px 8px', fontWeight: 600 }}>
|
|
804
812
|
<option value="">Réponse rapide...</option>
|
|
805
813
|
{cannedResponses.map((cr) => (
|
|
806
814
|
<option key={cr.id} value={String(cr.id)}>{cr.title}</option>
|
|
@@ -818,6 +826,7 @@ const TicketConversation: React.FC = () => {
|
|
|
818
826
|
{/* Reply textarea (replacing RichTextEditor) */}
|
|
819
827
|
<div style={{ border: `1px solid ${C.border}`, borderRadius: '8px', overflow: 'hidden' }}>
|
|
820
828
|
<textarea
|
|
829
|
+
aria-label="Reponse au client"
|
|
821
830
|
value={replyBody}
|
|
822
831
|
onChange={(e) => {
|
|
823
832
|
const text = e.target.value
|
|
@@ -831,7 +840,6 @@ const TicketConversation: React.FC = () => {
|
|
|
831
840
|
minHeight: '120px',
|
|
832
841
|
padding: '12px',
|
|
833
842
|
border: 'none',
|
|
834
|
-
outline: 'none',
|
|
835
843
|
fontSize: '14px',
|
|
836
844
|
lineHeight: 1.5,
|
|
837
845
|
resize: 'vertical',
|
|
@@ -848,6 +856,7 @@ const TicketConversation: React.FC = () => {
|
|
|
848
856
|
ref={fileInputRef}
|
|
849
857
|
type="file"
|
|
850
858
|
multiple
|
|
859
|
+
aria-label="Joindre des fichiers"
|
|
851
860
|
onChange={handleReplyFileChange}
|
|
852
861
|
style={{ display: 'none' }}
|
|
853
862
|
accept="image/*,.pdf,.doc,.docx,.txt,.zip"
|
|
@@ -866,6 +875,7 @@ const TicketConversation: React.FC = () => {
|
|
|
866
875
|
{'\uD83D\uDCCE'} {file.name}
|
|
867
876
|
<button
|
|
868
877
|
type="button"
|
|
878
|
+
aria-label={`Retirer la piece jointe ${file.name}`}
|
|
869
879
|
onClick={() => setReplyFiles((prev) => prev.filter((_, idx) => idx !== i))}
|
|
870
880
|
style={{ border: 'none', background: 'none', color: '#ef4444', fontWeight: 700, cursor: 'pointer', fontSize: '14px', lineHeight: 1 }}
|
|
871
881
|
>
|
|
@@ -879,6 +889,7 @@ const TicketConversation: React.FC = () => {
|
|
|
879
889
|
{/* Send row */}
|
|
880
890
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap', marginTop: '8px' }}>
|
|
881
891
|
<select
|
|
892
|
+
aria-label="Envoyer en tant que"
|
|
882
893
|
value={sendAsClient ? 'client' : 'admin'}
|
|
883
894
|
onChange={(e) => setSendAsClient(e.target.value === 'client')}
|
|
884
895
|
style={{ ...s.input, fontSize: '12px', padding: '6px 8px', fontWeight: 600 }}
|
|
@@ -896,11 +907,12 @@ const TicketConversation: React.FC = () => {
|
|
|
896
907
|
Envoyer au client
|
|
897
908
|
</label>
|
|
898
909
|
)}
|
|
899
|
-
<button data-action="send-reply" onClick={handleSendReply} disabled={sending || (!replyBody.trim() && !replyHtml)} style={{ ...s.btn(isInternal ? C.amber : notifyClient ? '#16a34a' : C.blue, sending || (!replyBody.trim() && !replyHtml)), fontSize: '13px', padding: '8px 20px', marginLeft: 'auto' }}>
|
|
910
|
+
<button type="button" data-action="send-reply" onClick={handleSendReply} disabled={sending || (!replyBody.trim() && !replyHtml)} style={{ ...s.btn(isInternal ? C.amber : notifyClient ? '#16a34a' : C.blue, sending || (!replyBody.trim() && !replyHtml)), fontSize: '13px', padding: '8px 20px', marginLeft: 'auto' }}>
|
|
900
911
|
{sending ? 'Envoi...' : isInternal ? 'Ajouter note' : notifyClient ? 'Envoyer + Notifier' : 'Sauvegarder'}
|
|
901
912
|
</button>
|
|
902
913
|
{!isInternal && (
|
|
903
914
|
<button
|
|
915
|
+
type="button"
|
|
904
916
|
onClick={() => setShowSchedule(!showSchedule)}
|
|
905
917
|
disabled={!replyBody.trim() && !replyHtml}
|
|
906
918
|
style={{ ...s.outlineBtn('#17807c', !replyBody.trim() && !replyHtml), fontSize: '12px', padding: '8px 12px' }}
|
|
@@ -915,19 +927,21 @@ const TicketConversation: React.FC = () => {
|
|
|
915
927
|
<span style={{ fontSize: '12px', fontWeight: 600, color: '#17807c' }}>Programmer pour :</span>
|
|
916
928
|
<input
|
|
917
929
|
type="datetime-local"
|
|
930
|
+
aria-label="Date et heure d'envoi programme"
|
|
918
931
|
value={scheduleDate}
|
|
919
932
|
onChange={(e) => setScheduleDate(e.target.value)}
|
|
920
933
|
min={new Date().toISOString().slice(0, 16)}
|
|
921
934
|
style={{ ...s.input, fontSize: '12px', width: 'auto' }}
|
|
922
935
|
/>
|
|
923
936
|
<button
|
|
937
|
+
type="button"
|
|
924
938
|
onClick={handleScheduleReply}
|
|
925
939
|
disabled={sending || !scheduleDate || (!replyBody.trim() && !replyHtml)}
|
|
926
940
|
style={{ ...s.btn('#17807c', sending || !scheduleDate || (!replyBody.trim() && !replyHtml)), fontSize: '12px', padding: '6px 14px' }}
|
|
927
941
|
>
|
|
928
942
|
{sending ? '...' : '\u23F0 Programmer'}
|
|
929
943
|
</button>
|
|
930
|
-
<button onClick={() => setShowSchedule(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#94a3b8', fontSize: '14px' }}>{'\u2715'}</button>
|
|
944
|
+
<button type="button" aria-label="Annuler la programmation" onClick={() => setShowSchedule(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#94a3b8', fontSize: '14px' }}>{'\u2715'}</button>
|
|
931
945
|
</div>
|
|
932
946
|
)}
|
|
933
947
|
<div style={{ fontSize: '11px', color: C.textMuted, marginTop: '4px', textAlign: 'right' }}>
|
|
@@ -1088,4 +1102,24 @@ const TicketConversation: React.FC = () => {
|
|
|
1088
1102
|
|
|
1089
1103
|
|
|
1090
1104
|
|
|
1105
|
+
/**
|
|
1106
|
+
* Payload mounts this component straight from the import map as the `ui` field
|
|
1107
|
+
* of the Tickets edit view, so the plugin never gets to render an ancestor for
|
|
1108
|
+
* it: the boundary has to live inside the module itself. Without it a single
|
|
1109
|
+
* render error in any of the twelve sub-components blanks the whole ticket
|
|
1110
|
+
* screen.
|
|
1111
|
+
*
|
|
1112
|
+
* The document id is used as a reset key so that navigating from a ticket that
|
|
1113
|
+
* broke to another one clears the error on its own.
|
|
1114
|
+
*/
|
|
1115
|
+
const TicketConversation: React.FC = () => {
|
|
1116
|
+
const { id } = useDocumentIdFromUrl()
|
|
1117
|
+
|
|
1118
|
+
return (
|
|
1119
|
+
<AdminErrorBoundary viewName="TicketConversation" resetKeys={[id]}>
|
|
1120
|
+
<TicketConversationInner />
|
|
1121
|
+
</AdminErrorBoundary>
|
|
1122
|
+
)
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1091
1125
|
export default TicketConversation
|