@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.
Files changed (193) hide show
  1. package/README.md +347 -435
  2. package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
  4. package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
  5. package/dist/components/TicketConversation/SkeletonText.js +20 -0
  6. package/dist/components/TicketConversation/constants.cjs +27 -0
  7. package/dist/components/TicketConversation/constants.js +26 -1
  8. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
  9. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
  10. package/dist/components/TicketConversation/index.cjs +10 -147
  11. package/dist/components/TicketConversation/index.js +4 -141
  12. package/dist/index.cjs +1604 -671
  13. package/dist/index.d.cts +3 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +1602 -671
  16. package/dist/views/BillingView/client.cjs +40 -54
  17. package/dist/views/BillingView/client.js +40 -54
  18. package/dist/views/ChatView/client.cjs +4 -3
  19. package/dist/views/ChatView/client.js +4 -3
  20. package/dist/views/CrmView/client.cjs +7 -7
  21. package/dist/views/CrmView/client.js +7 -7
  22. package/dist/views/EmailTrackingView/client.cjs +6 -5
  23. package/dist/views/EmailTrackingView/client.js +6 -5
  24. package/dist/views/ImportConversationView/client.cjs +4 -4
  25. package/dist/views/ImportConversationView/client.js +4 -4
  26. package/dist/views/LogsView/client.cjs +3 -2
  27. package/dist/views/LogsView/client.js +3 -2
  28. package/dist/views/NewTicketView/client.cjs +1 -1
  29. package/dist/views/NewTicketView/client.js +1 -1
  30. package/dist/views/PendingEmailsView/client.cjs +7 -6
  31. package/dist/views/PendingEmailsView/client.js +7 -6
  32. package/dist/views/SupportDashboardView/client.cjs +19 -22
  33. package/dist/views/SupportDashboardView/client.js +19 -22
  34. package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
  35. package/dist/views/TicketDetailView/NextActionItem.js +34 -0
  36. package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
  37. package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
  38. package/dist/views/TicketDetailView/client.cjs +59 -190
  39. package/dist/views/TicketDetailView/client.js +52 -183
  40. package/dist/views/TicketDetailView/constants.cjs +20 -0
  41. package/dist/views/TicketDetailView/constants.js +16 -0
  42. package/dist/views/TicketDetailView/helpers.cjs +23 -0
  43. package/dist/views/TicketDetailView/helpers.js +20 -0
  44. package/dist/views/TicketDetailView/types.cjs +2 -0
  45. package/dist/views/TicketDetailView/types.js +1 -0
  46. package/dist/views/TicketInboxView/client.cjs +14 -10
  47. package/dist/views/TicketInboxView/client.js +14 -10
  48. package/dist/views/TicketingSettingsView/client.cjs +101 -96
  49. package/dist/views/TicketingSettingsView/client.js +92 -88
  50. package/dist/views/TimeDashboardView/client.cjs +2 -2
  51. package/dist/views/TimeDashboardView/client.js +2 -2
  52. package/dist/views/shared/StatusPill.cjs +23 -12
  53. package/dist/views/shared/StatusPill.js +23 -12
  54. package/dist/views/shared/dateLocale.cjs +5 -0
  55. package/dist/views/shared/dateLocale.js +3 -0
  56. package/package.json +23 -15
  57. package/src/__tests__/generateTrackingToken.test.ts +53 -0
  58. package/src/__tests__/integration/ai-agent.test.ts +46 -0
  59. package/src/__tests__/integration/auth.test.ts +43 -0
  60. package/src/__tests__/integration/automation-rules.test.ts +68 -0
  61. package/src/__tests__/integration/buildTestPayload.ts +52 -0
  62. package/src/__tests__/integration/channels.test.ts +52 -0
  63. package/src/__tests__/integration/core-behaviors.test.ts +58 -0
  64. package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
  65. package/src/__tests__/integration/digest.test.ts +48 -0
  66. package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
  67. package/src/__tests__/integration/isolation.test.ts +81 -0
  68. package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
  69. package/src/__tests__/integration/nps.test.ts +37 -0
  70. package/src/__tests__/integration/sanitization.test.ts +35 -0
  71. package/src/__tests__/integration/sla-pause.test.ts +39 -0
  72. package/src/__tests__/integration/smoke.test.ts +26 -0
  73. package/src/__tests__/integration/snooze.test.ts +68 -0
  74. package/src/__tests__/integration/teams.test.ts +38 -0
  75. package/src/__tests__/rateLimiter.test.ts +89 -0
  76. package/src/__tests__/sanitizeHtml.test.ts +165 -0
  77. package/src/__tests__/sla.test.ts +258 -0
  78. package/src/collections/AutomationRules.ts +66 -0
  79. package/src/collections/NotificationQueue.ts +30 -0
  80. package/src/collections/SatisfactionSurveys.ts +11 -2
  81. package/src/collections/SupportClients.ts +58 -1
  82. package/src/collections/SupportTeam.ts +27 -0
  83. package/src/collections/TicketActivityLog.ts +9 -4
  84. package/src/collections/TicketMessages.ts +129 -31
  85. package/src/collections/Tickets.ts +31 -73
  86. package/src/collections/index.ts +3 -0
  87. package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
  88. package/src/components/TicketConversation/SkeletonText.tsx +23 -0
  89. package/src/components/TicketConversation/constants.ts +29 -0
  90. package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
  91. package/src/components/TicketConversation/index.tsx +4 -152
  92. package/src/endpoints/admin-chat-stream.ts +3 -4
  93. package/src/endpoints/admin-chat.ts +11 -20
  94. package/src/endpoints/admin-stats.ts +70 -32
  95. package/src/endpoints/ai-agent.ts +36 -0
  96. package/src/endpoints/apply-macro.ts +8 -14
  97. package/src/endpoints/auth-2fa.ts +12 -12
  98. package/src/endpoints/auto-close.ts +6 -10
  99. package/src/endpoints/billing.ts +17 -4
  100. package/src/endpoints/bulk-action.ts +7 -12
  101. package/src/endpoints/channels.ts +89 -0
  102. package/src/endpoints/chat-stream.ts +3 -4
  103. package/src/endpoints/chat.ts +7 -12
  104. package/src/endpoints/chatbot.ts +2 -2
  105. package/src/endpoints/client-intelligence.ts +4 -4
  106. package/src/endpoints/delete-account.ts +9 -16
  107. package/src/endpoints/email-stats.ts +2 -2
  108. package/src/endpoints/escalate.ts +2 -2
  109. package/src/endpoints/export-csv.ts +2 -2
  110. package/src/endpoints/export-data.ts +5 -8
  111. package/src/endpoints/import-conversation.ts +6 -10
  112. package/src/endpoints/index.ts +18 -1
  113. package/src/endpoints/invite-collaborator.ts +30 -15
  114. package/src/endpoints/invoice.ts +127 -0
  115. package/src/endpoints/kb-search.ts +2 -2
  116. package/src/endpoints/login.ts +12 -4
  117. package/src/endpoints/merge-clients.ts +14 -19
  118. package/src/endpoints/merge-tickets.ts +11 -18
  119. package/src/endpoints/oauth-google.ts +86 -39
  120. package/src/endpoints/pending-emails-process.ts +15 -25
  121. package/src/endpoints/process-digests.ts +93 -0
  122. package/src/endpoints/process-scheduled.ts +6 -10
  123. package/src/endpoints/process-snooze.ts +78 -0
  124. package/src/endpoints/purge-logs.ts +2 -2
  125. package/src/endpoints/resend-notification.ts +3 -4
  126. package/src/endpoints/round-robin-config.ts +3 -4
  127. package/src/endpoints/satisfaction.ts +21 -14
  128. package/src/endpoints/search.ts +5 -8
  129. package/src/endpoints/seed-kb.ts +3 -4
  130. package/src/endpoints/send-reminder.ts +6 -9
  131. package/src/endpoints/settings.ts +5 -2
  132. package/src/endpoints/signature.ts +3 -4
  133. package/src/endpoints/sla-check.ts +2 -2
  134. package/src/endpoints/split-ticket.ts +7 -12
  135. package/src/endpoints/statuses.ts +2 -2
  136. package/src/endpoints/ticket-feedback.ts +4 -6
  137. package/src/endpoints/ticket-synthesis.ts +2 -2
  138. package/src/endpoints/track-open.ts +16 -28
  139. package/src/endpoints/transfer-ticket.ts +21 -9
  140. package/src/endpoints/user-prefs.ts +3 -4
  141. package/src/hooks/automationRules.ts +84 -0
  142. package/src/hooks/checkSLA.ts +48 -16
  143. package/src/plugin.ts +6 -0
  144. package/src/portal/LiveChat.tsx +2 -63
  145. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
  146. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
  147. package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
  148. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
  149. package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
  150. package/src/portal/auth/tickets/detail/page.tsx +2 -2
  151. package/src/portal/helpers.ts +46 -0
  152. package/src/portal/login/page.tsx +3 -8
  153. package/src/portal/types.ts +15 -0
  154. package/src/types/lucide-react.d.ts +34 -0
  155. package/src/types/payload-config.d.ts +10 -0
  156. package/src/utils/adminNotification.ts +2 -2
  157. package/src/utils/aiAgent.ts +147 -0
  158. package/src/utils/auth.ts +5 -4
  159. package/src/utils/channels.ts +36 -0
  160. package/src/utils/db.ts +73 -0
  161. package/src/utils/emailTemplate.ts +8 -1
  162. package/src/utils/fireWebhooks.ts +4 -6
  163. package/src/utils/generateTicketSynthesis.ts +7 -12
  164. package/src/utils/invoicePdf.ts +88 -0
  165. package/src/utils/notificationQueue.ts +36 -0
  166. package/src/utils/readSettings.ts +22 -6
  167. package/src/utils/sanitizeHtml.ts +57 -0
  168. package/src/utils/slugs.ts +6 -0
  169. package/src/utils/teamAccess.ts +27 -0
  170. package/src/utils/ticketAccess.ts +53 -0
  171. package/src/utils/webhookDispatcher.ts +4 -6
  172. package/src/views/BillingView/client.tsx +49 -37
  173. package/src/views/ChatView/client.tsx +4 -3
  174. package/src/views/CrmView/client.tsx +7 -7
  175. package/src/views/EmailTrackingView/client.tsx +7 -6
  176. package/src/views/ImportConversationView/client.tsx +4 -4
  177. package/src/views/LogsView/client.tsx +3 -2
  178. package/src/views/NewTicketView/client.tsx +1 -1
  179. package/src/views/PendingEmailsView/client.tsx +7 -6
  180. package/src/views/SupportDashboardView/client.tsx +21 -21
  181. package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
  182. package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
  183. package/src/views/TicketDetailView/client.tsx +54 -179
  184. package/src/views/TicketDetailView/constants.ts +23 -0
  185. package/src/views/TicketDetailView/helpers.ts +17 -0
  186. package/src/views/TicketDetailView/types.ts +10 -0
  187. package/src/views/TicketInboxView/client.tsx +16 -10
  188. package/src/views/TicketingSettingsView/client.tsx +90 -87
  189. package/src/views/TimeDashboardView/client.tsx +2 -2
  190. package/src/views/shared/StatusPill.tsx +24 -12
  191. package/src/views/shared/dateLocale.ts +5 -0
  192. package/src/views/shared/locales/en.json +708 -0
  193. 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.find({
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.find({
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.find({
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.find({
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.find({
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.create({
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.find({
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.create({
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.update({
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.create({
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.find({
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.create({
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.count({
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.count({
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.count({
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.count({
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.count({
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.find({
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: use count + paginated average ──
128
- const surveyCount = await payload.count({
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 totalRating = 0
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.find({
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
- totalRating += ((s as any).rating || 0)
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((totalRating / surveyCount.totalDocs) * 10) / 10
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 [clientCount, pendingEmailsCount] = await Promise.all([
157
- payload.count({
158
- collection: slugs.supportClients as any,
159
- overrideAccess: true,
160
- }),
161
- payload.count({
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: surveyCount.totalDocs,
219
+ satisfactionCount,
220
+ npsScore,
221
+ npsCount,
184
222
  clientCount: clientCount.totalDocs,
185
- pendingEmailsCount: pendingEmailsCount.totalDocs,
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.findByID({
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.findByID({
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.update({
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.update({
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.update({
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.create({
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.update({
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,