@consilioweb/payload-support 0.16.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/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/index.cjs +10 -147
- package/dist/components/TicketConversation/index.js +4 -141
- package/dist/index.cjs +1604 -671
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1602 -671
- 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 +129 -31
- package/src/collections/Tickets.ts +31 -73
- 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/constants.ts +29 -0
- package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
- package/src/components/TicketConversation/index.tsx +4 -152
- 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 +6 -10
- 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 +18 -1
- 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 +6 -9
- 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,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'
|
|
@@ -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())
|
|
@@ -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.',
|
|
@@ -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, dbCount } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* GET /api/support/admin-stats
|
|
@@ -21,8 +22,7 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
21
22
|
const statuses = ['open', 'waiting_client', 'resolved'] as const
|
|
22
23
|
const statusCounts = await Promise.all(
|
|
23
24
|
statuses.map(async (status) => {
|
|
24
|
-
const result = await payload.
|
|
25
|
-
collection: slugs.tickets as any,
|
|
25
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
26
26
|
where: { status: { equals: status } },
|
|
27
27
|
overrideAccess: true,
|
|
28
28
|
})
|
|
@@ -36,8 +36,7 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
36
36
|
const priorities = ['low', 'normal', 'high', 'urgent'] as const
|
|
37
37
|
const priorityCounts = await Promise.all(
|
|
38
38
|
priorities.map(async (priority) => {
|
|
39
|
-
const result = await payload.
|
|
40
|
-
collection: slugs.tickets as any,
|
|
39
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
41
40
|
where: { priority: { equals: priority } },
|
|
42
41
|
overrideAccess: true,
|
|
43
42
|
})
|
|
@@ -50,8 +49,7 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
50
49
|
const categories = ['bug', 'content', 'feature', 'question', 'hosting'] as const
|
|
51
50
|
const categoryCounts = await Promise.all(
|
|
52
51
|
categories.map(async (category) => {
|
|
53
|
-
const result = await payload.
|
|
54
|
-
collection: slugs.tickets as any,
|
|
52
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
55
53
|
where: { category: { equals: category } },
|
|
56
54
|
overrideAccess: true,
|
|
57
55
|
})
|
|
@@ -68,18 +66,38 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
68
66
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
69
67
|
|
|
70
68
|
const [created7, created30] = await Promise.all([
|
|
71
|
-
payload.
|
|
72
|
-
collection: slugs.tickets as any,
|
|
69
|
+
dbCount(payload, slugs.tickets, {
|
|
73
70
|
where: { createdAt: { greater_than_equal: sevenDaysAgo.toISOString() } },
|
|
74
71
|
overrideAccess: true,
|
|
75
72
|
}),
|
|
76
|
-
payload.
|
|
77
|
-
collection: slugs.tickets as any,
|
|
73
|
+
dbCount(payload, slugs.tickets, {
|
|
78
74
|
where: { createdAt: { greater_than_equal: thirtyDaysAgo.toISOString() } },
|
|
79
75
|
overrideAccess: true,
|
|
80
76
|
}),
|
|
81
77
|
])
|
|
82
78
|
|
|
79
|
+
// Real per-day ticket volume for the last 7 days (chronological), replacing
|
|
80
|
+
// the previous synthetic distribution in the dashboard chart.
|
|
81
|
+
const volumeByDay = await Promise.all(
|
|
82
|
+
Array.from({ length: 7 }, (_, i) => i).map(async (offset) => {
|
|
83
|
+
const dayStart = new Date(now)
|
|
84
|
+
dayStart.setHours(0, 0, 0, 0)
|
|
85
|
+
dayStart.setDate(dayStart.getDate() - (6 - offset))
|
|
86
|
+
const dayEnd = new Date(dayStart)
|
|
87
|
+
dayEnd.setDate(dayEnd.getDate() + 1)
|
|
88
|
+
const r = await dbCount(payload, slugs.tickets, {
|
|
89
|
+
where: {
|
|
90
|
+
and: [
|
|
91
|
+
{ createdAt: { greater_than_equal: dayStart.toISOString() } },
|
|
92
|
+
{ createdAt: { less_than: dayEnd.toISOString() } },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
overrideAccess: true,
|
|
96
|
+
})
|
|
97
|
+
return { date: dayStart.toISOString(), count: r.totalDocs }
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
|
|
83
101
|
// ── Averages: paginate with limit:100, only fetch needed fields ──
|
|
84
102
|
const PAGE_SIZE = 100
|
|
85
103
|
const MAX_PAGES = 50
|
|
@@ -93,8 +111,7 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
93
111
|
let hasMore = true
|
|
94
112
|
|
|
95
113
|
while (hasMore && page <= MAX_PAGES) {
|
|
96
|
-
const batch = await payload.
|
|
97
|
-
collection: slugs.tickets as any,
|
|
114
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
98
115
|
limit: PAGE_SIZE,
|
|
99
116
|
page,
|
|
100
117
|
depth: 0,
|
|
@@ -124,46 +141,64 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
124
141
|
page++
|
|
125
142
|
}
|
|
126
143
|
|
|
127
|
-
// ── Satisfaction:
|
|
128
|
-
const surveyCount = await payload.
|
|
129
|
-
collection: slugs.satisfactionSurveys as any,
|
|
144
|
+
// ── Satisfaction (CSAT) + NPS: count + paginated aggregation ──
|
|
145
|
+
const surveyCount = await dbCount(payload, slugs.satisfactionSurveys, {
|
|
130
146
|
overrideAccess: true,
|
|
131
147
|
})
|
|
132
148
|
|
|
133
149
|
let satisfactionAvg = 0
|
|
150
|
+
let satisfactionCount = 0
|
|
151
|
+
let npsScore: number | null = null
|
|
152
|
+
let npsCount = 0
|
|
134
153
|
if (surveyCount.totalDocs > 0) {
|
|
135
|
-
let
|
|
154
|
+
let ratingSum = 0
|
|
155
|
+
let ratingN = 0
|
|
156
|
+
let promoters = 0
|
|
157
|
+
let detractors = 0
|
|
158
|
+
let npsN = 0
|
|
136
159
|
let surveyPage = 1
|
|
137
160
|
let surveyHasMore = true
|
|
138
161
|
while (surveyHasMore && surveyPage <= MAX_PAGES) {
|
|
139
|
-
const batch = await payload.
|
|
140
|
-
collection: slugs.satisfactionSurveys as any,
|
|
162
|
+
const batch = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
141
163
|
limit: PAGE_SIZE,
|
|
142
164
|
page: surveyPage,
|
|
143
165
|
depth: 0,
|
|
144
166
|
overrideAccess: true,
|
|
145
|
-
select: { rating: true },
|
|
167
|
+
select: { rating: true, nps: true },
|
|
146
168
|
})
|
|
147
169
|
for (const s of batch.docs) {
|
|
148
|
-
|
|
170
|
+
const r = (s as { rating?: number }).rating
|
|
171
|
+
if (typeof r === 'number' && r > 0) { ratingSum += r; ratingN++ }
|
|
172
|
+
const n = (s as { nps?: number }).nps
|
|
173
|
+
if (typeof n === 'number') {
|
|
174
|
+
npsN++
|
|
175
|
+
if (n >= 9) promoters++
|
|
176
|
+
else if (n <= 6) detractors++
|
|
177
|
+
}
|
|
149
178
|
}
|
|
150
179
|
surveyHasMore = batch.hasNextPage ?? false
|
|
151
180
|
surveyPage++
|
|
152
181
|
}
|
|
153
|
-
satisfactionAvg = Math.round((
|
|
182
|
+
satisfactionAvg = ratingN > 0 ? Math.round((ratingSum / ratingN) * 10) / 10 : 0
|
|
183
|
+
satisfactionCount = ratingN
|
|
184
|
+
if (npsN > 0) {
|
|
185
|
+
// NPS = %promoters - %detractors, on a -100..100 scale.
|
|
186
|
+
npsScore = Math.round(((promoters - detractors) / npsN) * 100)
|
|
187
|
+
npsCount = npsN
|
|
188
|
+
}
|
|
154
189
|
}
|
|
155
190
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
payload.
|
|
162
|
-
collection: slugs.pendingEmails as any,
|
|
191
|
+
const clientCount = await dbCount(payload, slugs.supportClients, { overrideAccess: true })
|
|
192
|
+
// pendingEmails is an optional feature — tolerate its collection being absent
|
|
193
|
+
// (otherwise the whole stats endpoint 500s on installs without it).
|
|
194
|
+
let pendingEmailsTotal = 0
|
|
195
|
+
try {
|
|
196
|
+
const pe = await dbCount(payload, slugs.pendingEmails, {
|
|
163
197
|
where: { status: { equals: 'pending' } },
|
|
164
198
|
overrideAccess: true,
|
|
165
|
-
})
|
|
166
|
-
|
|
199
|
+
})
|
|
200
|
+
pendingEmailsTotal = pe.totalDocs
|
|
201
|
+
} catch { /* pendingEmails feature disabled — count stays 0 */ }
|
|
167
202
|
|
|
168
203
|
return new Response(JSON.stringify({
|
|
169
204
|
total,
|
|
@@ -172,6 +207,7 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
172
207
|
byCategory,
|
|
173
208
|
createdLast7Days: created7.totalDocs,
|
|
174
209
|
createdLast30Days: created30.totalDocs,
|
|
210
|
+
volumeByDay,
|
|
175
211
|
avgResponseTimeHours: responseTimeCount > 0
|
|
176
212
|
? Math.round((totalResponseTimeMs / responseTimeCount / (1000 * 60 * 60)) * 10) / 10
|
|
177
213
|
: null,
|
|
@@ -180,9 +216,11 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
180
216
|
: null,
|
|
181
217
|
totalTimeMinutes,
|
|
182
218
|
satisfactionAvg,
|
|
183
|
-
satisfactionCount
|
|
219
|
+
satisfactionCount,
|
|
220
|
+
npsScore,
|
|
221
|
+
npsCount,
|
|
184
222
|
clientCount: clientCount.totalDocs,
|
|
185
|
-
pendingEmailsCount:
|
|
223
|
+
pendingEmailsCount: pendingEmailsTotal,
|
|
186
224
|
}), {
|
|
187
225
|
headers: {
|
|
188
226
|
'Content-Type': 'application/json',
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload'
|
|
2
|
+
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { runAiAgent } from '../utils/aiAgent'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* POST /api/support/ai-agent body: { ticketId, confidenceThreshold? }
|
|
8
|
+
*
|
|
9
|
+
* Runs the autonomous AI agent on a ticket: it either posts a KB-grounded answer
|
|
10
|
+
* (when confident) or escalates to a human with an internal note. Admin-only.
|
|
11
|
+
*/
|
|
12
|
+
export function createAiAgentEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
13
|
+
return {
|
|
14
|
+
path: '/support/ai-agent',
|
|
15
|
+
method: 'post',
|
|
16
|
+
handler: async (req) => {
|
|
17
|
+
try {
|
|
18
|
+
requireAdmin(req, slugs)
|
|
19
|
+
let body: { ticketId?: number | string; confidenceThreshold?: number } = {}
|
|
20
|
+
try { body = (await req.json!()) as typeof body } catch { /* no body */ }
|
|
21
|
+
if (!body.ticketId) {
|
|
22
|
+
return Response.json({ error: 'ticketId requis.' }, { status: 400 })
|
|
23
|
+
}
|
|
24
|
+
const result = await runAiAgent(req.payload, slugs, body.ticketId, {
|
|
25
|
+
confidenceThreshold: body.confidenceThreshold,
|
|
26
|
+
})
|
|
27
|
+
return Response.json({ ok: true, ...result })
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const authResponse = handleAuthError(error)
|
|
30
|
+
if (authResponse) return authResponse
|
|
31
|
+
console.error('[ai-agent] Error:', error)
|
|
32
|
+
return Response.json({ error: 'Erreur interne' }, { status: 500 })
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -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 { dbFindByID, dbUpdate, dbCreate } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
interface MacroAction {
|
|
6
7
|
type: 'set_status' | 'set_priority' | 'add_tag' | 'send_reply' | 'assign'
|
|
@@ -28,8 +29,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
28
29
|
return Response.json({ error: 'macroId and ticketId are required' }, { status: 400 })
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const macro = await payload.
|
|
32
|
-
collection: slugs.macros as any,
|
|
32
|
+
const macro = await dbFindByID(payload, slugs.macros, {
|
|
33
33
|
id: macroId,
|
|
34
34
|
depth: 0,
|
|
35
35
|
overrideAccess: true,
|
|
@@ -38,8 +38,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
38
38
|
if (!macro) return Response.json({ error: 'Macro not found' }, { status: 404 })
|
|
39
39
|
if (!macro.isActive) return Response.json({ error: 'Macro is disabled' }, { status: 400 })
|
|
40
40
|
|
|
41
|
-
const ticket = await payload.
|
|
42
|
-
collection: slugs.tickets as any,
|
|
41
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
43
42
|
id: ticketId,
|
|
44
43
|
depth: 0,
|
|
45
44
|
overrideAccess: true,
|
|
@@ -54,8 +53,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
54
53
|
try {
|
|
55
54
|
switch (action.type) {
|
|
56
55
|
case 'set_status':
|
|
57
|
-
await payload.
|
|
58
|
-
collection: slugs.tickets as any,
|
|
56
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
59
57
|
id: ticketId,
|
|
60
58
|
data: { status: action.value },
|
|
61
59
|
overrideAccess: true,
|
|
@@ -64,8 +62,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
64
62
|
break
|
|
65
63
|
|
|
66
64
|
case 'set_priority':
|
|
67
|
-
await payload.
|
|
68
|
-
collection: slugs.tickets as any,
|
|
65
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
69
66
|
id: ticketId,
|
|
70
67
|
data: { priority: action.value },
|
|
71
68
|
overrideAccess: true,
|
|
@@ -78,8 +75,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
78
75
|
if (!currentTags.includes(action.value)) {
|
|
79
76
|
currentTags.push(action.value)
|
|
80
77
|
}
|
|
81
|
-
await payload.
|
|
82
|
-
collection: slugs.tickets as any,
|
|
78
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
83
79
|
id: ticketId,
|
|
84
80
|
data: { tags: currentTags },
|
|
85
81
|
overrideAccess: true,
|
|
@@ -89,8 +85,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
89
85
|
}
|
|
90
86
|
|
|
91
87
|
case 'send_reply':
|
|
92
|
-
await payload.
|
|
93
|
-
collection: slugs.ticketMessages as any,
|
|
88
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
94
89
|
data: {
|
|
95
90
|
ticket: ticketId,
|
|
96
91
|
body: action.value,
|
|
@@ -108,8 +103,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
108
103
|
appliedActions.push({ type: action.type, value: action.value, success: false, error: 'Invalid user ID' })
|
|
109
104
|
break
|
|
110
105
|
}
|
|
111
|
-
await payload.
|
|
112
|
-
collection: slugs.tickets as any,
|
|
106
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
113
107
|
id: ticketId,
|
|
114
108
|
data: { assignedTo: userId },
|
|
115
109
|
overrideAccess: true,
|