@consilioweb/payload-support 2.0.0 β†’ 3.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 (222) hide show
  1. package/README.md +656 -355
  2. package/dist/components/RichTextEditor/index.d.ts +19 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.d.ts +6 -0
  4. package/dist/components/TicketConversation/SkeletonText.d.ts +4 -0
  5. package/dist/components/TicketConversation/components/AISummaryPanel.d.ts +14 -0
  6. package/dist/components/TicketConversation/components/ActionPanels.d.ts +47 -0
  7. package/dist/components/TicketConversation/components/ActivityLog.d.ts +5 -0
  8. package/dist/components/TicketConversation/components/ClientBar.d.ts +5 -0
  9. package/dist/components/TicketConversation/components/ClientHistory.d.ts +24 -0
  10. package/dist/components/TicketConversation/components/CodeBlock.d.ts +26 -0
  11. package/dist/components/TicketConversation/components/CodeBlockInserter.d.ts +12 -0
  12. package/dist/components/TicketConversation/components/QuickActions.d.ts +32 -0
  13. package/dist/components/TicketConversation/components/TicketHeader.d.ts +19 -0
  14. package/dist/components/TicketConversation/components/TimeTrackingPanel.d.ts +24 -0
  15. package/dist/components/TicketConversation/config.d.ts +10 -0
  16. package/dist/components/TicketConversation/config.js +1 -40
  17. package/dist/components/TicketConversation/constants.d.ts +56 -0
  18. package/dist/components/TicketConversation/context.d.ts +53 -0
  19. package/dist/components/TicketConversation/hooks/useAI.d.ts +21 -0
  20. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.d.ts +3 -0
  21. package/dist/components/TicketConversation/hooks/useFeatures.d.ts +23 -0
  22. package/dist/components/TicketConversation/hooks/useFeatures.js +61 -0
  23. package/dist/components/TicketConversation/hooks/useMessageActions.d.ts +20 -0
  24. package/dist/components/TicketConversation/hooks/useReply.d.ts +27 -0
  25. package/dist/components/TicketConversation/hooks/useTicketActions.d.ts +58 -0
  26. package/dist/components/TicketConversation/hooks/useTimeTracking.d.ts +18 -0
  27. package/dist/components/TicketConversation/hooks/useTranslation.d.ts +33 -0
  28. package/dist/components/TicketConversation/hooks/useTranslation.js +22 -3
  29. package/dist/components/TicketConversation/index.d.ts +4 -0
  30. package/dist/components/TicketConversation/index.js +2 -2
  31. package/dist/components/TicketConversation/types.d.ts +56 -0
  32. package/dist/components/TicketConversation/utils.d.ts +2 -0
  33. package/dist/index.cjs +392 -320
  34. package/dist/index.d.cts +100 -3
  35. package/dist/index.d.ts +100 -3
  36. package/dist/index.js +388 -321
  37. package/dist/utils/features.d.ts +123 -0
  38. package/dist/utils/features.js +147 -0
  39. package/dist/views/BillingView/client.d.ts +2 -0
  40. package/dist/views/BillingView/index.d.ts +4 -0
  41. package/dist/views/ChatView/client.d.ts +2 -0
  42. package/dist/views/ChatView/client.js +9 -9
  43. package/dist/views/ChatView/index.d.ts +4 -0
  44. package/dist/views/CrmView/client.d.ts +2 -0
  45. package/dist/views/CrmView/client.js +6 -9
  46. package/dist/views/CrmView/index.d.ts +4 -0
  47. package/dist/views/EmailTrackingView/client.d.ts +2 -0
  48. package/dist/views/EmailTrackingView/index.d.ts +4 -0
  49. package/dist/views/ImportConversationView/client.d.ts +2 -0
  50. package/dist/views/ImportConversationView/index.d.ts +4 -0
  51. package/dist/views/LogsView/client.d.ts +2 -0
  52. package/dist/views/LogsView/index.d.ts +4 -0
  53. package/dist/views/NewTicketView/client.d.ts +2 -0
  54. package/dist/views/NewTicketView/index.d.ts +4 -0
  55. package/dist/views/PendingEmailsView/client.d.ts +2 -0
  56. package/dist/views/PendingEmailsView/index.d.ts +4 -0
  57. package/dist/views/SupportDashboardView/client.d.ts +2 -0
  58. package/dist/views/SupportDashboardView/client.js +1 -1
  59. package/dist/views/SupportDashboardView/index.d.ts +4 -0
  60. package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
  61. package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
  62. package/dist/views/TicketDetailView/client.d.ts +2 -0
  63. package/dist/views/TicketDetailView/client.js +2 -2
  64. package/dist/views/TicketDetailView/constants.d.ts +13 -0
  65. package/dist/views/TicketDetailView/helpers.d.ts +2 -0
  66. package/dist/views/TicketDetailView/index.d.ts +4 -0
  67. package/dist/views/TicketDetailView/types.d.ts +40 -0
  68. package/dist/views/TicketInboxView/client.d.ts +2 -0
  69. package/dist/views/TicketInboxView/client.js +5 -5
  70. package/dist/views/TicketInboxView/index.d.ts +4 -0
  71. package/dist/views/TicketingSettingsView/client.d.ts +2 -0
  72. package/dist/views/TicketingSettingsView/client.js +6 -5
  73. package/dist/views/TicketingSettingsView/index.d.ts +4 -0
  74. package/dist/views/TimeDashboardView/client.d.ts +2 -0
  75. package/dist/views/TimeDashboardView/index.d.ts +4 -0
  76. package/dist/views/shared/AdminViewHeader.d.ts +13 -0
  77. package/dist/views/shared/ErrorBoundary.d.ts +17 -0
  78. package/dist/views/shared/Skeleton.d.ts +21 -0
  79. package/dist/views/shared/StatusPill.d.ts +8 -0
  80. package/dist/views/shared/adminTokens.d.ts +19 -0
  81. package/dist/views/shared/config.d.ts +8 -0
  82. package/dist/views/shared/config.js +1 -40
  83. package/dist/views/shared/dateLocale.d.ts +5 -0
  84. package/dist/views/shared/index.d.ts +12 -0
  85. package/dist/views/shared/index.js +2 -1
  86. package/dist/views/shared/locales/en.json +708 -0
  87. package/dist/views/shared/locales/fr.json +708 -0
  88. package/dist/views/shared/sla.d.ts +14 -0
  89. package/dist/views.d.ts +13 -13
  90. package/package.json +26 -41
  91. package/src/collections/SupportClients.ts +29 -0
  92. package/src/collections/TicketCollaborators.ts +6 -1
  93. package/src/collections/TicketMessages.ts +46 -24
  94. package/src/collections/Tickets.ts +117 -79
  95. package/src/collections/TimeEntries.ts +57 -32
  96. package/src/components/TicketConversation/config.ts +16 -80
  97. package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
  98. package/src/components/TicketConversation/hooks/useTranslation.ts +38 -4
  99. package/src/components/TicketConversation/index.tsx +2 -2
  100. package/src/endpoints/ai.ts +2 -1
  101. package/src/endpoints/client-intelligence.ts +2 -1
  102. package/src/endpoints/process-scheduled.ts +13 -8
  103. package/src/endpoints/round-robin-config.ts +30 -20
  104. package/src/endpoints/settings.ts +28 -77
  105. package/src/index.ts +6 -2
  106. package/src/portal/LiveChat.tsx +33 -22
  107. package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
  108. package/src/types/lucide-react.d.ts +10 -2
  109. package/src/utils/aiAgent.ts +2 -1
  110. package/src/utils/aiProvider.ts +21 -0
  111. package/src/utils/features.ts +297 -0
  112. package/src/utils/fireWebhooks.ts +6 -0
  113. package/src/utils/generateTicketSynthesis.ts +2 -1
  114. package/src/utils/index.ts +4 -2
  115. package/src/utils/readSettings.ts +82 -9
  116. package/src/utils/webhookDispatcher.ts +19 -7
  117. package/src/views/ChatView/client.tsx +13 -9
  118. package/src/views/CrmView/client.tsx +10 -11
  119. package/src/views/SupportDashboardView/client.tsx +1 -1
  120. package/src/views/TicketDetailView/client.tsx +2 -2
  121. package/src/views/TicketInboxView/client.tsx +10 -5
  122. package/src/views/TicketingSettingsView/client.tsx +9 -7
  123. package/src/views/shared/config.ts +14 -80
  124. package/src/views/shared/index.ts +3 -1
  125. package/src/views/shared/locales/en.json +2 -2
  126. package/src/views/shared/locales/fr.json +2 -2
  127. package/dist/components/RichTextEditor/index.cjs +0 -279
  128. package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
  129. package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
  130. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
  131. package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
  132. package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
  133. package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
  134. package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
  135. package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
  136. package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
  137. package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
  138. package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
  139. package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
  140. package/dist/components/TicketConversation/config.cjs +0 -44
  141. package/dist/components/TicketConversation/constants.cjs +0 -127
  142. package/dist/components/TicketConversation/context.cjs +0 -13
  143. package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
  144. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
  145. package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
  146. package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
  147. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
  148. package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
  149. package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -78
  150. package/dist/components/TicketConversation/index.cjs +0 -1114
  151. package/dist/components/TicketConversation/types.cjs +0 -2
  152. package/dist/components/TicketConversation/utils.cjs +0 -27
  153. package/dist/views/BillingView/client.cjs +0 -469
  154. package/dist/views/BillingView/index.cjs +0 -34
  155. package/dist/views/ChatView/client.cjs +0 -301
  156. package/dist/views/ChatView/index.cjs +0 -34
  157. package/dist/views/CrmView/client.cjs +0 -458
  158. package/dist/views/CrmView/index.cjs +0 -34
  159. package/dist/views/EmailTrackingView/client.cjs +0 -171
  160. package/dist/views/EmailTrackingView/index.cjs +0 -34
  161. package/dist/views/ImportConversationView/client.cjs +0 -238
  162. package/dist/views/ImportConversationView/index.cjs +0 -34
  163. package/dist/views/LogsView/client.cjs +0 -148
  164. package/dist/views/LogsView/index.cjs +0 -32
  165. package/dist/views/NewTicketView/client.cjs +0 -213
  166. package/dist/views/NewTicketView/index.cjs +0 -32
  167. package/dist/views/PendingEmailsView/client.cjs +0 -471
  168. package/dist/views/PendingEmailsView/index.cjs +0 -34
  169. package/dist/views/SupportDashboardView/client.cjs +0 -437
  170. package/dist/views/SupportDashboardView/index.cjs +0 -34
  171. package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
  172. package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
  173. package/dist/views/TicketDetailView/client.cjs +0 -1412
  174. package/dist/views/TicketDetailView/constants.cjs +0 -20
  175. package/dist/views/TicketDetailView/helpers.cjs +0 -23
  176. package/dist/views/TicketDetailView/index.cjs +0 -34
  177. package/dist/views/TicketDetailView/types.cjs +0 -2
  178. package/dist/views/TicketInboxView/client.cjs +0 -359
  179. package/dist/views/TicketInboxView/index.cjs +0 -32
  180. package/dist/views/TicketingSettingsView/client.cjs +0 -740
  181. package/dist/views/TicketingSettingsView/index.cjs +0 -34
  182. package/dist/views/TimeDashboardView/client.cjs +0 -180
  183. package/dist/views/TimeDashboardView/index.cjs +0 -34
  184. package/dist/views/shared/AdminViewHeader.cjs +0 -68
  185. package/dist/views/shared/ErrorBoundary.cjs +0 -55
  186. package/dist/views/shared/Skeleton.cjs +0 -77
  187. package/dist/views/shared/StatusPill.cjs +0 -47
  188. package/dist/views/shared/adminTokens.cjs +0 -34
  189. package/dist/views/shared/config.cjs +0 -44
  190. package/dist/views/shared/dateLocale.cjs +0 -5
  191. package/dist/views/shared/index.cjs +0 -72
  192. package/dist/views/shared/sla.cjs +0 -37
  193. package/dist/views.cjs +0 -86
  194. package/dist/views.d.cts +0 -13
  195. package/src/__tests__/aiSummaryRendering.test.ts +0 -21
  196. package/src/__tests__/authResponses.test.ts +0 -69
  197. package/src/__tests__/capabilities.test.ts +0 -71
  198. package/src/__tests__/generateTrackingToken.test.ts +0 -67
  199. package/src/__tests__/integration/ai-agent.test.ts +0 -46
  200. package/src/__tests__/integration/auth.test.ts +0 -43
  201. package/src/__tests__/integration/automation-rules.test.ts +0 -68
  202. package/src/__tests__/integration/buildTestPayload.ts +0 -52
  203. package/src/__tests__/integration/channels.test.ts +0 -52
  204. package/src/__tests__/integration/core-behaviors.test.ts +0 -81
  205. package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
  206. package/src/__tests__/integration/digest.test.ts +0 -48
  207. package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
  208. package/src/__tests__/integration/isolation.test.ts +0 -81
  209. package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
  210. package/src/__tests__/integration/nps.test.ts +0 -37
  211. package/src/__tests__/integration/per-team-sla.test.ts +0 -44
  212. package/src/__tests__/integration/push.test.ts +0 -75
  213. package/src/__tests__/integration/sanitization.test.ts +0 -35
  214. package/src/__tests__/integration/sla-pause.test.ts +0 -39
  215. package/src/__tests__/integration/smoke.test.ts +0 -26
  216. package/src/__tests__/integration/snooze.test.ts +0 -68
  217. package/src/__tests__/integration/teams.test.ts +0 -38
  218. package/src/__tests__/rateLimiter.test.ts +0 -83
  219. package/src/__tests__/sanitizeHtml.test.ts +0 -165
  220. package/src/__tests__/sla.test.ts +0 -258
  221. package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
  222. package/src/__tests__/webhookSecurity.test.ts +0 -58
package/README.md CHANGED
@@ -1,98 +1,115 @@
1
- <div align="center" style="background: linear-gradient(135deg, #1f8a5b 0%, #16a34a 50%, #0d9488 100%); padding: 50px 40px; border-radius: 12px; color: white; margin-bottom: 40px;">
2
- <h1 style="margin: 0 0 15px 0; font-size: 42px; font-weight: 700; letter-spacing: -0.5px;">payload-support</h1>
3
- <p style="margin: 0 auto; font-size: 18px; opacity: 0.95; max-width: 640px; line-height: 1.6;">A complete, self-hosted support &amp; ticketing system for Payload CMS 3 + Next.js β€” tickets, SLA, AI assists and an autonomous AI agent, live chat, a full client portal, time tracking &amp; invoicing, and a visual automation-rules engine. No third-party support SaaS required.</p>
4
- </div>
1
+ # @consilioweb/payload-support
2
+
3
+ > A self-hosted support and ticketing system for Payload CMS 3 + Next.js: tickets, SLA, live chat, a client portal, time tracking and invoicing, AI assists and an automation-rules engine.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@consilioweb/payload-support.svg)](https://www.npmjs.com/package/@consilioweb/payload-support)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![payload](https://img.shields.io/badge/payload-%5E3.37-blue.svg)](https://payloadcms.com)
8
+
9
+ ## About
10
+
11
+ `supportPlugin()` turns a Payload 3 admin into a helpdesk: it injects 14 to 25 collections
12
+ (depending on the feature flags), 13 admin views and around 60 REST endpoints covering the whole
13
+ ticket lifecycle β€” intake, assignment, SLA, replies by email or live chat, resolution, CSAT and
14
+ billing. It is aimed at agencies and product teams who already run Payload and do not want to pay
15
+ for, or export their customers' data to, a third-party support SaaS.
16
+
17
+ Two things it deliberately does *not* do: it does not mount the client portal for you (the portal
18
+ ships as a template you copy into your own `app/` directory β€” see
19
+ [Mounting the client portal](#mounting-the-client-portal)), and it does not bundle an LLM SDK
20
+ (install `@anthropic-ai/sdk` yourself if you enable the AI features).
21
+
22
+ ## Table of Contents
23
+
24
+ - [Features](#features)
25
+ - [Installation](#installation)
26
+ - [Quick Start](#quick-start)
27
+ - [Mounting the client portal](#mounting-the-client-portal)
28
+ - [Configuration](#configuration)
29
+ - [Deployment adapters](#deployment-adapters)
30
+ - [API Endpoints](#api-endpoints)
31
+ - [Webhooks](#webhooks)
32
+ - [Collections](#collections)
33
+ - [Package Exports](#package-exports)
34
+ - [Requirements](#requirements)
35
+ - [Security](#security)
36
+ - [Performance](#performance)
37
+ - [Troubleshooting](#troubleshooting)
38
+ - [FAQ](#faq)
39
+ - [Upgrading](#upgrading)
40
+ - [Contributing](#contributing)
41
+ - [Changelog](#changelog)
42
+ - [Support](#support)
43
+ - [License](#license)
44
+
45
+ ## Features
46
+
47
+ - **Ticketing** β€” statuses, priorities, categories, tags, merge, split, snooze, scheduled replies,
48
+ internal notes, collaborators, and an append-only activity log (`create` and `update` are denied
49
+ to everyone; only hooks write to it).
50
+ - **Agent inbox** β€” a keyboard-driven inbox view, bulk actions, macros, canned responses, and typing
51
+ and presence indicators.
52
+ - **SLA** β€” per-team or default policies, business-hours deadlines, pause-on-hold, escalation, and a
53
+ `sla-check` endpoint that lists what is breaching.
54
+ - **Automation** β€” an `automation-rules` collection (event β†’ conditions β†’ actions) and optional
55
+ round-robin assignment.
56
+ - **AI assists** β€” sentiment, reply suggestion, multi-style rewrite, per-ticket cached synthesis, a
57
+ knowledge-base chatbot, a client-intelligence summary, and an autonomous agent that either answers
58
+ from the knowledge base or escalates. Anthropic, or `provider: 'ollama'` to reach a self-hosted
59
+ gateway through the same SDK pointed at your own `OLLAMA_API_URL`.
60
+ - **Omnichannel** β€” inbound and outbound email with open tracking, live chat (SSE in the portal
61
+ widget, polling in the shipped agent console; the portal widget stays hidden until
62
+ `NEXT_PUBLIC_ENABLE_CHAT_WIDGET=true`), and a normalized inbound webhook for WhatsApp /
63
+ Messenger.
64
+ - **Client portal** β€” password login, email 2FA, Google OAuth, ticket list and detail, FAQ, profile,
65
+ GDPR data export and account deletion. Shipped as a template you copy (see below).
66
+ - **Time tracking and billing** β€” timer and manual entries, a per-ticket rollup, a time dashboard,
67
+ per-project pre-billing, and an invoice rendered as HTML or PDF.
68
+ - **Reporting** β€” dashboard KPIs, CSAT and NPS, email statistics, CSV export.
69
+ - **Notifications** β€” admin notifications, Web Push (VAPID), and daily/weekly digest emails.
70
+ - **Webhooks** β€” HMAC-SHA256 signed outbound deliveries on ticket events.
71
+ - **i18n** β€” French and English catalogs across the admin views, selectable per user.
72
+
73
+ ## Installation
5
74
 
6
- <div align="center">
7
-
8
- [![npm version](https://img.shields.io/npm/v/@consilioweb/payload-support?color=1f8a5b&label=npm)](https://www.npmjs.com/package/@consilioweb/payload-support)
9
- [![MIT License](https://img.shields.io/badge/license-MIT-1f8a5b)](LICENSE)
10
- [![Node.js](https://img.shields.io/badge/node-18+-1f8a5b)](https://nodejs.org)
11
- [![Payload](https://img.shields.io/badge/payload-3.x-1f8a5b)](https://payloadcms.com)
12
- [![Tests](https://img.shields.io/badge/tests-102%20passing-1f8a5b)](src/__tests__)
13
- [![TypeScript](https://img.shields.io/badge/typescript-strict-1f8a5b)](https://www.typescriptlang.org)
14
-
15
- </div>
16
-
17
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
18
-
19
- ## πŸ“‘ Table of Contents
20
-
21
- - [Features](#-features)
22
- - [Quick Start](#-quick-start)
23
- - [Installation](#-installation)
24
- - [Usage](#-usage)
25
- - [API Reference](#-api-reference)
26
- - [Configuration](#-configuration)
27
- - [Performance](#-performance)
28
- - [Examples](#-examples)
29
- - [FAQ](#-faq)
30
- - [Troubleshooting](#-troubleshooting)
31
- - [Security](#-security)
32
- - [Contributing](#-contributing)
33
- - [Changelog](#-changelog)
34
- - [Roadmap](#-roadmap)
35
- - [Support](#-support)
36
- - [License](#-license)
37
-
38
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
39
-
40
- ## ✨ Features
41
-
42
- <table>
43
- <tr>
44
- <td width="50%">
45
-
46
- **🎫 Complete ticketing**
47
-
48
- Statuses, priorities, categories, tags, merge/split, snooze, scheduled replies, internal notes, an immutable activity log and a keyboard-driven Superhuman-style inbox.
49
-
50
- </td>
51
- <td width="50%">
52
-
53
- **⏱️ SLA & automation**
54
-
55
- SLA policies (business hours + pause-on-hold), escalation, macros, round-robin, and a **visual automation-rules engine** (conditions β†’ actions).
56
-
57
- </td>
58
- </tr>
59
- <tr>
60
- <td width="50%">
61
-
62
- **πŸ€– Built-in AI**
63
-
64
- Sentiment, reply suggestions, multi-style rewriting, cached synthesis, a KB chatbot, and an **autonomous AI agent** that answers or escalates. Anthropic, OpenAI or self-hosted Ollama.
65
-
66
- </td>
67
- <td width="50%">
68
-
69
- **πŸ’¬ Omnichannel & portal**
70
-
71
- Inbound/outbound email, live chat (SSE), widget, a full **client portal** (auth, 2FA, Google OAuth), knowledge base and deflection.
72
-
73
- </td>
74
- </tr>
75
- <tr>
76
- <td width="50%">
77
-
78
- **πŸ’Ά Time tracking & billing**
75
+ ```bash
76
+ pnpm add @consilioweb/payload-support
77
+ # or: npm install @consilioweb/payload-support
78
+ # or: yarn add @consilioweb/payload-support
79
+ ```
79
80
 
80
- Timer, manual entries, time dashboard, per-project pre-billing and a **print-ready invoice** (HTML β†’ PDF).
81
+ **Peer dependencies** β€” all six are required, none is optional:
81
82
 
82
- </td>
83
- <td width="50%">
83
+ ```bash
84
+ pnpm add payload@^3.37.0 @payloadcms/next@^3.37.0 next@^15.2.9 react@^19 react-dom@^19 lucide-react@">=0.300.0"
85
+ ```
84
86
 
85
- **πŸ“Š Reporting & privacy**
87
+ - `payload@^3.37.0` β€” the Google OAuth endpoint imports `jwtSign` from the `payload` barrel, which
88
+ only exists from 3.37.0 on. Below that, the missing named export fails the whole
89
+ `payload.config.ts`, not just that endpoint.
90
+ - `@payloadcms/next` and `next` β€” the 13 admin views are registered unless you pass
91
+ `skipViews: true`, and each imports `DefaultTemplate` from `@payloadcms/next/templates` plus
92
+ `next/navigation` / `next/link` statically. Both are already present in any Payload 3 admin app.
93
+ - `lucide-react` β€” imported statically by `PendingEmailsView` and `TicketingSettingsView`. (The
94
+ third importer, `src/portal/LiveChat.tsx`, is a leftover: nothing in the shipped portal mounts
95
+ it.)
96
+
97
+ **AI runtime dependency** β€” the AI features load `@anthropic-ai/sdk` through a runtime
98
+ `require`/dynamic `import` and it is marked `external` at build time. It is intentionally not a
99
+ dependency of this package, so install it yourself if you use `features.ai`, the chatbot, the
100
+ autonomous agent, ticket synthesis, client intelligence or the AI conversation import:
86
101
 
87
- Real-time dashboard, **CSAT + NPS**, email tracking, HMAC-signed webhooks, notification digests, CSV exports. 100% self-hostable.
102
+ ```bash
103
+ pnpm add @anthropic-ai/sdk
104
+ ```
88
105
 
89
- </td>
90
- </tr>
91
- </table>
106
+ After adding admin components, regenerate the import map:
92
107
 
93
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
108
+ ```bash
109
+ pnpm payload generate:importmap
110
+ ```
94
111
 
95
- ## πŸš€ Quick Start
112
+ ## Quick Start
96
113
 
97
114
  ```ts
98
115
  // payload.config.ts
@@ -103,8 +120,7 @@ export default buildConfig({
103
120
  plugins: [
104
121
  supportPlugin({
105
122
  features: { ai: true, sla: true, timeTracking: true, chat: true },
106
- ai: { provider: 'anthropic', model: 'claude-haiku-4-5-20251001' },
107
- locale: 'fr',
123
+ ticketNumber: { prefix: 'TK-', padding: 6 },
108
124
  }),
109
125
  ],
110
126
  })
@@ -115,55 +131,319 @@ pnpm payload generate:importmap
115
131
  pnpm dev
116
132
  ```
117
133
 
118
- Open `/admin/support/inbox` for the agent inbox, or `/support` for the client portal. The plugin injects collections, API endpoints and admin views automatically β€” no external SaaS.
119
-
120
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
121
-
122
- ## πŸ“¦ Installation
123
-
124
- ### npm
125
- ```bash
126
- npm install @consilioweb/payload-support
134
+ Open `/admin/support/inbox` for the agent inbox and `/admin/support/settings` to pick the AI
135
+ provider, the sender addresses, the SLA targets and the runtime feature flags.
136
+
137
+ > **The client portal is not a route the plugin mounts.** `/support` returns 404 until you copy the
138
+ > portal template into your own `app/` directory β€” see
139
+ > [Mounting the client portal](#mounting-the-client-portal). Everything the portal talks to (the REST
140
+ > endpoints, the auth/2FA/OAuth endpoints, the collections) *is* installed by the plugin; only the
141
+ > Next.js pages have to live in your app.
142
+
143
+ ## Mounting the client portal
144
+
145
+ The portal ships as a **template you copy**, not as an importable subpath β€” and that is a
146
+ constraint, not an oversight: `src/portal/auth/layout.tsx` and three of its pages import
147
+ `@payload-config`, an alias that only exists inside your application, and `src/portal/layout.tsx`
148
+ renders its own `<html>`/`<body>` as a Next.js **root layout**. Neither can be resolved from
149
+ `node_modules`, so there is no `./portal` export and `plugin.ts` registers no Next route.
150
+
151
+ The sources are published in the tarball (`files` includes `src`), so copy them out of
152
+ `node_modules` into a route group of your own `app/` directory. Two directories have to be
153
+ renamed on the way in β€” the shipped names are plain folders, not Next.js route syntax β€” and
154
+ `layout.tsx` has to sit at the **group** level, because it renders its own `<html>`/`<body>`
155
+ and only one root layout per group may do that (the same convention `create-payload-app`
156
+ uses for its `(payload)` / `(frontend)` groups):
157
+
158
+ ```text
159
+ src/app/
160
+ └── (support)/
161
+ β”œβ”€β”€ layout.tsx ← src/portal/layout.tsx (root layout of the group)
162
+ └── support/
163
+ β”œβ”€β”€ page.tsx ← src/portal/page.tsx β†’ /support
164
+ β”œβ”€β”€ login/ register/ forgot-password/ reset-password/
165
+ └── (auth)/ ← src/portal/auth/ (route group: no URL segment)
166
+ β”œβ”€β”€ layout.tsx (auth guard + header + chatbot and chat widgets)
167
+ β”œβ”€β”€ dashboard/ faq/ profile/
168
+ └── tickets/
169
+ β”œβ”€β”€ new/
170
+ └── [id]/ ← src/portal/auth/tickets/detail/ (the page reads `params.id`)
127
171
  ```
128
172
 
129
- ### yarn
130
173
  ```bash
131
- yarn add @consilioweb/payload-support
174
+ mkdir -p 'src/app/(support)/support'
175
+ cp -R node_modules/@consilioweb/payload-support/src/portal/. 'src/app/(support)/support/'
176
+ cd 'src/app/(support)'
177
+ mv support/layout.tsx layout.tsx
178
+ mv support/auth 'support/(auth)'
179
+ mv 'support/(auth)/tickets/detail' 'support/(auth)/tickets/[id]'
132
180
  ```
133
181
 
134
- ### pnpm
135
- ```bash
136
- pnpm add @consilioweb/payload-support
137
- ```
182
+ That yields `/support`, `/support/login`, `/support/register`, `/support/forgot-password`,
183
+ `/support/reset-password` and, behind the auth layout, `/support/dashboard`, `/support/faq`,
184
+ `/support/profile`, `/support/tickets/new` and `/support/tickets/<id>` β€” exactly the paths the
185
+ plugin's notification emails and the portal's own links point to, so the renames are
186
+ mandatory, not cosmetic.
138
187
 
139
- **Peer dependencies:** `payload@^3`, `react@^18 || ^19`, `react-dom@^18 || ^19`, `next@^14 || ^15 || ^16`. `lucide-react` and the `@payloadcms/*` packages are optional depending on the features you enable. After adding admin components, run `pnpm payload generate:importmap`.
188
+ What your app must provide for those pages to work:
140
189
 
141
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
190
+ | Requirement | Why |
191
+ |---|---|
192
+ | The `@payload-config` alias | The authenticated pages call `getPayload({ config })` directly. Standard in a `create-payload-app` project. |
193
+ | **Tailwind CSS** | 28 of the 35 portal files are styled with Tailwind utility classes only. Without Tailwind the portal renders unstyled. |
194
+ | `@payloadcms/richtext-lexical` | `auth/faq/page.tsx` renders knowledge-base entries with `RichText` from `@payloadcms/richtext-lexical/react`. It is **not** a peer dependency of this plugin β€” add it yourself if you mount the FAQ page. |
195
+ | `NEXT_PUBLIC_SERVER_URL` | Email and portal links are built from it. |
196
+ | `NEXT_PUBLIC_SUPPORT_PHONE` | The ticket detail page prints a support phone number; without this variable it prints the placeholder `01 23 45 67 89` to your clients. |
142
197
 
143
- ## πŸ’» Usage
198
+ `lucide-react` is **not** needed by the pages above: the only portal file importing it is
199
+ `src/portal/LiveChat.tsx`, which the shipped `(auth)` layout never mounts. It stays a required peer
200
+ for the admin views.
144
201
 
145
- ### Basic
202
+ The `(auth)` layout mounts two widgets. `ChatbotWidget` is always rendered; `ChatWidget` β€” the
203
+ floating live-chat FAB β€” returns `null` unless the host app sets
204
+ `NEXT_PUBLIC_ENABLE_CHAT_WIDGET=true`, whatever `features.chat` says.
146
205
 
147
- ```ts
148
- // All features are on by default β€” turn off what you don't need:
149
- supportPlugin({ features: { chat: false, pendingEmails: false }, locale: 'en' })
206
+ Because it is a copy, the portal is yours to restyle and re-brand; the trade-off is that
207
+ plugin upgrades do not update it. Diff it against
208
+ `node_modules/@consilioweb/payload-support/src/portal` after a version bump.
209
+
210
+ ## Configuration
211
+
212
+ ### Plugin options
213
+
214
+ | Option | Type | Default | Description |
215
+ |---|---|---|---|
216
+ | `features` | `SupportFeatures` | see below | Build-time feature flags β€” decide which collections, endpoints and views exist. |
217
+ | `rateLimitStore` | `RateLimitStore \| 'payload'` | in-memory | Shared storage for the endpoint rate limits. `'payload'` adds the `support-rate-limits` collection. |
218
+ | `ticketNumber` | `{ prefix?, padding? }` | `{ prefix: 'TK-', padding: 4 }` | Sequential ticket-number formatting, backed by an atomic counter. |
219
+ | `capabilities` | `SupportCapabilities` | β€” | Host adapters for SMS, inbound email, digests, AI titles/summaries, detailed billing, volunteering, thread cleanup and project suggestions. See [Deployment adapters](#deployment-adapters). |
220
+ | `basePath` | `string` | `'/support'` | Prefix of the admin view routes (`/admin<basePath>/inbox`, …). The conversation-import view is the exception: it is registered at the fixed `/admin/import-conversation`. |
221
+ | `userCollectionSlug` | `string` | `'users'` | The staff/agent auth collection. |
222
+ | `allowedEmailDomains` | `string[]` | β€” | Restricts Google OAuth **auto-registration** to these domains. Existing accounts are unaffected. |
223
+ | `collectionSlugs` | `object` | see [Collections](#collections) | Per-collection slug overrides. |
224
+ | `notificationSlug` | `string` | `'admin-notifications'` | Collection the plugin writes admin notifications to. |
225
+ | `conversationComponent` | `string` | `'@consilioweb/payload-support/components/TicketConversation'` | Import path of the conversation UI field rendered on a ticket. |
226
+ | `projectCollectionSlug` | `string` | β€” | When set, adds a `project` relationship to tickets (and enables per-project billing). |
227
+ | `documentsCollectionSlug` | `string` | β€” | When set, adds quote/invoice upload fields to tickets. |
228
+ | `skipCollections` | `boolean` | `false` | Do not inject the collections. |
229
+ | `skipViews` | `boolean` | `false` | Do not inject the admin views. |
230
+ | `skipEndpoints` | `boolean` | `false` | Do not inject the REST endpoints. |
231
+
232
+ ### Options accepted but not read
233
+
234
+ Four keys are part of `SupportPluginConfig` but no code path reads them. They are listed here so you
235
+ do not configure them and wonder why nothing changes:
236
+
237
+ | Option | What actually decides | Where to set it |
238
+ |---|---|---|
239
+ | `ai` (`AIProviderConfig`) | `settings.ai.provider` / `settings.ai.model` | Admin β†’ Support β†’ Settings, or `POST /api/support/settings`. Defaults to `anthropic` / `claude-haiku-4-5-20251001`. |
240
+ | `email` (`EmailConfig`) | `settings.email.*`, falling back to `SUPPORT_EMAIL` / `SUPPORT_REPLY_TO` | Same settings view, or the environment. |
241
+ | `locale` | The per-user preference | `GET`/`POST /api/support/user-prefs` (defaults to `fr`). |
242
+ | `navGroup` | β€” | Nowhere: each collection hard-codes its own `admin.group`. 20 sit under `Support`, three under `Gestion` (`canned-responses`, `ticket-activity-log`, `time-entries`), and `support-counters` / `support-rate-limits` declare none β€” they are `hidden` anyway. |
243
+
244
+ ### Build-time feature flags
245
+
246
+ `supportPlugin({ features })` accepts `SupportFeatures`. Every flag defaults to `true` except
247
+ `roundRobin` and `customStatuses`; `autoCloseDays` defaults to `7`.
248
+
249
+ | Flag | Default | Turning it off removes |
250
+ |---|---|---|
251
+ | `ai` | `true` | `client-summaries`, `/support/ai`, `/support/ai-agent`, `/support/client-intelligence`, `/support/ticket-synthesis` |
252
+ | `timeTracking` | `true` | `time-entries`, `/support/billing`, `/support/billing/invoice`, the Time dashboard view |
253
+ | `chat` | `true` | `chat-messages`, the four chat endpoints, the Chat view |
254
+ | `emailTracking` | `true` | `email-logs`, `/support/email-stats`, `/support/track-open`, the Tracking view |
255
+ | `pendingEmails` | `true` | `pending-emails`, `/support/pending-emails/:id/process`, the Pending emails view |
256
+ | `sla` | `true` | `sla-policies`, `/support/sla-check` |
257
+ | `webhooks` | `true` | `webhook-endpoints` (and therefore every outbound delivery) |
258
+ | `macros` | `true` | `macros`, `/support/apply-macro` |
259
+ | `authLogs` | `true` | `auth-logs` |
260
+ | `customStatuses` | `false` | *(off by default)* `ticket-statuses`, `/support/statuses` |
261
+ | `autoClose` | `true` | `/support/auto-close`, `/support/send-reminder` |
262
+ | `autoCloseDays` | `7` | β€” (projection of `settings.autoClose.daysBeforeClose`) |
263
+ | `snooze` | `true` | `/support/process-snooze` |
264
+ | `scheduledReplies` | `true` | `/support/process-scheduled` |
265
+ | `satisfaction` | `true` | `/support/satisfaction` |
266
+ | `merge` | `true` | `/support/merge-tickets` |
267
+ | `splitTicket` | `true` | `/support/split-ticket` |
268
+ | `bulkActions` | `true` | `/support/bulk-action` |
269
+ | `collisionDetection` | `true` | `/support/typing`, `/support/presence` |
270
+ | `signatures` | `true` | `/support/signature` |
271
+ | `chatbot` | `true` | `/support/chatbot` |
272
+ | `roundRobin` | `false` | *(off by default)* `/support/round-robin-config` |
273
+ | `canned`, `externalMessages`, `clientHistory`, `activityLog` | `true` | Nothing at build time β€” they exist as runtime flags (below) and only hide UI sections. |
274
+ | `commandPalette`, `knowledgeBase` | `true` | Nothing β€” no code reads either flag, at build time or at runtime. |
275
+
276
+ ### Build-time vs runtime feature flags
277
+
278
+ Two distinct things share the word "features", and they act at different moments:
279
+
280
+ | | `supportPlugin({ features })` | Runtime flags (Settings view) |
281
+ |---|---|---|
282
+ | Type | `SupportFeatures` (28 keys) | `TicketingFeatures` (16 keys) |
283
+ | Decides | which collections and endpoints **exist** | which UI sections are **shown**, plus round-robin and auto-close |
284
+ | Changed by | editing `payload.config.ts` + redeploy | an admin, in the ticketing settings view |
285
+ | Stored in | code | the `features` block of the `support-settings` preference row |
286
+
287
+ Turning a feature off at build time removes its collection and endpoints, so the matching runtime
288
+ flag has nothing left to show. Runtime flags only ever narrow what a build-time-enabled feature
289
+ exposes.
290
+
291
+ Runtime flags are read through `GET /api/support/settings` and written through
292
+ `POST /api/support/settings` (admin-only). They apply to every browser and to server-side code β€” the
293
+ auto-assign hook and the auto-close cron read the same values. `localStorage` is still written, but
294
+ only as a cache so an admin screen stays usable when the settings call fails; it is never
295
+ authoritative.
296
+
297
+ Two flags are projections, deliberately not stored twice: `features.autoClose` mirrors
298
+ `settings.autoClose.enabled`, and `features.autoCloseDays` mirrors
299
+ `settings.autoClose.daysBeforeClose`.
300
+
301
+ > **Upgrading from ≀ 2.0.1** β€” flags used to live in each browser's `localStorage`
302
+ > (`ticketing_features`). The first admin view loaded after the upgrade pushes that browser's values
303
+ > to the server, once, if the server has none yet; from then on the server wins. Nothing to run by
304
+ > hand, nothing is lost. If several machines disagree, the first one to load seeds the server and the
305
+ > others adopt its copy.
306
+
307
+ ### Environment variables
308
+
309
+ | Variable | Required | Description |
310
+ |---|---|---|
311
+ | `PAYLOAD_SECRET` | yes | Payload secret. Also signs the 2FA tokens and the email tracking pixels; the 2FA endpoint refuses to operate without it. |
312
+ | `NEXT_PUBLIC_SERVER_URL` | yes | Public URL used to build every email and portal link. |
313
+ | `CRON_SECRET` | for the cron endpoints | Expected value of the `x-cron-secret` header. |
314
+ | `ANTHROPIC_API_KEY` | with the `anthropic` provider | Anthropic API key. |
315
+ | `OLLAMA_API_URL` | with the `ollama` provider | Base URL of your own AI gateway, used as the SDK `baseURL`. **Mandatory** β€” there is no default host, and the AI paths throw `500 "provider 'ollama' requires OLLAMA_API_URL"` rather than sending ticket content anywhere you did not choose. |
316
+ | `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` | for portal OAuth | Google sign-in on the portal. |
317
+ | `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | for Web Push | Generate with `npx web-push generate-vapid-keys`. |
318
+ | `VAPID_SUBJECT` | optional | Push contact (`mailto:` or URL). Defaults to `mailto:support@example.com`. |
319
+ | `SUPPORT_EMAIL` / `SUPPORT_REPLY_TO` | optional | Fallback sender / reply-to addresses when the settings row leaves them empty. |
320
+ | `SUPPORT_CLIENT_BCC` | optional | Archive mailbox blind-copied on client-facing emails. Use a dedicated address, not `support@`, or the inbound anti-loop guard will re-create tickets from the copies. |
321
+ | `SUPPORT_TEAM_SCOPING` | optional | `1` or `true` to scope agents to their own team's tickets. |
322
+ | `SUPPORT_WEBHOOK_SECRET` | for machine imports | Expected value of the `x-webhook-secret` header on `POST /api/support/import-conversation`. |
323
+ | `CHANNELS_WEBHOOK_SECRET` | for social channels | Expected value of the `x-channel-secret` header on `POST /api/support/channels/webhook`. |
324
+ | `WHATSAPP_TOKEN` / `WHATSAPP_PHONE_ID` | for WhatsApp replies | Outbound WhatsApp credentials. |
325
+ | `MESSENGER_PAGE_TOKEN` | for Messenger replies | Outbound Messenger page token. |
326
+ | `CONTACT_EMAIL` | optional | Address treated as "the admin" when importing an existing email conversation. |
327
+ | `NEXT_PUBLIC_ENABLE_CHAT_WIDGET` | to show the portal chat widget | `'true'` mounts the floating live-chat widget in the portal `(auth)` layout. Anything else β€” including leaving it unset β€” makes `ChatWidget` return `null`, so `features.chat: true` on its own displays nothing to clients. |
328
+ | `NEXT_PUBLIC_SUPPORT_PHONE` | for the portal ticket page | Support phone number shown on the portal ticket detail page. Set it: the fallback is the placeholder `01 23 45 67 89`. |
329
+ | `NEXT_PUBLIC_SUPPORT_SEND_ALIASES` | optional | Comma-separated "send as" identities offered in the agent composer. Empty by default, and then the composer offers only the agent themselves. |
330
+ | `NEXT_PUBLIC_SMTP_HOST` / `NEXT_PUBLIC_SMTP_PORT` | optional | Shown read-only in the Settings view. Informational only β€” mail is sent through `payload.sendEmail`, so these change nothing about delivery. |
331
+
332
+ ### Cron jobs
333
+
334
+ Four endpoints are meant to be called by a scheduler and are guarded by the `x-cron-secret` header.
335
+ Note that `auto-close` is a **GET**:
336
+
337
+ ```bash
338
+ # Hourly
339
+ curl -X GET https://your-app/api/support/auto-close -H "x-cron-secret: $CRON_SECRET"
340
+ curl -X POST https://your-app/api/support/process-snooze -H "x-cron-secret: $CRON_SECRET"
341
+ curl -X POST https://your-app/api/support/process-scheduled -H "x-cron-secret: $CRON_SECRET"
342
+
343
+ # Daily / weekly
344
+ curl -X POST https://your-app/api/support/process-digests -H "x-cron-secret: $CRON_SECRET" \
345
+ -H "Content-Type: application/json" -d '{"frequency":"daily"}'
150
346
  ```
151
347
 
152
- ### Advanced
348
+ ### Automation rules
349
+
350
+ `automation-rules` is edited in the admin, no code required. A rule has a trigger
351
+ (`ticket_created`, `ticket_updated` or `ticket_status_changed`), a match mode (`all` / `any`), a list
352
+ of conditions (field, `equals` / `not_equals` / `contains`, value) and a list of actions
353
+ (`set_status`, `set_priority`, `set_category`, `assign`, `add_tag`). For example: trigger
354
+ `ticket_created`, condition `category equals bug`, action `set_priority = urgent`.
355
+
356
+ ## Deployment adapters
357
+
358
+ The plugin owns the generic support workflow; anything provider-specific stays in your application
359
+ and is injected through `capabilities`. Every key is optional and a missing adapter simply disables
360
+ the corresponding behaviour (`inboundEmail`, `projectSuggestions` and `aiTitles` also gate the
361
+ endpoints they need).
153
362
 
154
363
  ```ts
155
364
  supportPlugin({
156
- features: { ai: true, sla: true, roundRobin: true, webhooks: true, snooze: true },
157
- ai: { provider: 'ollama', model: 'qwen2.5', baseUrl: process.env.OLLAMA_API_URL },
158
- email: { fromName: 'Support ACME', fromAddress: 'support@acme.com', replyTo: 'support@acme.com' },
159
- allowedEmailDomains: ['acme.com'], // restrict OAuth auto-registration
160
- collectionSlugs: { tickets: 'support-tickets' }, // slug overrides
161
- navGroup: 'Support',
162
- basePath: '/support',
365
+ rateLimitStore: 'payload',
366
+ capabilities: {
367
+ sms: {
368
+ adapter: {
369
+ isConfigured: () => Boolean(process.env.SMS_PROVIDER_ACCOUNT),
370
+ send: async ({ message, to }) => mySmsProvider.send({ message, to }),
371
+ },
372
+ },
373
+ inboundEmail: {
374
+ secret: process.env.SUPPORT_WEBHOOK_SECRET,
375
+ secretHeader: 'x-webhook-secret',
376
+ handle: handleInboundSupportEmail,
377
+ },
378
+ detailedBilling: true,
379
+ volunteering: true,
380
+ },
163
381
  })
164
382
  ```
165
383
 
166
- ### Run the AI agent on a ticket
384
+ | Capability | Shape | Effect |
385
+ |---|---|---|
386
+ | `sms` | `{ adapter, buildMessage? }` | Sends an SMS alongside the client notification. |
387
+ | `digests` | `boolean` | **Inert** β€” no code reads it. `notification-queue` and `POST /api/support/process-digests` are registered unconditionally. |
388
+ | `inboundEmail` | `{ handle, secret?, secretHeader? }` | Registers `POST /api/support-webhook/inbound-email`, validated and rate-limited by the plugin. |
389
+ | `aiTitles` | `{ generate }` | Registers `POST /api/support/ticket-title` and `POST /api/support/generate-missing-titles`, and adds the title fields on clients. |
390
+ | `aiSummaries` | `{ generate }` | Overrides the default resolved-ticket synthesis generator. |
391
+ | `detailedBilling` | `boolean` | Adds the billing-line fields and their invoicing state. |
392
+ | `volunteering` | `boolean` | Adds pro-bono tracking and its estimated value. |
393
+ | `threadCleanup` | `{ clean }` | Best-effort cleanup of noisy messages after an inbound client reply. |
394
+ | `projectSuggestions` | `{ suggest }` | Registers `POST /api/support/suggest-projects`. |
395
+
396
+ ## API Endpoints
397
+
398
+ All paths are relative to `/api`. "Staff" means a user of `userCollectionSlug` (default `users`);
399
+ "client" means a `support-clients` user. The *Flag* column names the build-time feature flag that has
400
+ to stay enabled for the endpoint to be registered.
401
+
402
+ ### Tickets and agent tools
403
+
404
+ | Method | Path | Access | Flag | Purpose |
405
+ |---|---|---|---|---|
406
+ | `GET` | `/support/search` | staff | β€” | Global search over tickets, messages, clients and the knowledge base. |
407
+ | `GET` | `/support/kb/search` | public | β€” | Knowledge-base search (used by the portal and the chatbot). |
408
+ | `GET` | `/support/statuses` | authenticated | `customStatuses` | Ticket statuses, sorted. |
409
+ | `POST` | `/support/bulk-action` | staff | `bulkActions` | Apply one action to many tickets. |
410
+ | `POST` | `/support/merge-tickets` | staff | `merge` | Merge a source ticket into a target. |
411
+ | `POST` | `/support/split-ticket` | staff | `splitTicket` | Extract a message into a new ticket. |
412
+ | `POST` | `/support/apply-macro` | staff | `macros` | Run a multi-action macro on a ticket. |
413
+ | `POST` | `/support/tickets/:id/escalate` | staff | β€” | Move a ticket to `escalated`. |
414
+ | `POST` | `/support/tickets/:id/transfer` | staff or ticket owner | β€” | Email a conversation recap to an external address. |
415
+ | `POST` | `/support/tickets/:id/invite` | staff or ticket owner | β€” | Invite a collaborator on the ticket. |
416
+ | `POST` | `/support/tickets/:id/feedback` | client | β€” | Submit a rating (1-5) and an optional comment. |
417
+ | `POST` | `/support/satisfaction` | client | `satisfaction` | Submit the CSAT survey of a resolved ticket. |
418
+ | `POST` | `/support/send-reminder` | staff | `autoClose` | Nudge a client whose ticket awaits their reply. |
419
+ | `POST` | `/support/resend-notification` | staff | β€” | Re-send the email notification of one message. |
420
+ | `POST` | `/support/import-conversation` | staff, or `x-webhook-secret` | β€” | Import an existing email thread as a ticket. |
421
+ | `GET`/`POST` | `/support/typing` | authenticated | `collisionDetection` | Typing indicator. |
422
+ | `GET`/`POST` | `/support/presence` | staff | `collisionDetection` | Who is looking at this ticket. |
423
+ | `GET`/`POST` | `/support/signature` | staff | `signatures` | Per-agent email signature. |
424
+ | `GET`/`POST` | `/support/user-prefs` | staff | β€” | Per-user locale and signature. |
425
+
426
+ ### Portal authentication and GDPR
427
+
428
+ | Method | Path | Access | Purpose |
429
+ |---|---|---|---|
430
+ | `POST` | `/support/login` | public, rate-limited | Portal password login. Sets an `HttpOnly` cookie; the JWT is never returned in JSON. |
431
+ | `POST` | `/support/2fa` | public, rate-limited | Email second factor. |
432
+ | `POST` | `/support/oauth/google` | public | Google sign-in, optionally restricted by `allowedEmailDomains`. |
433
+ | `GET` | `/support/export-data` | client | GDPR data export. |
434
+ | `POST` | `/support/delete-account` | client | GDPR right to erasure. |
435
+ | `POST` | `/support/merge-clients` | staff | Merge two client records. |
436
+
437
+ ### AI
438
+
439
+ | Method | Path | Access | Flag | Purpose |
440
+ |---|---|---|---|---|
441
+ | `POST` | `/support/ai` | staff, rate-limited | `ai` | `sentiment`, `synthesis`, `suggest_reply` or `rewrite`. |
442
+ | `POST` | `/support/ai-agent` | staff, rate-limited | `ai` | Autonomous agent: answers from the knowledge base or escalates. |
443
+ | `POST` | `/support/ticket-synthesis` | staff, rate-limited | `ai` | Generate (or return the cached) per-ticket synthesis. |
444
+ | `GET`/`POST` | `/support/client-intelligence` | staff, rate-limited | `ai` | Cached client summary. |
445
+ | `POST` | `/support/chatbot` | public, rate-limited | `chatbot` | Knowledge-base chatbot used for deflection. |
446
+ | `POST` | `/support/seed-kb` | staff | β€” | Seed the knowledge base. |
167
447
 
168
448
  ```ts
169
449
  await fetch('/api/support/ai-agent', {
@@ -173,279 +453,300 @@ await fetch('/api/support/ai-agent', {
173
453
  })
174
454
  ```
175
455
 
176
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
177
-
178
- ## πŸ”Œ API Reference
179
-
180
- ### `supportPlugin(config)`
181
-
182
- The Payload plugin. Adds collections, admin views, editor components and API endpoints.
183
-
184
- ```typescript
185
- supportPlugin(config?: SupportPluginConfig): Plugin
456
+ ### Live chat
457
+
458
+ | Method | Path | Access | Flag | Purpose |
459
+ |---|---|---|---|---|
460
+ | `GET`/`POST` | `/support/chat` | client | `chat` | Portal chat polling and send. |
461
+ | `GET` | `/support/chat-stream` | client | `chat` | SSE stream for the portal widget. |
462
+ | `GET`/`POST` | `/support/admin-chat` | staff | `chat` | Agent console: sessions, messages, send. |
463
+ | `GET` | `/support/admin-chat-stream` | staff | `chat` | SSE stream of the session list, or of one session's messages with `?session=`. Available for a custom agent console β€” the shipped Chat view polls `/support/admin-chat` instead. |
464
+
465
+ ### Reporting, billing and email
466
+
467
+ | Method | Path | Access | Flag | Purpose |
468
+ |---|---|---|---|---|
469
+ | `GET` | `/support/admin-stats` | staff | β€” | Dashboard KPIs, volume series, CSAT and NPS. |
470
+ | `GET` | `/support/export-csv` | staff | β€” | CSV export of the tickets. |
471
+ | `GET` | `/support/sla-check` | staff | `sla` | Tickets breaching or close to breaching SLA. |
472
+ | `GET` | `/support/billing` | staff | `timeTracking` | Pre-billing for a period, optionally per project. |
473
+ | `GET` | `/support/billing/invoice` | staff | `timeTracking` | Invoice for the period, `format=html\|pdf`. |
474
+ | `GET` | `/support/email-stats` | authenticated | `emailTracking` | Email open/sent aggregates. |
475
+ | `GET` | `/support/track-open` | public, HMAC-signed | `emailTracking` | Tracking pixel. |
476
+ | `POST` | `/support/pending-emails/:id/process` | staff | `pendingEmails` | Turn a pending email into a ticket or a reply. |
477
+ | `DELETE` | `/support/purge-logs` | staff | β€” | Delete logs older than N days. |
478
+
479
+ ### Settings, notifications and machine-to-machine
480
+
481
+ | Method | Path | Access | Flag | Purpose |
482
+ |---|---|---|---|---|
483
+ | `GET`/`POST` | `/support/settings` | staff | β€” | Read and write the support settings, including the runtime feature flags. |
484
+ | `GET`/`POST` | `/support/round-robin-config` | staff | `roundRobin` | Read and toggle round-robin assignment. |
485
+ | `GET` | `/support/push/vapid-public-key` | public | β€” | VAPID public key for the browser subscription. |
486
+ | `POST` | `/support/push/subscribe` | staff | β€” | Register an agent's push subscription. |
487
+ | `GET` | `/support/auto-close` | `x-cron-secret` | `autoClose` | Remind, then close, inactive tickets. |
488
+ | `POST` | `/support/process-snooze` | `x-cron-secret` | `snooze` | Wake snoozed tickets. |
489
+ | `POST` | `/support/process-scheduled` | `x-cron-secret` | `scheduledReplies` | Release scheduled replies. |
490
+ | `POST` | `/support/process-digests` | `x-cron-secret` | β€” | Send the daily or weekly digests. |
491
+ | `POST` | `/support/channels/webhook` | `x-channel-secret` | β€” | Normalized inbound WhatsApp / Messenger webhook. |
492
+ | `POST` | `/support-webhook/inbound-email` | configured secret header | `capabilities.inboundEmail` | Inbound email, validated and bounded by the plugin. |
493
+ | `POST` | `/support/suggest-projects` | staff | `capabilities.projectSuggestions` | Deployment-specific project suggestions. |
494
+ | `POST` | `/support/ticket-title` | staff | `capabilities.aiTitles` | Generate a short display title for one ticket. |
495
+ | `POST` | `/support/generate-missing-titles` | staff | `capabilities.aiTitles` | Backfill the missing display titles. |
496
+
497
+ ## Webhooks
498
+
499
+ Create a row in `webhook-endpoints` with a URL, a `secret` and the events you want, and set it
500
+ `active`. Each event is delivered **once** per matching endpoint (up to 50, 10 s timeout each), as a
501
+ single `POST`:
502
+
503
+ ```http
504
+ Content-Type: application/json
505
+ User-Agent: PayloadSupport-Webhook/1.0
506
+ X-Webhook-Signature: <HMAC-SHA256 of the raw body, keyed with the endpoint secret>
507
+
508
+ { "event": "ticket_created", "data": { … }, "timestamp": "2026-09-07T10:00:00.000Z" }
186
509
  ```
187
510
 
188
- ### Exports
511
+ The signature header is only sent when the endpoint has a `secret`, so always set one. There is no
512
+ `X-Webhook-Secret` header any more β€” verify the HMAC signature instead. `lastTriggeredAt` and
513
+ `lastStatus` are written back on the endpoint after every attempt. Requires `features.webhooks`.
514
+
515
+ | Event | `data` |
516
+ |---|---|
517
+ | `ticket_created` | `ticketId`, `id`, `ticketNumber`, `subject`, `status`, `priority`, `category` |
518
+ | `ticket_resolved` | `ticketId`, `id`, `ticketNumber`, `subject`, `previousStatus` |
519
+ | `ticket_assigned` | `ticketId`, `id`, `ticketNumber`, `subject`, `assignedTo` |
520
+ | `ticket_replied` | `ticketId`, `messageId`, `authorType`, `body` (first 500 characters), plus `scheduled: true` when released by the scheduler |
521
+ | `sla_breached` | Selectable on an endpoint, but nothing dispatches it β€” subscribing to it delivers nothing. |
522
+
523
+ ## Collections
524
+
525
+ "Staff" is `userCollectionSlug` (default `users`), "client" is a `support-clients` user.
526
+
527
+ `collectionSlugs` accepts 19 keys: `tickets`, `ticketMessages`, `supportClients`, `timeEntries`,
528
+ `cannedResponses`, `ticketActivityLog`, `satisfactionSurveys`, `knowledgeBase`, `chatMessages`,
529
+ `pendingEmails`, `emailLogs`, `authLogs`, `webhookEndpoints`, `slaPolicies`, `macros`,
530
+ `ticketStatuses`, `ticketFeedback`, `rateLimits` and `counters`. Three caveats:
531
+
532
+ - **`media` has no key, and it is the blocking one.** The internal `CollectionSlugs` type carries a
533
+ `media` slug (default `media`) and uses it as the `relationTo` of the attachment fields on
534
+ `ticket-messages` and `pending-emails`, but `collectionSlugs` exposes no way to set it. If your
535
+ uploads collection is named anything else, Payload throws `InvalidFieldRelationship` at config
536
+ sanitisation and the whole `payload.config.ts` fails to build. Your uploads collection has to be
537
+ named `media`.
538
+ - Three slugs are genuinely fixed: `ticketFeedback` is accepted but ignored, and
539
+ `ticket-collaborators` and `client-summaries` have no key at all β€” those three files write their
540
+ own `slug:` as a literal.
541
+ - `notificationQueue`, `automationRules`, `supportTeams` and `pushSubscriptions` are a different
542
+ case: their collections *do* read `slugs.*`, only the public option type omits the keys. The
543
+ plumbing is there; widening the type would be enough.
544
+
545
+ | Slug | Injected | Role | Read | Write |
546
+ |---|---|---|---|---|
547
+ | `tickets` | always | The tickets themselves | staff; clients see the tickets they own or collaborate on | staff; a client create keeps only `subject`, `category`, `priority`, `project` |
548
+ | `ticket-messages` | always | Conversation, internal notes, scheduled replies | staff; clients see non-internal, already-sent messages of their tickets | staff; clients only on an accessible ticket, and may edit only their own messages |
549
+ | `support-clients` | always | Portal auth collection (2FA, OAuth, notification preferences) | staff; a client reads their own row | staff; a client updates their own row minus `tier`, `accountManager`, `opportunities`, `notes`, `googleId` and the 2FA internals |
550
+ | `ticket-collaborators` | always | Extra clients invited on a ticket β€” source of truth for client read scope | staff and the invited client | staff only (`update` denied to everyone); the invite endpoint writes with `overrideAccess`. **Fixed slug.** |
551
+ | `ticket-activity-log` | always | Append-only audit trail | staff; clients on their own tickets | nobody β€” hooks only (`create`/`update` denied) |
552
+ | `canned-responses` | always | Quick reply templates | staff | staff |
553
+ | `satisfaction-surveys` | always | CSAT after resolution | staff and the surveyed client | immutable once submitted |
554
+ | `ticket-feedback` | always | Portal rating and comment | staff and the author | immutable once submitted. **Fixed slug.** |
555
+ | `knowledge-base` | always | FAQ and chatbot corpus | staff; published entries publicly | staff |
556
+ | `notification-queue` | always | Digest queue | staff | hooks only |
557
+ | `automation-rules` | always | Event β†’ conditions β†’ actions | staff | staff |
558
+ | `support-teams` | always | Teams, used by team SLA and `SUPPORT_TEAM_SCOPING` | staff | staff |
559
+ | `push-subscriptions` | always | Agent Web Push subscriptions | staff | the subscribe endpoint only |
560
+ | `support-counters` | always | Atomic ticket-number counter | nobody | internal only |
561
+ | `auth-logs` | `authLogs` | Portal authentication audit | staff | system-generated |
562
+ | `time-entries` | `timeTracking` | Time spent, rolled up into `tickets.totalTimeMinutes` | staff | staff |
563
+ | `email-logs` | `emailTracking` | Sent/opened tracking | staff | system-generated |
564
+ | `webhook-endpoints` | `webhooks` | Outbound webhook subscriptions | staff | staff |
565
+ | `sla-policies` | `sla` | Default and per-team SLA targets | staff | staff |
566
+ | `macros` | `macros` | Multi-action shortcuts | staff | staff |
567
+ | `chat-messages` | `chat` | Live chat sessions | staff and the session's client | authenticated |
568
+ | `pending-emails` | `pendingEmails` | Inbound emails awaiting triage | staff | staff, or a create carrying a valid `x-webhook-secret` |
569
+ | `ticket-statuses` | `customStatuses` | Configurable statuses | authenticated | staff |
570
+ | `client-summaries` | `ai` | Cached client-intelligence summaries | staff | staff. **Fixed slug**, and its access rules compare against the literal `users` collection, so a renamed staff collection loses access to it. |
571
+ | `support-rate-limits` | `rateLimitStore: 'payload'` | Shared rate-limit counters | nobody | internal only |
572
+
573
+ ## Package Exports
574
+
575
+ | Subpath | Exposes | Formats | Environment |
576
+ |---|---|---|---|
577
+ | `@consilioweb/payload-support` | `supportPlugin`, the collection factories, the SLA and status-email hooks, `dispatchWebhook`, `generateTicketSynthesis`, the rate-limit stores, the settings and feature helpers, and the public types | ESM + CJS, typed | Server (`payload.config.ts`) |
578
+ | `@consilioweb/payload-support/client` | `SUPPORT_CLIENT_VERSION` β€” the `'use client'` barrel marker | ESM + CJS, typed | Client |
579
+ | `@consilioweb/payload-support/views` | The 13 admin views, for the Payload import map | **ESM only**, typed | Server components |
580
+ | `@consilioweb/payload-support/components/TicketConversation` | The conversation field component | **ESM only**, typed | Client |
581
+
582
+ The two view/component subpaths lost their CJS build in 3.0.0: the emitted `.cjs` barrels
583
+ `require`d their ESM siblings and threw `ERR_REQUIRE_ESM`, so they were unreachable. Payload 3 and
584
+ Next are ESM-first; import them with ESM.
189
585
 
190
- ```typescript
191
- import { supportPlugin, generateTicketSynthesis, dispatchWebhook } from '@consilioweb/payload-support'
586
+ ```ts
587
+ import { supportPlugin, dispatchWebhook, generateTicketSynthesis } from '@consilioweb/payload-support'
192
588
  import type { SupportPluginConfig, SupportFeatures } from '@consilioweb/payload-support'
193
589
  ```
194
590
 
195
- ### Key HTTP endpoints (under `/api/support`)
591
+ ## Requirements
196
592
 
197
- | Method | Path | Purpose |
593
+ | Requirement | Range | Source |
198
594
  |---|---|---|
199
- | `POST` | `/ai` | Sentiment, reply suggestion, rewrite, synthesis (admin). |
200
- | `POST` | `/ai-agent` | **Autonomous AI agent** β€” answers from the KB or escalates. |
201
- | `POST` | `/login`, `/2fa`, `/oauth/google` | Client portal auth (password, 2FA, OAuth). |
202
- | `GET` | `/admin-stats` | Dashboard KPIs (real volume series, CSAT, NPS). |
203
- | `GET` | `/billing`, `/billing/invoice` | Pre-billing + print-ready invoice. |
204
- | `POST` | `/process-snooze`, `/process-digests`, `/process-scheduled`, `/auto-close` | Cron jobs (guarded by `x-cron-secret`). |
205
-
206
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
207
-
208
- ## βš™οΈ Configuration
209
-
210
- | Option | Type | Default | Description |
211
- |--------|------|---------|-------------|
212
- | `features` | `SupportFeatures` | all `true` | Toggle each feature on/off. |
213
- | `ai` | `AIProviderConfig` | `anthropic` | AI provider: `anthropic` \| `openai` \| `ollama` \| `custom`. |
214
- | `email` | `EmailConfig` | β€” | `fromName`, `fromAddress`, `replyTo`. |
215
- | `locale` | `'fr' \| 'en'` | `'fr'` | Admin/portal language. |
216
- | `basePath` | `string` | `'/support'` | Admin views prefix. |
217
- | `userCollectionSlug` | `string` | `'users'` | Agents collection. |
218
- | `allowedEmailDomains` | `string[]` | β€” | Domains allowed for OAuth auto-registration. |
219
- | `collectionSlugs` | `object` | β€” | Collection slug overrides. |
220
- | `skipCollections` / `skipViews` / `skipEndpoints` | `boolean` | `false` | Skip injecting that part. |
221
-
222
- ### Environment variables
223
-
224
- | Env var | Required | Description |
225
- |---------|----------|-------------|
226
- | `PAYLOAD_SECRET` | βœ… | Payload secret (also signs 2FA & tracking). |
227
- | `NEXT_PUBLIC_SERVER_URL` | βœ… | Public URL (email/portal links). |
228
- | `CRON_SECRET` | for crons | `x-cron-secret` header for the cron endpoints. |
229
- | `ANTHROPIC_API_KEY` / `OLLAMA_API_URL` | if AI | AI provider keys/URL. |
230
- | `GOOGLE_OAUTH_CLIENT_ID` / `_SECRET` | if OAuth | Portal Google sign-in. |
231
- | `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | if push | Web Push keys (`npx web-push generate-vapid-keys`). |
232
- | `VAPID_SUBJECT` | optional | Push contact (`mailto:` or URL), defaults to `mailto:support@example.com`. |
233
- | `SUPPORT_TEAM_SCOPING` | optional | `1` to scope agents to their team's tickets. |
234
- | `SUPPORT_EMAIL` / `SUPPORT_REPLY_TO` | optional | From/reply-to addresses. |
235
-
236
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
237
-
238
- ## ⚑ Performance
239
-
240
- Engineered to stay fast under load (no fabricated benchmarks β€” figures depend on your volume and host):
595
+ | Node.js | `>=20.9.0` | `engines.node` |
596
+ | Payload | `^3.37.0` | `peerDependencies.payload` |
597
+ | `@payloadcms/next` | `^3.37.0` | `peerDependencies` |
598
+ | Next.js | `^15.2.9 \|\| ^16.0.0` | `peerDependencies` |
599
+ | React / React DOM | `^19.0.0` | `peerDependencies` |
600
+ | `lucide-react` | `>=0.300.0` | `peerDependencies` |
601
+ | `@anthropic-ai/sdk` | any recent version | Runtime-only, install it yourself if you use the AI features |
602
+ | `@payloadcms/richtext-lexical` | any 3.x | Only if you mount the portal FAQ page |
603
+
604
+ Installing on Node 18, React 18, Next 14 or Payload < 3.37 warns, and fails outright under
605
+ `engine-strict` or a strict peer resolver.
606
+
607
+ ## Security
608
+
609
+ - **Cross-client isolation** β€” reads on `tickets`, `ticket-messages` and `ticket-activity-log` are
610
+ constrained to the ticket ids a client owns or collaborates on, resolved server-side and failing
611
+ closed on an empty set. Client writes are validated against that same set.
612
+ - **Allow-listed client writes** β€” a client-initiated ticket create keeps only `subject`, `category`,
613
+ `priority` and `project`; `client`, `status` and `source` are forced. A client updating their own
614
+ profile cannot touch `tier`, `accountManager`, `opportunities`, `notes`, `googleId` or the 2FA
615
+ internals.
616
+ - **Cookie-only portal JWTs** β€” `HttpOnly`, `SameSite=Lax`, `Path=/`, 2 h max age, and `Secure`
617
+ outside development. Login, OAuth and 2FA responses never return the token in JSON.
618
+ - **2FA enforced server-side** (`beforeLogin`), and the endpoint refuses to run without
619
+ `PAYLOAD_SECRET` rather than falling back to an insecure default. OAuth verifies the Google email.
620
+ - **Stored-XSS protection** β€” message HTML is sanitized on write, not on render.
621
+ - **HMAC everywhere it matters** β€” webhook deliveries and tracking pixels are signed and verified in
622
+ constant time, with idempotent writes. Cron and webhook secrets are read from headers only; a
623
+ query-string secret is rejected.
624
+ - **Persistent rate limiting** on login, 2FA, chat, invitations, transfers, imports and every AI
625
+ endpoint. Use `rateLimitStore: 'payload'` (or your own shared store) on multi-instance deployments.
626
+ - **No silent third-party AI host** β€” the `ollama` provider fails loudly on a missing
627
+ `OLLAMA_API_URL` instead of defaulting to someone else's server.
628
+ - **Bounded inbound email** payloads and attachments.
629
+
630
+ ### Reporting security issues
631
+
632
+ Email **contact@consilioweb.fr** instead of opening a public issue.
633
+
634
+ ### Best practices
635
+
636
+ - Set a strong `PAYLOAD_SECRET` and a real `CRON_SECRET`.
637
+ - Restrict read access on your `media` collection to the ticket owner.
638
+ - Put the app behind a trusted proxy so `x-forwarded-for` is meaningful.
639
+ - Rotate every webhook endpoint secret that existed before 3.0.0 β€” the legacy transport sent it in
640
+ clear on every delivery.
641
+
642
+ ## Performance
643
+
644
+ No benchmarks are claimed here; the figures depend on your volume and host. What the code actually
645
+ does:
241
646
 
242
647
  | Concern | Approach |
243
- |---------|----------|
244
- | Settings reads | In-process cache (TTL + invalidation) β€” avoids ~8 redundant DB reads per ticket mutation. |
245
- | List queries | SQLite indexes on filtered fields β€” inbox/dashboard/SLA in `O(index)` instead of full scans. |
246
- | Email sends | Fire-and-forget β€” the response no longer waits on the SMTP round-trip (βˆ’200-500 ms). |
247
- | Stats & billing | Paginated aggregation + bounded `select` β€” never loads all tickets in memory. |
248
- | AI synthesis | Cached per ticket β€” no LLM recompute on every view. |
249
-
250
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
251
-
252
- ## πŸ“š Examples
253
-
254
- ### Automation rule (no code)
255
-
256
- Create a **Automation Rules** entry: `event = ticket_created`, condition `category = bug`, action `set_priority = urgent`. New "bug" tickets become "urgent" automatically.
257
-
258
- ### Wire the cron jobs
259
-
260
- ```bash
261
- # Hourly: auto-close, snooze wake-up, scheduled replies
262
- curl -X POST https://your-app/api/support/auto-close -H "x-cron-secret: $CRON_SECRET"
263
- curl -X POST https://your-app/api/support/process-snooze -H "x-cron-secret: $CRON_SECRET"
264
- # Daily / weekly: notification digests
265
- curl -X POST https://your-app/api/support/process-digests -H "x-cron-secret: $CRON_SECRET" -d '{"frequency":"daily"}'
266
- ```
267
-
268
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
269
-
270
- ## ❓ FAQ
271
-
272
- <details>
273
- <summary><b>Which databases are supported?</b></summary>
274
-
275
- Built and tested on **SQLite** (`@payloadcms/db-sqlite`) with a sequential seed and `busyTimeout`. It works with any Payload adapter, but indexes and queries were validated on SQLite.
276
-
277
- </details>
278
-
279
- <details>
280
- <summary><b>What Node / Payload versions are supported?</b></summary>
281
-
282
- Node.js 18+, Payload 3.x, React 18 or 19.
283
-
284
- </details>
648
+ |---|---|
649
+ | Settings reads | In-process cache with a 60 s TTL and explicit invalidation on save β€” the `afterChange` hook chain reads the same preference row many times per ticket mutation. |
650
+ | List queries | Indexed fields on `tickets` for the columns the inbox, dashboard and SLA views filter on. |
651
+ | Stats and billing | Paginated aggregation (100 rows per page) with a bounded `select` β€” never loads all tickets in memory. |
652
+ | Counters | The inbox counters and the portal poller call `/api/*/count` instead of fetching rows. |
653
+ | Client time totals | The CRM sums the per-ticket `totalTimeMinutes` rollup instead of scanning `time-entries`. |
654
+ | AI synthesis | Persisted on the ticket (`aiSummary`, `aiSummaryGeneratedAt`) and reused until forced. |
285
655
 
286
- <details>
287
- <summary><b>Is there TypeScript support?</b></summary>
656
+ ## Troubleshooting
288
657
 
289
- Yes β€” strict TypeScript, with business types exported (`SupportPluginConfig`, `SupportFeatures`, …).
658
+ **`useServerFunctions must be used within ServerFunctionsProvider`** β€” align every `@payloadcms/*`
659
+ package on the same version, 3.75 or later.
290
660
 
291
- </details>
661
+ **`SQLITE_BUSY` / database is locked during seed** β€” make the seed sequential (no `Promise.all` on
662
+ inserts) and add `busyTimeout: 10000` to the SQLite adapter.
292
663
 
293
- <details>
294
- <summary><b>Do I need an external AI service?</b></summary>
664
+ **Admin views do not load** β€” regenerate the import map (`pnpm payload generate:importmap`). In a
665
+ headless context, pass `skipViews: true`.
295
666
 
296
- No. AI features are optional and you can run **self-hosted Ollama** to depend on no cloud (sovereignty/GDPR). Anthropic and OpenAI are also supported.
667
+ **New fields or collections missing in production** β€” standalone builds do not run migrations. Push
668
+ the schema before deploying, including `googleId`, `twoFactorVerifiedAt`, `slaPausedAt`, `nps`,
669
+ `mentions`, and the `notification-queue`, `automation-rules`, `ticket-collaborators`,
670
+ `support-counters` collections.
297
671
 
298
- </details>
672
+ **`Cannot find module '@anthropic-ai/sdk'`** β€” install it in your app; the plugin does not bundle or
673
+ declare it.
299
674
 
300
- <details>
301
- <summary><b>How do I migrate the schema in production?</b></summary>
675
+ **`ERR_REQUIRE_ESM` on `@consilioweb/payload-support/views`** β€” that subpath is ESM-only since 3.0.0.
302
676
 
303
- In standalone mode the schema isn't auto-migrated: generate/push the schema (new fields and collections) before deploying. See [Troubleshooting](#-troubleshooting).
677
+ ## FAQ
304
678
 
305
- </details>
679
+ **Which databases are supported?** β€” built and tested on SQLite (`@payloadcms/db-sqlite`) with a
680
+ sequential seed and `busyTimeout`. It works with any Payload adapter, but the indexes and queries
681
+ were validated on SQLite.
306
682
 
307
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
683
+ **Do I need an external AI service?** β€” no. The AI features are optional, and `provider: 'ollama'`
684
+ points the SDK at the `OLLAMA_API_URL` you choose, so no ticket content has to leave your
685
+ infrastructure. `provider: 'openai'` and `provider: 'custom'` are accepted by the `AIProviderConfig`
686
+ type but not implemented: at runtime everything that is not `ollama` goes to Anthropic.
308
687
 
309
- ## πŸ”§ Troubleshooting
688
+ **Is TypeScript supported?** β€” yes, strict, with the business types exported
689
+ (`SupportPluginConfig`, `SupportFeatures`, `TicketData`, …). All four export subpaths ship
690
+ declarations.
310
691
 
311
- ### `useServerFunctions must be used within ServerFunctionsProvider`
692
+ **How do I migrate the schema in production?** β€” in standalone mode the schema is not
693
+ auto-migrated: generate and push it before deploying. See [Troubleshooting](#troubleshooting).
312
694
 
313
- Use Payload **3.75+** for all `@payloadcms/*` packages (aligned versions).
695
+ ## Upgrading
314
696
 
315
- ### `SQLITE_BUSY` / database is locked during seed
697
+ ### 2.x β†’ 3.0
316
698
 
317
- Make the seed **sequential** (no `Promise.all` on inserts) and add `busyTimeout: 10000` to the SQLite adapter.
699
+ 1. **Rotate the secret of every webhook endpoint.** The legacy transport sent it in clear in
700
+ `X-Webhook-Secret` on every delivery. Subscribers must now verify `X-Webhook-Signature`.
701
+ 2. **Set `OLLAMA_API_URL`** before restarting if you use `provider: 'ollama'` β€” the hard-coded
702
+ fallback host is gone and its absence is now a hard error.
703
+ 3. **Move any integration that authenticated as a portal client** to a staff account or to a
704
+ server-side call with `overrideAccess: true`: client writes are scoped and allow-listed now.
705
+ 4. **Recompute `tickets.totalTimeMinutes`** where time entries were deleted under 2.x. The new
706
+ `afterDelete` hook only fires on future deletions, and the CRM now trusts the rollup instead of
707
+ recomputing it β€” so a stale total keeps over-billing until something touches a time entry on that
708
+ ticket.
709
+ 5. **Import `./views` and `./components/TicketConversation` with ESM** β€” their CJS builds are gone.
710
+ 6. Check your platform: Node 20.9+, React 19, Next 15.2.9+, Payload 3.37+, and install
711
+ `lucide-react` explicitly if you were relying on it being optional.
318
712
 
319
- ### Admin views don't load
713
+ ### 1.x β†’ 2.0
320
714
 
321
- Regenerate the import map after adding components: `pnpm payload generate:importmap`. In a plugin/headless context, use `skipViews: true` if you don't mount the admin UI.
715
+ 1. Generate an additive Payload migration for the `support-counters` collection and, when
716
+ `rateLimitStore: 'payload'` is enabled, `support-rate-limits`. Run it before restart.
717
+ 2. Regenerate the Payload types and the admin import map.
718
+ 3. Remove duplicated host routes and enable the plugin endpoints (`skipEndpoints: false`).
719
+ 4. Read portal authentication exclusively from the `HttpOnly` cookie. Login, OAuth and 2FA responses
720
+ no longer expose the JWT in JSON.
721
+ 5. Send cron and webhook secrets only through their configured headers. Query-string secrets are
722
+ rejected.
723
+ 6. Custom rate-limit stores must implement the asynchronous `RateLimitStore` interface.
322
724
 
323
- ### New fields/collections missing in production
725
+ ## Contributing
324
726
 
325
- Standalone doesn't run migrations: push the schema for `googleId`, `twoFactorVerifiedAt`, `slaPausedAt`, `nps`, `mentions`, and the `notification-queue` / `automation-rules` collections.
727
+ 1. Fork the repository.
728
+ 2. Create a feature branch (`git checkout -b feature/AmazingFeature`).
729
+ 3. Commit your changes (`git commit -m 'feat: add AmazingFeature'`).
730
+ 4. Push the branch and open a Pull Request.
326
731
 
327
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
328
-
329
- ## πŸ” Security
330
-
331
- Security is a first-class concern β€” several guardrails are validated by integration tests.
332
-
333
- - **Cross-client isolation** β€” a client can never read another's tickets/messages (filtered by owned tickets).
334
- - **2FA** enforced server-side (`beforeLogin`); **OAuth** verifies the Google email.
335
- - **Sanitization** of message HTML server-side (stored-XSS protection).
336
- - **HMAC-signed** webhooks, signed tracking pixel, fail-closed secrets.
337
-
338
- ### Reporting Security Issues
339
-
340
- Please email **contact@consilioweb.fr** instead of opening a public issue.
341
-
342
- ### Best Practices
343
-
344
- - βœ… Set a strong `PAYLOAD_SECRET` (never a default value)
345
- - βœ… Set `CRON_SECRET` to protect the cron endpoints
346
- - βœ… Restrict read access to the `media` collection to the ticket owner
347
- - βœ… Put the app behind a trusted proxy (`x-forwarded-for` header)
348
- - βœ… Keep the plugin up-to-date
349
-
350
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
351
-
352
- ## 🀝 Contributing
353
-
354
- Contributions are very welcome!
355
-
356
- 1. **Fork** the repository
357
- 2. **Create** a feature branch (`git checkout -b feature/AmazingFeature`)
358
- 3. **Commit** your changes (`git commit -m 'feat: add AmazingFeature'`)
359
- 4. **Push** the branch (`git push origin feature/AmazingFeature`)
360
- 5. **Open** a Pull Request
361
-
362
- Run the checks before submitting:
732
+ Run the checks before submitting β€” the suite is 179 vitest tests:
363
733
 
364
734
  ```bash
365
- npm run typecheck && npm test && npm run build
735
+ pnpm typecheck && pnpm test && pnpm build
366
736
  ```
367
737
 
368
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
369
-
370
- ## πŸ“ Changelog
738
+ ## Changelog
371
739
 
372
740
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
373
741
 
374
- ### [1.1.1] β€” 2026-06-26
375
-
376
- - πŸ› **Fix client ticket reopen**: allow clients to set `waiting_support` (the status the portal's "Reopen" button sends). It was silently rejected, leaving the ticket `resolved`.
377
-
378
- ### [1.1.0] β€” 2026-06-25
379
-
380
- - 🎯 **Per-team SLA policies & dashboards** (team policy overrides the default, `?teamId=` scoping).
381
- - πŸ”” **Native push / browser notifications** (Web Push / VAPID, agent pushed on new client messages).
382
- - 🎭 **End-to-end UI test harness** (browser-driven admin via Playwright, `pnpm test:e2e`).
383
- - βœ… 109 integration tests (up from 102).
384
-
385
- ### [1.0.0] β€” 2026-06-25
386
-
387
- - ✨ Complete ticketing, SLA (+ pause-on-hold), automation & **visual rules engine**.
388
- - πŸ€– AI (sentiment, suggestion, synthesis, chatbot) + **autonomous AI agent**.
389
- - πŸ’¬ Live chat, client portal (2FA, OAuth), knowledge base.
390
- - πŸ’Ά Time tracking, pre-billing & print-ready invoice.
391
- - πŸ“Š CSAT + NPS, real volume series, digests, webhooks.
392
- - πŸ” Security hardening (cross-client isolation, anti-XSS, server-side 2FA).
393
- - βœ… Integration test harness (Payload + in-memory SQLite), 102 tests.
394
-
395
- <details>
396
- <summary><b>Previous versions (0.x)</b></summary>
397
-
398
- - **0.16.0** β€” Manual client reminder + auto-close after 24h
399
- - **0.15.0** β€” Next 16 compatibility + accumulated features
400
- - **0.9.0 β†’ 0.9.13** β€” Enriched views (Client Intelligence, Billing), bundled RichTextEditor, code blocks in emails, inline message editing, enriched pre-billing, per-ticket cached AI synthesis, multi-style rewriting
401
- - **0.6.0 β†’ 0.6.4** β€” Split build (`bundle:false`) for Next.js RSC compatibility + barrel exports
402
- - **0.5.0** β€” Full i18n across the 13 admin views
403
- - **0.4.0** β€” Per-user preferences (locale, signature) vs global settings
404
- - **0.3.0** β€” Feature parity with ConsilioWEB + `skipCollections` / `skipViews` / `skipEndpoints`
405
- - **0.2.0** β€” 2026-04-08 β€” Security overhaul, SSE live chat, webhooks, SLA, scheduled replies, client portal
406
- - **0.1.0** β€” 2026-04-08 β€” Initial scaffold: `supportPlugin()`, 15 collections, feature flags, AI provider abstraction
407
-
408
- </details>
409
-
410
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
411
-
412
- ## πŸ—ΊοΈ Roadmap
413
-
414
- - [x] Ticketing, SLA, time tracking & billing
415
- - [x] AI assists + autonomous AI agent
416
- - [x] Live chat, client portal, knowledge base
417
- - [x] Visual automation-rules engine
418
- - [x] Native binary PDF invoice (alongside the print-ready HTML)
419
- - [x] Full admin i18n (all views)
420
- - [x] Social channels (WhatsApp, Messenger)
421
- - [x] Multi-team / workspaces mode
422
- - [x] Per-team SLA policies & dashboards
423
- - [x] Native push / browser notifications
424
- - [x] End-to-end UI test harness (browser-driven admin)
425
-
426
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
427
-
428
- ## β˜• Support
429
-
430
- If this plugin saves you time, consider buying me a coffee!
431
-
432
- <a href="https://buymeacoffee.com/pown3d">
433
- <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="217" />
434
- </a>
435
-
436
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
742
+ ## Support
437
743
 
438
- ## πŸ“„ License
744
+ If this plugin saves you time, consider [buying me a coffee](https://buymeacoffee.com/pown3d).
439
745
 
440
- Licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
746
+ ## License
441
747
 
442
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
748
+ MIT β€” see [LICENSE](LICENSE).
443
749
 
444
- <div align="center" style="padding: 40px 0; color: #666; border-top: 1px solid #e0e0e0; margin-top: 50px;">
445
- <p style="margin: 10px 0;">Built and maintained by <a href="https://consilioweb.fr" style="color: #1f8a5b; text-decoration: none;">ConsilioWEB</a></p>
446
- <p style="margin: 10px 0; font-size: 13px;">
447
- <a href="https://github.com/pOwn3d/payload-support#readme" style="color: #1f8a5b; text-decoration: none; margin: 0 15px;">Documentation</a>
448
- <a href="https://github.com/pOwn3d/payload-support/issues" style="color: #1f8a5b; text-decoration: none; margin: 0 15px;">Issues</a>
449
- <a href="https://www.npmjs.com/package/@consilioweb/payload-support" style="color: #1f8a5b; text-decoration: none; margin: 0 15px;">npm</a>
450
- </p>
451
- </div>
750
+ Built and maintained by [ConsilioWEB](https://consilioweb.fr) Β·
751
+ [Issues](https://github.com/pOwn3d/payload-support/issues) Β·
752
+ [npm](https://www.npmjs.com/package/@consilioweb/payload-support)