@consilioweb/payload-support 0.15.0 → 1.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 +347 -435
- package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
- package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
- package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
- package/dist/components/TicketConversation/SkeletonText.js +20 -0
- package/dist/components/TicketConversation/components/ActionPanels.cjs +72 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +73 -2
- package/dist/components/TicketConversation/components/QuickActions.cjs +22 -1
- package/dist/components/TicketConversation/components/QuickActions.js +22 -1
- package/dist/components/TicketConversation/constants.cjs +27 -0
- package/dist/components/TicketConversation/constants.js +26 -1
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.cjs +59 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.js +59 -0
- package/dist/components/TicketConversation/index.cjs +42 -149
- package/dist/components/TicketConversation/index.js +37 -144
- package/dist/index.cjs +1835 -741
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1833 -741
- package/dist/views/BillingView/client.cjs +40 -54
- package/dist/views/BillingView/client.js +40 -54
- package/dist/views/ChatView/client.cjs +4 -3
- package/dist/views/ChatView/client.js +4 -3
- package/dist/views/CrmView/client.cjs +7 -7
- package/dist/views/CrmView/client.js +7 -7
- package/dist/views/EmailTrackingView/client.cjs +6 -5
- package/dist/views/EmailTrackingView/client.js +6 -5
- package/dist/views/ImportConversationView/client.cjs +4 -4
- package/dist/views/ImportConversationView/client.js +4 -4
- package/dist/views/LogsView/client.cjs +3 -2
- package/dist/views/LogsView/client.js +3 -2
- package/dist/views/NewTicketView/client.cjs +1 -1
- package/dist/views/NewTicketView/client.js +1 -1
- package/dist/views/PendingEmailsView/client.cjs +7 -6
- package/dist/views/PendingEmailsView/client.js +7 -6
- package/dist/views/SupportDashboardView/client.cjs +19 -22
- package/dist/views/SupportDashboardView/client.js +19 -22
- package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
- package/dist/views/TicketDetailView/NextActionItem.js +34 -0
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
- package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
- package/dist/views/TicketDetailView/client.cjs +59 -190
- package/dist/views/TicketDetailView/client.js +52 -183
- package/dist/views/TicketDetailView/constants.cjs +20 -0
- package/dist/views/TicketDetailView/constants.js +16 -0
- package/dist/views/TicketDetailView/helpers.cjs +23 -0
- package/dist/views/TicketDetailView/helpers.js +20 -0
- package/dist/views/TicketDetailView/types.cjs +2 -0
- package/dist/views/TicketDetailView/types.js +1 -0
- package/dist/views/TicketInboxView/client.cjs +14 -10
- package/dist/views/TicketInboxView/client.js +14 -10
- package/dist/views/TicketingSettingsView/client.cjs +101 -96
- package/dist/views/TicketingSettingsView/client.js +92 -88
- package/dist/views/TimeDashboardView/client.cjs +2 -2
- package/dist/views/TimeDashboardView/client.js +2 -2
- package/dist/views/shared/StatusPill.cjs +23 -12
- package/dist/views/shared/StatusPill.js +23 -12
- package/dist/views/shared/dateLocale.cjs +5 -0
- package/dist/views/shared/dateLocale.js +3 -0
- package/package.json +23 -15
- package/src/__tests__/generateTrackingToken.test.ts +53 -0
- package/src/__tests__/integration/ai-agent.test.ts +46 -0
- package/src/__tests__/integration/auth.test.ts +43 -0
- package/src/__tests__/integration/automation-rules.test.ts +68 -0
- package/src/__tests__/integration/buildTestPayload.ts +52 -0
- package/src/__tests__/integration/channels.test.ts +52 -0
- package/src/__tests__/integration/core-behaviors.test.ts +58 -0
- package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
- package/src/__tests__/integration/digest.test.ts +48 -0
- package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
- package/src/__tests__/integration/isolation.test.ts +81 -0
- package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
- package/src/__tests__/integration/nps.test.ts +37 -0
- package/src/__tests__/integration/sanitization.test.ts +35 -0
- package/src/__tests__/integration/sla-pause.test.ts +39 -0
- package/src/__tests__/integration/smoke.test.ts +26 -0
- package/src/__tests__/integration/snooze.test.ts +68 -0
- package/src/__tests__/integration/teams.test.ts +38 -0
- package/src/__tests__/rateLimiter.test.ts +89 -0
- package/src/__tests__/sanitizeHtml.test.ts +165 -0
- package/src/__tests__/sla.test.ts +258 -0
- package/src/collections/AutomationRules.ts +66 -0
- package/src/collections/NotificationQueue.ts +30 -0
- package/src/collections/SatisfactionSurveys.ts +11 -2
- package/src/collections/SupportClients.ts +58 -1
- package/src/collections/SupportTeam.ts +27 -0
- package/src/collections/TicketActivityLog.ts +9 -4
- package/src/collections/TicketMessages.ts +137 -31
- package/src/collections/Tickets.ts +31 -72
- package/src/collections/index.ts +3 -0
- package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
- package/src/components/TicketConversation/SkeletonText.tsx +23 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +82 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +25 -0
- package/src/components/TicketConversation/constants.ts +29 -0
- package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +51 -0
- package/src/components/TicketConversation/index.tsx +29 -157
- package/src/endpoints/admin-chat-stream.ts +3 -4
- package/src/endpoints/admin-chat.ts +11 -20
- package/src/endpoints/admin-stats.ts +70 -32
- package/src/endpoints/ai-agent.ts +36 -0
- package/src/endpoints/apply-macro.ts +8 -14
- package/src/endpoints/auth-2fa.ts +12 -12
- package/src/endpoints/auto-close.ts +38 -18
- package/src/endpoints/billing.ts +17 -4
- package/src/endpoints/bulk-action.ts +7 -12
- package/src/endpoints/channels.ts +89 -0
- package/src/endpoints/chat-stream.ts +3 -4
- package/src/endpoints/chat.ts +7 -12
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/client-intelligence.ts +4 -4
- package/src/endpoints/delete-account.ts +9 -16
- package/src/endpoints/email-stats.ts +2 -2
- package/src/endpoints/escalate.ts +2 -2
- package/src/endpoints/export-csv.ts +2 -2
- package/src/endpoints/export-data.ts +5 -8
- package/src/endpoints/import-conversation.ts +6 -10
- package/src/endpoints/index.ts +24 -2
- package/src/endpoints/invite-collaborator.ts +30 -15
- package/src/endpoints/invoice.ts +127 -0
- package/src/endpoints/kb-search.ts +2 -2
- package/src/endpoints/login.ts +12 -4
- package/src/endpoints/merge-clients.ts +14 -19
- package/src/endpoints/merge-tickets.ts +11 -18
- package/src/endpoints/oauth-google.ts +86 -39
- package/src/endpoints/pending-emails-process.ts +15 -25
- package/src/endpoints/process-digests.ts +93 -0
- package/src/endpoints/process-scheduled.ts +6 -10
- package/src/endpoints/process-snooze.ts +78 -0
- package/src/endpoints/purge-logs.ts +2 -2
- package/src/endpoints/resend-notification.ts +3 -4
- package/src/endpoints/round-robin-config.ts +3 -4
- package/src/endpoints/satisfaction.ts +21 -14
- package/src/endpoints/search.ts +5 -8
- package/src/endpoints/seed-kb.ts +3 -4
- package/src/endpoints/send-reminder.ts +186 -0
- package/src/endpoints/settings.ts +5 -2
- package/src/endpoints/signature.ts +3 -4
- package/src/endpoints/sla-check.ts +2 -2
- package/src/endpoints/split-ticket.ts +7 -12
- package/src/endpoints/statuses.ts +2 -2
- package/src/endpoints/ticket-feedback.ts +4 -6
- package/src/endpoints/ticket-synthesis.ts +2 -2
- package/src/endpoints/track-open.ts +16 -28
- package/src/endpoints/transfer-ticket.ts +21 -9
- package/src/endpoints/user-prefs.ts +3 -4
- package/src/hooks/automationRules.ts +84 -0
- package/src/hooks/checkSLA.ts +48 -16
- package/src/plugin.ts +6 -0
- package/src/portal/LiveChat.tsx +2 -63
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
- package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
- package/src/portal/auth/tickets/detail/page.tsx +2 -2
- package/src/portal/helpers.ts +46 -0
- package/src/portal/login/page.tsx +3 -8
- package/src/portal/types.ts +15 -0
- package/src/types/lucide-react.d.ts +34 -0
- package/src/types/payload-config.d.ts +10 -0
- package/src/utils/adminNotification.ts +2 -2
- package/src/utils/aiAgent.ts +147 -0
- package/src/utils/auth.ts +5 -4
- package/src/utils/channels.ts +36 -0
- package/src/utils/db.ts +73 -0
- package/src/utils/emailTemplate.ts +8 -1
- package/src/utils/fireWebhooks.ts +4 -6
- package/src/utils/generateTicketSynthesis.ts +7 -12
- package/src/utils/invoicePdf.ts +88 -0
- package/src/utils/notificationQueue.ts +36 -0
- package/src/utils/readSettings.ts +22 -6
- package/src/utils/sanitizeHtml.ts +57 -0
- package/src/utils/slugs.ts +6 -0
- package/src/utils/teamAccess.ts +27 -0
- package/src/utils/ticketAccess.ts +53 -0
- package/src/utils/webhookDispatcher.ts +4 -6
- package/src/views/BillingView/client.tsx +49 -37
- package/src/views/ChatView/client.tsx +4 -3
- package/src/views/CrmView/client.tsx +7 -7
- package/src/views/EmailTrackingView/client.tsx +7 -6
- package/src/views/ImportConversationView/client.tsx +4 -4
- package/src/views/LogsView/client.tsx +3 -2
- package/src/views/NewTicketView/client.tsx +1 -1
- package/src/views/PendingEmailsView/client.tsx +7 -6
- package/src/views/SupportDashboardView/client.tsx +21 -21
- package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
- package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
- package/src/views/TicketDetailView/client.tsx +54 -179
- package/src/views/TicketDetailView/constants.ts +23 -0
- package/src/views/TicketDetailView/helpers.ts +17 -0
- package/src/views/TicketDetailView/types.ts +10 -0
- package/src/views/TicketInboxView/client.tsx +16 -10
- package/src/views/TicketingSettingsView/client.tsx +90 -87
- package/src/views/TimeDashboardView/client.tsx +2 -2
- package/src/views/shared/StatusPill.tsx +24 -12
- package/src/views/shared/dateLocale.ts +5 -0
- package/src/views/shared/locales/en.json +708 -0
- package/src/views/shared/locales/fr.json +708 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import React, { useRef } from 'react'
|
|
4
|
-
import { s } from '../constants'
|
|
4
|
+
import { C, s } from '../constants'
|
|
5
5
|
|
|
6
6
|
// ========== MERGE PANEL ==========
|
|
7
7
|
interface MergePanelProps {
|
|
@@ -115,6 +115,87 @@ export function ExtMessagePanel({
|
|
|
115
115
|
)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// ========== REMINDER PANEL ==========
|
|
119
|
+
interface ReminderPanelProps {
|
|
120
|
+
clientFirstName: string
|
|
121
|
+
clientEmail: string
|
|
122
|
+
reminderHours: number
|
|
123
|
+
setReminderHours: (v: number) => void
|
|
124
|
+
reminderSending: boolean
|
|
125
|
+
reminderError: string
|
|
126
|
+
handleSendReminder: () => void
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const REMINDER_DELAYS = [
|
|
130
|
+
{ hours: 24, label: '24 h' },
|
|
131
|
+
{ hours: 48, label: '48 h' },
|
|
132
|
+
{ hours: 72, label: '72 h' },
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
export function ReminderPanel({
|
|
136
|
+
clientFirstName, clientEmail, reminderHours, setReminderHours,
|
|
137
|
+
reminderSending, reminderError, handleSendReminder,
|
|
138
|
+
}: ReminderPanelProps) {
|
|
139
|
+
const deadline = new Date(Date.now() + reminderHours * 60 * 60 * 1000)
|
|
140
|
+
const deadlineLabel = deadline.toLocaleString('fr-FR', {
|
|
141
|
+
weekday: 'long', day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit',
|
|
142
|
+
})
|
|
143
|
+
const name = clientFirstName?.trim() || 'le client'
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#fff7ed', border: '1px solid #fed7aa', marginBottom: '14px' }}>
|
|
147
|
+
<h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '8px', color: '#9a3412' }}>
|
|
148
|
+
Relancer {name} — fermeture automatique sans réponse
|
|
149
|
+
</h4>
|
|
150
|
+
{clientEmail ? (
|
|
151
|
+
<p style={{ fontSize: '12px', color: '#7c2d12', margin: '0 0 12px 0', lineHeight: 1.5 }}>
|
|
152
|
+
Un email de relance générique sera envoyé à <strong>{clientEmail}</strong>. Sans retour de sa part avant l'échéance, le ticket sera automatiquement résolu (via le cron auto-close).
|
|
153
|
+
</p>
|
|
154
|
+
) : (
|
|
155
|
+
<p style={{ fontSize: '12px', color: '#b91c1c', margin: '0 0 12px 0', fontWeight: 600 }}>
|
|
156
|
+
Ce client n'a pas d'adresse email : impossible d'envoyer une relance.
|
|
157
|
+
</p>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap', marginBottom: '12px' }}>
|
|
161
|
+
<span style={{ fontSize: '12px', fontWeight: 600, color: '#9a3412' }}>Fermer dans :</span>
|
|
162
|
+
{REMINDER_DELAYS.map((d) => {
|
|
163
|
+
const active = reminderHours === d.hours
|
|
164
|
+
return (
|
|
165
|
+
<button
|
|
166
|
+
key={d.hours}
|
|
167
|
+
type="button"
|
|
168
|
+
onClick={() => setReminderHours(d.hours)}
|
|
169
|
+
style={{
|
|
170
|
+
...(active ? s.btn(C.orange) : s.outlineBtn('#ea580c')),
|
|
171
|
+
fontSize: '12px', padding: '6px 14px',
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{d.label}
|
|
175
|
+
</button>
|
|
176
|
+
)
|
|
177
|
+
})}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div style={{ fontSize: '12px', color: '#7c2d12', marginBottom: '12px' }}>
|
|
181
|
+
Fermeture automatique prévue le <strong>{deadlineLabel}</strong> sans réponse.
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{reminderError && (
|
|
185
|
+
<div style={{ fontSize: '12px', color: '#b91c1c', fontWeight: 600, marginBottom: '10px' }}>{reminderError}</div>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
<button
|
|
189
|
+
onClick={handleSendReminder}
|
|
190
|
+
disabled={reminderSending || !clientEmail}
|
|
191
|
+
style={{ ...s.btn(C.orange, reminderSending || !clientEmail), color: '#fff', fontSize: '13px', padding: '8px 18px' }}
|
|
192
|
+
>
|
|
193
|
+
{reminderSending ? 'Envoi...' : 'Confirmer la relance'}
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
118
199
|
// ========== SNOOZE PANEL ==========
|
|
119
200
|
interface SnoozePanelProps {
|
|
120
201
|
snoozeSaving: boolean
|
|
@@ -18,6 +18,13 @@ interface QuickActionsProps {
|
|
|
18
18
|
onToggleExtMsg: () => void
|
|
19
19
|
onToggleSnooze: () => void
|
|
20
20
|
onNextTicket: () => void
|
|
21
|
+
// Reminder (relance + auto-close)
|
|
22
|
+
showReminderButton: boolean
|
|
23
|
+
onToggleReminder: () => void
|
|
24
|
+
autoCloseScheduledAt: string | null
|
|
25
|
+
onCancelScheduledClose: () => void
|
|
26
|
+
cancelingClose: boolean
|
|
27
|
+
reminderSuccess: boolean
|
|
21
28
|
// Next ticket banner
|
|
22
29
|
showNextTicket: boolean
|
|
23
30
|
nextTicketId: number | null
|
|
@@ -30,8 +37,12 @@ export function QuickActions({
|
|
|
30
37
|
snoozeUntil, snoozeSaving, onCancelSnooze,
|
|
31
38
|
showMerge: _showMerge, showExtMsg: _showExtMsg, showSnooze: _showSnooze,
|
|
32
39
|
onToggleMerge, onToggleExtMsg, onToggleSnooze, onNextTicket,
|
|
40
|
+
showReminderButton, onToggleReminder, autoCloseScheduledAt, onCancelScheduledClose, cancelingClose, reminderSuccess,
|
|
33
41
|
showNextTicket, nextTicketId, nextTicketInfo, onCloseNextTicket,
|
|
34
42
|
}: QuickActionsProps) {
|
|
43
|
+
const scheduledClose = autoCloseScheduledAt && new Date(autoCloseScheduledAt) > new Date()
|
|
44
|
+
? new Date(autoCloseScheduledAt).toLocaleString('fr-FR', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })
|
|
45
|
+
: null
|
|
35
46
|
return (
|
|
36
47
|
<>
|
|
37
48
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '16px', alignItems: 'center' }}>
|
|
@@ -54,7 +65,21 @@ export function QuickActions({
|
|
|
54
65
|
<button onClick={onToggleMerge} style={s.ghostBtn('#be185d')}>Fusionner</button>
|
|
55
66
|
<button onClick={onToggleExtMsg} style={s.ghostBtn('#4f46e5')}>+ Message reçu</button>
|
|
56
67
|
<button onClick={onToggleSnooze} style={s.ghostBtn('#7c3aed')}>Snooze</button>
|
|
68
|
+
{showReminderButton && (
|
|
69
|
+
<button onClick={onToggleReminder} style={s.ghostBtn('#ea580c')}>{'⏰'} Relancer</button>
|
|
70
|
+
)}
|
|
57
71
|
<button onClick={onNextTicket} style={s.ghostBtn('#16a34a')}>Ticket suivant</button>
|
|
72
|
+
{scheduledClose && (
|
|
73
|
+
<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
|
+
{'⏰'} 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
|
+
{cancelingClose ? '...' : 'Annuler'}
|
|
77
|
+
</button>
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
{reminderSuccess && (
|
|
81
|
+
<span style={{ ...s.badge('#f0fdf4', '#16a34a'), fontSize: '12px', padding: '5px 10px' }}>Relance envoyée {'✓'}</span>
|
|
82
|
+
)}
|
|
58
83
|
</div>
|
|
59
84
|
|
|
60
85
|
{showNextTicket && (
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import type React from 'react'
|
|
2
2
|
|
|
3
|
+
// ── Rewrite style dropdown options ──────────────────────────────────────
|
|
4
|
+
export const REWRITE_STYLES = [
|
|
5
|
+
{ id: 'auto', label: '✏️ Auto', desc: 'Garde le ton actuel' },
|
|
6
|
+
{ id: 'tutoyer', label: '👋 Tutoyer', desc: 'Passe en tu' },
|
|
7
|
+
{ id: 'vouvoyer', label: '🎩 Vouvoyer', desc: 'Passe en vous' },
|
|
8
|
+
{ id: 'formel', label: '💼 Formel', desc: 'Ton professionnel' },
|
|
9
|
+
{ id: 'amical', label: '😊 Amical', desc: 'Ton chaleureux' },
|
|
10
|
+
{ id: 'court', label: '⚡ Court', desc: 'Version concise' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
// Inline layout styles
|
|
14
|
+
export const layoutStyles = {
|
|
15
|
+
root: { padding: '12px 0' } as React.CSSProperties,
|
|
16
|
+
twoColumns: {
|
|
17
|
+
display: 'grid',
|
|
18
|
+
gridTemplateColumns: '1fr 320px',
|
|
19
|
+
gap: '16px',
|
|
20
|
+
alignItems: 'start',
|
|
21
|
+
} as React.CSSProperties,
|
|
22
|
+
mainColumn: { minWidth: 0 } as React.CSSProperties,
|
|
23
|
+
sideColumn: {
|
|
24
|
+
display: 'flex',
|
|
25
|
+
flexDirection: 'column' as const,
|
|
26
|
+
gap: '12px',
|
|
27
|
+
position: 'sticky' as const,
|
|
28
|
+
top: '80px',
|
|
29
|
+
} as React.CSSProperties,
|
|
30
|
+
}
|
|
31
|
+
|
|
3
32
|
export const statusLabels: Record<string, { label: string; bg: string; color: string }> = {
|
|
4
33
|
open: { label: 'Ouvert', bg: '#dbeafe', color: '#1e40af' },
|
|
5
34
|
waiting_client: { label: 'Attente', bg: '#fef3c7', color: '#92400e' },
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
// Replacement for @payloadcms/ui's useDocumentInfo — avoids importing
|
|
4
|
+
// @payloadcms/ui (which has chunk splits that break webpack resolution
|
|
5
|
+
// when this bundle is consumed from node_modules).
|
|
6
|
+
// Extracts the document id from the admin URL pattern:
|
|
7
|
+
// /admin/collections/:slug/:id or /admin/globals/:slug
|
|
8
|
+
export function useDocumentIdFromUrl(): { id: string | number | undefined } {
|
|
9
|
+
const [id, setId] = useState<string | number | undefined>(undefined)
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const match = window.location.pathname.match(
|
|
12
|
+
/\/admin\/collections\/[^/]+\/([^/?#]+)/
|
|
13
|
+
)
|
|
14
|
+
if (match && match[1] !== 'create') {
|
|
15
|
+
const raw = match[1]
|
|
16
|
+
const num = Number(raw)
|
|
17
|
+
setId(Number.isFinite(num) && String(num) === raw ? num : raw)
|
|
18
|
+
}
|
|
19
|
+
}, [])
|
|
20
|
+
return { id }
|
|
21
|
+
}
|
|
@@ -23,6 +23,13 @@ export function useTicketActions(
|
|
|
23
23
|
const [showSnooze, setShowSnooze] = useState(false)
|
|
24
24
|
const [snoozeUntil, setSnoozeUntil] = useState<string | null>(null)
|
|
25
25
|
const [snoozeSaving, setSnoozeSaving] = useState(false)
|
|
26
|
+
// Reminder (relance + auto-close arming)
|
|
27
|
+
const [showReminder, setShowReminder] = useState(false)
|
|
28
|
+
const [reminderHours, setReminderHours] = useState(24)
|
|
29
|
+
const [reminderSending, setReminderSending] = useState(false)
|
|
30
|
+
const [reminderError, setReminderError] = useState('')
|
|
31
|
+
const [reminderSuccess, setReminderSuccess] = useState(false)
|
|
32
|
+
const [cancelingClose, setCancelingClose] = useState(false)
|
|
26
33
|
// Next ticket
|
|
27
34
|
const [showNextTicket, setShowNextTicket] = useState(false)
|
|
28
35
|
const [nextTicketId, setNextTicketId] = useState<number | null>(null)
|
|
@@ -178,6 +185,49 @@ export function useTicketActions(
|
|
|
178
185
|
}
|
|
179
186
|
}
|
|
180
187
|
|
|
188
|
+
const handleSendReminder = async () => {
|
|
189
|
+
if (!id) return
|
|
190
|
+
setReminderSending(true)
|
|
191
|
+
setReminderError('')
|
|
192
|
+
try {
|
|
193
|
+
const res = await fetch('/api/support/send-reminder', {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
credentials: 'include',
|
|
197
|
+
body: JSON.stringify({ ticketId: id, hours: reminderHours }),
|
|
198
|
+
})
|
|
199
|
+
const d = await res.json().catch(() => ({}))
|
|
200
|
+
if (res.ok) {
|
|
201
|
+
setShowReminder(false)
|
|
202
|
+
setReminderSuccess(true)
|
|
203
|
+
setTimeout(() => setReminderSuccess(false), 5000)
|
|
204
|
+
fetchAll()
|
|
205
|
+
} else {
|
|
206
|
+
setReminderError(d.error || 'Erreur lors de la relance')
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
setReminderError('Erreur réseau')
|
|
210
|
+
} finally {
|
|
211
|
+
setReminderSending(false)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const handleCancelScheduledClose = async () => {
|
|
216
|
+
if (!id) return
|
|
217
|
+
setCancelingClose(true)
|
|
218
|
+
try {
|
|
219
|
+
await fetch(`/api/tickets/${id}`, {
|
|
220
|
+
method: 'PATCH',
|
|
221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
credentials: 'include',
|
|
223
|
+
body: JSON.stringify({ autoCloseScheduledAt: null, autoCloseRemindedAt: null }),
|
|
224
|
+
})
|
|
225
|
+
fetchAll()
|
|
226
|
+
} catch { /* ignore */ } finally {
|
|
227
|
+
setCancelingClose(false)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
181
231
|
const handleNextTicket = async () => {
|
|
182
232
|
try {
|
|
183
233
|
const res = await fetch('/api/tickets?where[status][equals]=open&sort=updatedAt&limit=1&depth=0', { credentials: 'include' })
|
|
@@ -200,6 +250,7 @@ export function useTicketActions(
|
|
|
200
250
|
showMerge, setShowMerge, mergeTarget, setMergeTarget, mergeTargetInfo, setMergeTargetInfo, mergeError, setMergeError, merging, handleMergeLookup, handleMerge,
|
|
201
251
|
showExtMsg, setShowExtMsg, extMsgBody, setExtMsgBody, extMsgAuthor, setExtMsgAuthor, extMsgDate, setExtMsgDate, extMsgFiles, setExtMsgFiles, sendingExtMsg, handleExtFileChange, handleSendExtMsg,
|
|
202
252
|
showSnooze, setShowSnooze, snoozeUntil, setSnoozeUntil, snoozeSaving, handleSnooze,
|
|
253
|
+
showReminder, setShowReminder, reminderHours, setReminderHours, reminderSending, reminderError, setReminderError, reminderSuccess, handleSendReminder, cancelingClose, handleCancelScheduledClose,
|
|
203
254
|
showNextTicket, setShowNextTicket, nextTicketId, nextTicketInfo, handleNextTicket,
|
|
204
255
|
}
|
|
205
256
|
}
|
|
@@ -1,29 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
|
4
|
-
|
|
5
|
-
// Replacement for @payloadcms/ui's useDocumentInfo — avoids importing
|
|
6
|
-
// @payloadcms/ui (which has chunk splits that break webpack resolution
|
|
7
|
-
// when this bundle is consumed from node_modules).
|
|
8
|
-
// Extracts the document id from the admin URL pattern:
|
|
9
|
-
// /admin/collections/:slug/:id or /admin/globals/:slug
|
|
10
|
-
function useDocumentIdFromUrl(): { id: string | number | undefined } {
|
|
11
|
-
const [id, setId] = useState<string | number | undefined>(undefined)
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const match = window.location.pathname.match(
|
|
14
|
-
/\/admin\/collections\/[^/]+\/([^/?#]+)/
|
|
15
|
-
)
|
|
16
|
-
if (match && match[1] !== 'create') {
|
|
17
|
-
const raw = match[1]
|
|
18
|
-
const num = Number(raw)
|
|
19
|
-
setId(Number.isFinite(num) && String(num) === raw ? num : raw)
|
|
20
|
-
}
|
|
21
|
-
}, [])
|
|
22
|
-
return { id }
|
|
23
|
-
}
|
|
24
4
|
import type { Message, TimeEntry, ClientInfo, CannedResponse, ActivityEntry, SatisfactionSurvey } from './types'
|
|
25
5
|
import type { RichTextEditorHandle } from './context'
|
|
26
|
-
import { C, s } from './constants'
|
|
6
|
+
import { C, s, layoutStyles } from './constants'
|
|
27
7
|
import { getDateLabel, formatMessageDate } from './utils'
|
|
28
8
|
import { CodeBlockRenderer, CodeBlockRendererHtml } from './components/CodeBlock'
|
|
29
9
|
import { CodeBlockInserter } from './components/CodeBlockInserter'
|
|
@@ -31,7 +11,7 @@ import { TicketHeader } from './components/TicketHeader'
|
|
|
31
11
|
import { ClientBar } from './components/ClientBar'
|
|
32
12
|
import { QuickActions } from './components/QuickActions'
|
|
33
13
|
import { AISummaryPanel } from './components/AISummaryPanel'
|
|
34
|
-
import { MergePanel, ExtMessagePanel, SnoozePanel } from './components/ActionPanels'
|
|
14
|
+
import { MergePanel, ExtMessagePanel, SnoozePanel, ReminderPanel } from './components/ActionPanels'
|
|
35
15
|
import { ActivityLog } from './components/ActivityLog'
|
|
36
16
|
import { ClientHistory } from './components/ClientHistory'
|
|
37
17
|
import { TimeTrackingPanel } from './components/TimeTrackingPanel'
|
|
@@ -40,140 +20,12 @@ import { useMessageActions } from './hooks/useMessageActions'
|
|
|
40
20
|
import { useTicketActions } from './hooks/useTicketActions'
|
|
41
21
|
import { useReply } from './hooks/useReply'
|
|
42
22
|
import { useAI } from './hooks/useAI'
|
|
23
|
+
import { useDocumentIdFromUrl } from './hooks/useDocumentIdFromUrl'
|
|
43
24
|
import { getFeatures, type TicketingFeatures } from './config'
|
|
25
|
+
import { SkeletonText } from './SkeletonText'
|
|
26
|
+
import { RewriteDropdown } from './RewriteDropdown'
|
|
44
27
|
import '../../styles/theme.css'
|
|
45
28
|
|
|
46
|
-
// Inline skeleton replacement (no external dependency)
|
|
47
|
-
function SkeletonText({ lines = 3 }: { lines?: number }) {
|
|
48
|
-
return (
|
|
49
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '12px 0' }}>
|
|
50
|
-
{Array.from({ length: lines }).map((_, i) => (
|
|
51
|
-
<div
|
|
52
|
-
key={i}
|
|
53
|
-
style={{
|
|
54
|
-
height: '14px',
|
|
55
|
-
borderRadius: '4px',
|
|
56
|
-
backgroundColor: '#e2e8f0',
|
|
57
|
-
width: i === lines - 1 ? '60%' : '100%',
|
|
58
|
-
animation: 'pulse 1.5s ease-in-out infinite',
|
|
59
|
-
}}
|
|
60
|
-
/>
|
|
61
|
-
))}
|
|
62
|
-
</div>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Inline layout styles (replacing Layout.module.scss)
|
|
67
|
-
const layoutStyles = {
|
|
68
|
-
root: { padding: '12px 0' } as React.CSSProperties,
|
|
69
|
-
twoColumns: {
|
|
70
|
-
display: 'grid',
|
|
71
|
-
gridTemplateColumns: '1fr 320px',
|
|
72
|
-
gap: '16px',
|
|
73
|
-
alignItems: 'start',
|
|
74
|
-
} as React.CSSProperties,
|
|
75
|
-
mainColumn: { minWidth: 0 } as React.CSSProperties,
|
|
76
|
-
sideColumn: {
|
|
77
|
-
display: 'flex',
|
|
78
|
-
flexDirection: 'column' as const,
|
|
79
|
-
gap: '12px',
|
|
80
|
-
position: 'sticky' as const,
|
|
81
|
-
top: '80px',
|
|
82
|
-
} as React.CSSProperties,
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ── Rewrite style dropdown ──────────────────────────────────────────────
|
|
86
|
-
const REWRITE_STYLES = [
|
|
87
|
-
{ id: 'auto', label: '✏️ Auto', desc: 'Garde le ton actuel' },
|
|
88
|
-
{ id: 'tutoyer', label: '👋 Tutoyer', desc: 'Passe en tu' },
|
|
89
|
-
{ id: 'vouvoyer', label: '🎩 Vouvoyer', desc: 'Passe en vous' },
|
|
90
|
-
{ id: 'formel', label: '💼 Formel', desc: 'Ton professionnel' },
|
|
91
|
-
{ id: 'amical', label: '😊 Amical', desc: 'Ton chaleureux' },
|
|
92
|
-
{ id: 'court', label: '⚡ Court', desc: 'Version concise' },
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
const RewriteDropdown: React.FC<{
|
|
96
|
-
disabled: boolean
|
|
97
|
-
loading: boolean
|
|
98
|
-
onSelect: (style: string) => void
|
|
99
|
-
}> = ({ disabled, loading, onSelect }) => {
|
|
100
|
-
const [open, setOpen] = useState(false)
|
|
101
|
-
const ref = React.useRef<HTMLDivElement>(null)
|
|
102
|
-
|
|
103
|
-
React.useEffect(() => {
|
|
104
|
-
if (!open) return
|
|
105
|
-
const close = (e: MouseEvent) => {
|
|
106
|
-
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
|
107
|
-
}
|
|
108
|
-
document.addEventListener('mousedown', close)
|
|
109
|
-
return () => document.removeEventListener('mousedown', close)
|
|
110
|
-
}, [open])
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
|
|
114
|
-
<button
|
|
115
|
-
type="button"
|
|
116
|
-
onClick={() => setOpen(!open)}
|
|
117
|
-
disabled={disabled}
|
|
118
|
-
style={{
|
|
119
|
-
...s.outlineBtn('#0891b2', disabled),
|
|
120
|
-
fontSize: '11px',
|
|
121
|
-
padding: '3px 10px',
|
|
122
|
-
borderRadius: '14px',
|
|
123
|
-
display: 'flex',
|
|
124
|
-
alignItems: 'center',
|
|
125
|
-
gap: '4px',
|
|
126
|
-
}}
|
|
127
|
-
>
|
|
128
|
-
{loading ? 'Reformulation...' : '✏️ Reformuler'}
|
|
129
|
-
{!loading && <span style={{ fontSize: 9, opacity: 0.7 }}>▼</span>}
|
|
130
|
-
</button>
|
|
131
|
-
{open && !disabled && !loading && (
|
|
132
|
-
<div
|
|
133
|
-
style={{
|
|
134
|
-
position: 'absolute',
|
|
135
|
-
bottom: '100%',
|
|
136
|
-
left: 0,
|
|
137
|
-
marginBottom: 4,
|
|
138
|
-
background: 'var(--theme-elevation-0, #fff)',
|
|
139
|
-
border: '1px solid var(--theme-elevation-200, #e5e7eb)',
|
|
140
|
-
borderRadius: 8,
|
|
141
|
-
boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
|
|
142
|
-
zIndex: 100,
|
|
143
|
-
minWidth: 180,
|
|
144
|
-
overflow: 'hidden',
|
|
145
|
-
}}
|
|
146
|
-
>
|
|
147
|
-
{REWRITE_STYLES.map((style) => (
|
|
148
|
-
<button
|
|
149
|
-
key={style.id}
|
|
150
|
-
type="button"
|
|
151
|
-
onClick={() => { setOpen(false); onSelect(style.id) }}
|
|
152
|
-
style={{
|
|
153
|
-
display: 'flex',
|
|
154
|
-
flexDirection: 'column',
|
|
155
|
-
width: '100%',
|
|
156
|
-
padding: '8px 12px',
|
|
157
|
-
border: 'none',
|
|
158
|
-
background: 'transparent',
|
|
159
|
-
cursor: 'pointer',
|
|
160
|
-
textAlign: 'left',
|
|
161
|
-
borderBottom: '1px solid var(--theme-elevation-100, #f3f4f6)',
|
|
162
|
-
transition: 'background 120ms',
|
|
163
|
-
}}
|
|
164
|
-
onMouseEnter={(e) => { (e.target as HTMLElement).style.background = 'var(--theme-elevation-50, #f9fafb)' }}
|
|
165
|
-
onMouseLeave={(e) => { (e.target as HTMLElement).style.background = 'transparent' }}
|
|
166
|
-
>
|
|
167
|
-
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--theme-text, #111)' }}>{style.label}</span>
|
|
168
|
-
<span style={{ fontSize: 10, color: 'var(--theme-elevation-500, #6b7280)' }}>{style.desc}</span>
|
|
169
|
-
</button>
|
|
170
|
-
))}
|
|
171
|
-
</div>
|
|
172
|
-
)}
|
|
173
|
-
</div>
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
29
|
const TicketConversation: React.FC = () => {
|
|
178
30
|
const { id } = useDocumentIdFromUrl()
|
|
179
31
|
const [features] = useState<TicketingFeatures>(() => getFeatures())
|
|
@@ -201,6 +53,7 @@ const TicketConversation: React.FC = () => {
|
|
|
201
53
|
const [ticketSubject, setTicketSubject] = useState<string>('')
|
|
202
54
|
const [ticketSource, setTicketSource] = useState<string>('')
|
|
203
55
|
const [chatSession, setChatSession] = useState<string>('')
|
|
56
|
+
const [autoCloseScheduledAt, setAutoCloseScheduledAt] = useState<string | null>(null)
|
|
204
57
|
// Client history
|
|
205
58
|
const [clientTickets, setClientTickets] = useState<Array<{ id: number; ticketNumber: string; subject: string; status: string; createdAt: string }>>([])
|
|
206
59
|
const [clientProjects, setClientProjects] = useState<Array<{ id: number; name: string; status: string }>>([])
|
|
@@ -236,6 +89,7 @@ const TicketConversation: React.FC = () => {
|
|
|
236
89
|
setClient(d.client)
|
|
237
90
|
}
|
|
238
91
|
setSnoozeUntil(d.snoozeUntil || null)
|
|
92
|
+
setAutoCloseScheduledAt(d.autoCloseScheduledAt || null)
|
|
239
93
|
setLastClientReadAt(d.lastClientReadAt || null)
|
|
240
94
|
setCurrentStatus(d.status || '')
|
|
241
95
|
setTicketNumber(d.ticketNumber || '')
|
|
@@ -384,7 +238,7 @@ const TicketConversation: React.FC = () => {
|
|
|
384
238
|
|
|
385
239
|
// Ticket actions (status, merge, snooze, ext msg, next ticket)
|
|
386
240
|
const ta = useTicketActions(id, fetchAll)
|
|
387
|
-
const { statusUpdating, handleStatusChange, showMerge, setShowMerge, mergeTarget, setMergeTarget, mergeTargetInfo, setMergeTargetInfo, mergeError, setMergeError, merging, handleMergeLookup, handleMerge, showExtMsg, setShowExtMsg, extMsgBody, setExtMsgBody, extMsgAuthor, setExtMsgAuthor, extMsgDate, setExtMsgDate, extMsgFiles, setExtMsgFiles, sendingExtMsg, handleExtFileChange, handleSendExtMsg, showSnooze, setShowSnooze, snoozeUntil, setSnoozeUntil, snoozeSaving, handleSnooze, showNextTicket, setShowNextTicket, nextTicketId, nextTicketInfo, handleNextTicket } = ta
|
|
241
|
+
const { statusUpdating, handleStatusChange, showMerge, setShowMerge, mergeTarget, setMergeTarget, mergeTargetInfo, setMergeTargetInfo, mergeError, setMergeError, merging, handleMergeLookup, handleMerge, showExtMsg, setShowExtMsg, extMsgBody, setExtMsgBody, extMsgAuthor, setExtMsgAuthor, extMsgDate, setExtMsgDate, extMsgFiles, setExtMsgFiles, sendingExtMsg, handleExtFileChange, handleSendExtMsg, showSnooze, setShowSnooze, snoozeUntil, setSnoozeUntil, snoozeSaving, handleSnooze, showReminder, setShowReminder, reminderHours, setReminderHours, reminderSending, reminderError, setReminderError, reminderSuccess, handleSendReminder, cancelingClose, handleCancelScheduledClose, showNextTicket, setShowNextTicket, nextTicketId, nextTicketInfo, handleNextTicket } = ta
|
|
388
242
|
|
|
389
243
|
// Reply composer
|
|
390
244
|
const replyEditorRef = useRef<RichTextEditorHandle>(null)
|
|
@@ -419,6 +273,7 @@ const TicketConversation: React.FC = () => {
|
|
|
419
273
|
const d = await ticketRes.json()
|
|
420
274
|
setCurrentStatus(d.status || '')
|
|
421
275
|
setSnoozeUntil(d.snoozeUntil || null)
|
|
276
|
+
setAutoCloseScheduledAt(d.autoCloseScheduledAt || null)
|
|
422
277
|
setLastClientReadAt(d.lastClientReadAt || null)
|
|
423
278
|
}
|
|
424
279
|
if (activityRes.ok) {
|
|
@@ -1094,10 +949,16 @@ const TicketConversation: React.FC = () => {
|
|
|
1094
949
|
showMerge={showMerge}
|
|
1095
950
|
showExtMsg={showExtMsg}
|
|
1096
951
|
showSnooze={showSnooze}
|
|
1097
|
-
onToggleMerge={() => { setShowMerge(!showMerge); setShowExtMsg(false); setShowSnooze(false) }}
|
|
1098
|
-
onToggleExtMsg={() => { setShowExtMsg(!showExtMsg); setShowMerge(false); setShowSnooze(false) }}
|
|
1099
|
-
onToggleSnooze={() => { setShowSnooze(!showSnooze); setShowMerge(false); setShowExtMsg(false) }}
|
|
952
|
+
onToggleMerge={() => { setShowMerge(!showMerge); setShowExtMsg(false); setShowSnooze(false); setShowReminder(false) }}
|
|
953
|
+
onToggleExtMsg={() => { setShowExtMsg(!showExtMsg); setShowMerge(false); setShowSnooze(false); setShowReminder(false) }}
|
|
954
|
+
onToggleSnooze={() => { setShowSnooze(!showSnooze); setShowMerge(false); setShowExtMsg(false); setShowReminder(false) }}
|
|
1100
955
|
onNextTicket={handleNextTicket}
|
|
956
|
+
showReminderButton={features.autoClose}
|
|
957
|
+
onToggleReminder={() => { setShowReminder(!showReminder); setReminderError(''); setShowMerge(false); setShowExtMsg(false); setShowSnooze(false) }}
|
|
958
|
+
autoCloseScheduledAt={autoCloseScheduledAt}
|
|
959
|
+
onCancelScheduledClose={handleCancelScheduledClose}
|
|
960
|
+
cancelingClose={cancelingClose}
|
|
961
|
+
reminderSuccess={reminderSuccess}
|
|
1101
962
|
showNextTicket={showNextTicket}
|
|
1102
963
|
nextTicketId={nextTicketId}
|
|
1103
964
|
nextTicketInfo={nextTicketInfo}
|
|
@@ -1139,6 +1000,17 @@ const TicketConversation: React.FC = () => {
|
|
|
1139
1000
|
{features.snooze && showSnooze && (
|
|
1140
1001
|
<SnoozePanel snoozeSaving={snoozeSaving} handleSnooze={handleSnooze} />
|
|
1141
1002
|
)}
|
|
1003
|
+
{features.autoClose && showReminder && (
|
|
1004
|
+
<ReminderPanel
|
|
1005
|
+
clientFirstName={client?.firstName || ''}
|
|
1006
|
+
clientEmail={client?.email || ''}
|
|
1007
|
+
reminderHours={reminderHours}
|
|
1008
|
+
setReminderHours={setReminderHours}
|
|
1009
|
+
reminderSending={reminderSending}
|
|
1010
|
+
reminderError={reminderError}
|
|
1011
|
+
handleSendReminder={handleSendReminder}
|
|
1012
|
+
/>
|
|
1013
|
+
)}
|
|
1142
1014
|
|
|
1143
1015
|
|
|
1144
1016
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFind } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* GET /api/support/admin-chat-stream?session=xxx
|
|
@@ -58,8 +59,7 @@ function createSessionStream(
|
|
|
58
59
|
// Poll DB every 2 seconds for new messages in this session
|
|
59
60
|
const interval = setInterval(async () => {
|
|
60
61
|
try {
|
|
61
|
-
const messages = await req.payload.
|
|
62
|
-
collection: slugs.chatMessages as any,
|
|
62
|
+
const messages = await dbFind(req.payload, slugs.chatMessages, {
|
|
63
63
|
where: {
|
|
64
64
|
session: { equals: sessionId },
|
|
65
65
|
createdAt: { greater_than: lastCheck },
|
|
@@ -140,8 +140,7 @@ function createSessionListStream(req: any, slugs: CollectionSlugs): Response {
|
|
|
140
140
|
|
|
141
141
|
const interval = setInterval(async () => {
|
|
142
142
|
try {
|
|
143
|
-
const recentMessages = await req.payload.
|
|
144
|
-
collection: slugs.chatMessages as any,
|
|
143
|
+
const recentMessages = await dbFind(req.payload, slugs.chatMessages, {
|
|
145
144
|
sort: '-createdAt',
|
|
146
145
|
limit: 50,
|
|
147
146
|
depth: 1,
|
|
@@ -3,6 +3,7 @@ import type { Where } from 'payload'
|
|
|
3
3
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
4
4
|
import { RateLimiter } from '../utils/rateLimiter'
|
|
5
5
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
6
|
+
import { dbFind, dbCreate, dbUpdate } from '../utils/db'
|
|
6
7
|
|
|
7
8
|
const adminChatLimiter = new RateLimiter(60_000, 30) // 30 per minute
|
|
8
9
|
|
|
@@ -28,8 +29,7 @@ export function createAdminChatGetEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
28
29
|
const where: Where = { session: { equals: session } }
|
|
29
30
|
if (after) where.createdAt = { greater_than: after }
|
|
30
31
|
|
|
31
|
-
const messages = await payload.
|
|
32
|
-
collection: slugs.chatMessages as any,
|
|
32
|
+
const messages = await dbFind(payload, slugs.chatMessages, {
|
|
33
33
|
where,
|
|
34
34
|
sort: 'createdAt',
|
|
35
35
|
limit: 200,
|
|
@@ -46,8 +46,7 @@ export function createAdminChatGetEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// List recent sessions: fetch only recent messages, limit to 50
|
|
49
|
-
const recentMessages = await payload.
|
|
50
|
-
collection: slugs.chatMessages as any,
|
|
49
|
+
const recentMessages = await dbFind(payload, slugs.chatMessages, {
|
|
51
50
|
sort: '-createdAt',
|
|
52
51
|
limit: 50,
|
|
53
52
|
depth: 1,
|
|
@@ -134,8 +133,7 @@ export function createAdminChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
134
133
|
return Response.json({ error: 'Session requise' }, { status: 400 })
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
const sessionMsg = await payload.
|
|
138
|
-
collection: slugs.chatMessages as any,
|
|
136
|
+
const sessionMsg = await dbFind(payload, slugs.chatMessages, {
|
|
139
137
|
where: { session: { equals: session } },
|
|
140
138
|
limit: 1,
|
|
141
139
|
depth: 0,
|
|
@@ -161,8 +159,7 @@ export function createAdminChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
161
159
|
return Response.json({ error: 'Message invalide (1-2000 caractères).' }, { status: 400 })
|
|
162
160
|
}
|
|
163
161
|
|
|
164
|
-
const newMsg = await payload.
|
|
165
|
-
collection: slugs.chatMessages as any,
|
|
162
|
+
const newMsg = await dbCreate(payload, slugs.chatMessages, {
|
|
166
163
|
data: {
|
|
167
164
|
session,
|
|
168
165
|
client: clientId,
|
|
@@ -176,8 +173,7 @@ export function createAdminChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
176
173
|
|
|
177
174
|
// Try to link to ticket
|
|
178
175
|
try {
|
|
179
|
-
const linkedTicket = await payload.
|
|
180
|
-
collection: slugs.tickets as any,
|
|
176
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
181
177
|
where: { chatSession: { equals: session } },
|
|
182
178
|
limit: 1,
|
|
183
179
|
depth: 0,
|
|
@@ -185,8 +181,7 @@ export function createAdminChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
185
181
|
})
|
|
186
182
|
if (linkedTicket.docs.length > 0) {
|
|
187
183
|
const ticketId = linkedTicket.docs[0].id
|
|
188
|
-
await payload.
|
|
189
|
-
collection: slugs.ticketMessages as any,
|
|
184
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
190
185
|
data: {
|
|
191
186
|
ticket: ticketId,
|
|
192
187
|
body: trimmedMessage,
|
|
@@ -195,8 +190,7 @@ export function createAdminChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
195
190
|
},
|
|
196
191
|
overrideAccess: true,
|
|
197
192
|
})
|
|
198
|
-
await payload.
|
|
199
|
-
collection: slugs.chatMessages as any,
|
|
193
|
+
await dbUpdate(payload, slugs.chatMessages, {
|
|
200
194
|
id: newMsg.id,
|
|
201
195
|
data: { ticket: ticketId },
|
|
202
196
|
overrideAccess: true,
|
|
@@ -211,8 +205,7 @@ export function createAdminChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
211
205
|
|
|
212
206
|
// Admin closes session
|
|
213
207
|
if (action === 'close') {
|
|
214
|
-
await payload.
|
|
215
|
-
collection: slugs.chatMessages as any,
|
|
208
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
216
209
|
data: {
|
|
217
210
|
session,
|
|
218
211
|
client: clientId,
|
|
@@ -224,16 +217,14 @@ export function createAdminChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
224
217
|
})
|
|
225
218
|
|
|
226
219
|
try {
|
|
227
|
-
const linkedTicket = await payload.
|
|
228
|
-
collection: slugs.tickets as any,
|
|
220
|
+
const linkedTicket = await dbFind(payload, slugs.tickets, {
|
|
229
221
|
where: { chatSession: { equals: session } },
|
|
230
222
|
limit: 1,
|
|
231
223
|
depth: 0,
|
|
232
224
|
overrideAccess: true,
|
|
233
225
|
})
|
|
234
226
|
if (linkedTicket.docs.length > 0) {
|
|
235
|
-
await payload.
|
|
236
|
-
collection: slugs.ticketMessages as any,
|
|
227
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
237
228
|
data: {
|
|
238
229
|
ticket: linkedTicket.docs[0].id,
|
|
239
230
|
body: 'Session de chat terminée par un agent.',
|