@consilioweb/payload-support 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +272 -35
  2. package/dist/components/RichTextEditor/index.js +1 -1
  3. package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
  4. package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
  5. package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
  6. package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
  7. package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
  8. package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
  9. package/dist/components/TicketConversation/components/QuickActions.js +10 -8
  10. package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
  11. package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
  12. package/dist/components/TicketConversation/index.d.ts +10 -0
  13. package/dist/components/TicketConversation/index.js +25 -7
  14. package/dist/index.cjs +421 -73
  15. package/dist/index.d.cts +127 -38
  16. package/dist/index.d.ts +127 -38
  17. package/dist/index.js +415 -72
  18. package/dist/styles/BillingView.module.scss +11 -11
  19. package/dist/styles/ChatView.module.scss +7 -7
  20. package/dist/styles/CommandPalette.module.scss +1 -1
  21. package/dist/styles/CrmView.module.scss +9 -9
  22. package/dist/styles/EmailTracking.module.scss +6 -6
  23. package/dist/styles/ImportConversation.module.scss +5 -5
  24. package/dist/styles/Logs.module.scss +5 -5
  25. package/dist/styles/NewTicket.module.scss +2 -2
  26. package/dist/styles/PendingEmails.module.scss +13 -13
  27. package/dist/styles/SupportDashboard.module.scss +24 -8
  28. package/dist/styles/TicketDetail.module.scss +20 -20
  29. package/dist/styles/TicketInbox.module.scss +6 -6
  30. package/dist/styles/TicketingSettings.module.scss +10 -10
  31. package/dist/styles/TimeDashboard.module.scss +6 -6
  32. package/dist/styles/_tokens.scss +3 -1
  33. package/dist/utils/db.d.ts +34 -0
  34. package/dist/utils/readSettings.d.ts +90 -0
  35. package/dist/views/BillingView/client.js +15 -8
  36. package/dist/views/BillingView/index.js +4 -4
  37. package/dist/views/ChatView/client.js +2 -0
  38. package/dist/views/ChatView/index.js +4 -4
  39. package/dist/views/CrmView/client.js +2 -0
  40. package/dist/views/CrmView/index.js +4 -4
  41. package/dist/views/EmailTrackingView/client.js +23 -11
  42. package/dist/views/EmailTrackingView/index.js +4 -4
  43. package/dist/views/ImportConversationView/client.js +1 -0
  44. package/dist/views/ImportConversationView/index.js +4 -4
  45. package/dist/views/LogsView/client.js +1 -1
  46. package/dist/views/LogsView/index.js +4 -2
  47. package/dist/views/NewTicketView/client.js +20 -13
  48. package/dist/views/NewTicketView/index.js +4 -2
  49. package/dist/views/PendingEmailsView/client.js +9 -3
  50. package/dist/views/PendingEmailsView/index.js +4 -4
  51. package/dist/views/SupportDashboardView/client.js +17 -19
  52. package/dist/views/SupportDashboardView/index.js +4 -4
  53. package/dist/views/TicketDetailView/client.js +23 -11
  54. package/dist/views/TicketDetailView/index.js +4 -4
  55. package/dist/views/TicketInboxView/client.js +52 -26
  56. package/dist/views/TicketInboxView/index.js +4 -2
  57. package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  58. package/dist/views/TicketingSettingsView/client.js +44 -26
  59. package/dist/views/TicketingSettingsView/index.js +4 -4
  60. package/dist/views/TimeDashboardView/client.js +10 -7
  61. package/dist/views/TimeDashboardView/index.js +4 -4
  62. package/dist/views/shared/ErrorBoundary.d.ts +36 -1
  63. package/dist/views/shared/ErrorBoundary.js +59 -28
  64. package/dist/views/shared/adminTokens.d.ts +15 -5
  65. package/dist/views/shared/adminTokens.js +23 -7
  66. package/dist/views/shared/icons.d.ts +49 -0
  67. package/dist/views/shared/icons.js +92 -0
  68. package/dist/views/shared/locales/en.json +15 -1
  69. package/dist/views/shared/locales/fr.json +15 -1
  70. package/dist/views/shared/viewAccess.d.ts +29 -0
  71. package/dist/views/shared/viewAccess.js +24 -0
  72. package/package.json +31 -21
  73. package/scripts/uninstall-data.mjs +178 -0
  74. package/scripts/uninstall.mjs +184 -0
  75. package/src/collections/Tickets.ts +7 -0
  76. package/src/collections/TimeEntries.ts +11 -3
  77. package/src/components/RichTextEditor/index.tsx +1 -1
  78. package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
  79. package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
  80. package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
  81. package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
  82. package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
  83. package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
  84. package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
  85. package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
  86. package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
  87. package/src/components/TicketConversation/index.tsx +41 -7
  88. package/src/endpoints/auth-2fa.ts +53 -8
  89. package/src/endpoints/capabilities.ts +2 -4
  90. package/src/endpoints/chatbot.ts +2 -2
  91. package/src/endpoints/delete-account.ts +191 -47
  92. package/src/endpoints/export-data.ts +104 -4
  93. package/src/endpoints/import-conversation.ts +2 -2
  94. package/src/endpoints/login.ts +13 -3
  95. package/src/endpoints/purge-logs.ts +6 -15
  96. package/src/endpoints/push.ts +14 -1
  97. package/src/endpoints/statuses.ts +17 -0
  98. package/src/index.ts +2 -0
  99. package/src/plugin.ts +51 -1
  100. package/src/portal/LiveChat.tsx +1 -1
  101. package/src/portal/auth/profile/page.tsx +3 -2
  102. package/src/portal/icons.tsx +91 -0
  103. package/src/portal/login/page.tsx +14 -4
  104. package/src/styles/BillingView.module.scss +11 -11
  105. package/src/styles/ChatView.module.scss +7 -7
  106. package/src/styles/CommandPalette.module.scss +1 -1
  107. package/src/styles/CrmView.module.scss +9 -9
  108. package/src/styles/EmailTracking.module.scss +6 -6
  109. package/src/styles/ImportConversation.module.scss +5 -5
  110. package/src/styles/Logs.module.scss +5 -5
  111. package/src/styles/NewTicket.module.scss +2 -2
  112. package/src/styles/PendingEmails.module.scss +13 -13
  113. package/src/styles/SupportDashboard.module.scss +24 -8
  114. package/src/styles/TicketDetail.module.scss +20 -20
  115. package/src/styles/TicketInbox.module.scss +6 -6
  116. package/src/styles/TicketingSettings.module.scss +10 -10
  117. package/src/styles/TimeDashboard.module.scss +6 -6
  118. package/src/styles/_tokens.scss +3 -1
  119. package/src/types.ts +20 -0
  120. package/src/utils/push.ts +22 -0
  121. package/src/utils/rateLimiter.ts +98 -0
  122. package/src/utils/retention.ts +111 -0
  123. package/src/utils/slugs.ts +15 -0
  124. package/src/utils/twoFactorChallenge.ts +6 -1
  125. package/src/utils/urlSafety.ts +36 -1
  126. package/src/views/BillingView/client.tsx +17 -8
  127. package/src/views/BillingView/index.tsx +4 -4
  128. package/src/views/ChatView/client.tsx +2 -0
  129. package/src/views/ChatView/index.tsx +4 -4
  130. package/src/views/CrmView/client.tsx +2 -0
  131. package/src/views/CrmView/index.tsx +4 -4
  132. package/src/views/EmailTrackingView/client.tsx +18 -7
  133. package/src/views/EmailTrackingView/index.tsx +4 -4
  134. package/src/views/ImportConversationView/client.tsx +1 -0
  135. package/src/views/ImportConversationView/index.tsx +4 -4
  136. package/src/views/LogsView/client.tsx +1 -1
  137. package/src/views/LogsView/index.tsx +4 -2
  138. package/src/views/NewTicketView/client.tsx +22 -13
  139. package/src/views/NewTicketView/index.tsx +4 -2
  140. package/src/views/PendingEmailsView/client.tsx +9 -3
  141. package/src/views/PendingEmailsView/index.tsx +4 -4
  142. package/src/views/SupportDashboardView/client.tsx +11 -5
  143. package/src/views/SupportDashboardView/index.tsx +4 -4
  144. package/src/views/TicketDetailView/client.tsx +25 -11
  145. package/src/views/TicketDetailView/index.tsx +4 -4
  146. package/src/views/TicketInboxView/client.tsx +47 -7
  147. package/src/views/TicketInboxView/index.tsx +4 -2
  148. package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  149. package/src/views/TicketingSettingsView/client.tsx +201 -144
  150. package/src/views/TicketingSettingsView/index.tsx +4 -4
  151. package/src/views/TimeDashboardView/client.tsx +10 -7
  152. package/src/views/TimeDashboardView/index.tsx +4 -4
  153. package/src/views/shared/ErrorBoundary.tsx +95 -31
  154. package/src/views/shared/adminTokens.ts +28 -11
  155. package/src/views/shared/icons.tsx +146 -0
  156. package/src/views/shared/locales/en.json +15 -1
  157. package/src/views/shared/locales/fr.json +15 -1
  158. package/src/views/shared/viewAccess.ts +73 -0
  159. package/src/types/lucide-react.d.ts +0 -42
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@consilioweb/payload-support.svg)](https://www.npmjs.com/package/@consilioweb/payload-support)
6
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)
7
+ [![payload](https://img.shields.io/badge/payload-%5E3.79.1-blue.svg)](https://payloadcms.com)
8
8
 
9
9
  ## About
10
10
 
@@ -26,6 +26,7 @@ ships as a template you copy into your own `app/` directory — see
26
26
  - [Quick Start](#quick-start)
27
27
  - [Mounting the client portal](#mounting-the-client-portal)
28
28
  - [Configuration](#configuration)
29
+ - [Database and updates](#database-and-updates)
29
30
  - [Deployment adapters](#deployment-adapters)
30
31
  - [API Endpoints](#api-endpoints)
31
32
  - [Webhooks](#webhooks)
@@ -34,6 +35,7 @@ ships as a template you copy into your own `app/` directory — see
34
35
  - [Requirements](#requirements)
35
36
  - [Security](#security)
36
37
  - [Performance](#performance)
38
+ - [Personal data, retention and uninstall](#personal-data-retention-and-uninstall)
37
39
  - [Troubleshooting](#troubleshooting)
38
40
  - [FAQ](#faq)
39
41
  - [Upgrading](#upgrading)
@@ -78,21 +80,28 @@ pnpm add @consilioweb/payload-support
78
80
  # or: yarn add @consilioweb/payload-support
79
81
  ```
80
82
 
81
- **Peer dependencies** — all six are required, none is optional:
83
+ **Peer dependencies** — all five are required, none is optional:
82
84
 
83
85
  ```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"
86
+ pnpm add payload@^3.79.1 @payloadcms/next@^3.79.1 next@^15.2.9 react@^19 react-dom@^19
85
87
  ```
86
88
 
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.
89
+ - `payload@^3.79.1` — the floor is a security floor, and it is stated as such rather than
90
+ dressed up as an API constraint. Below 3.79.1 Payload is vulnerable to a pre-authentication
91
+ account takeover (GHSA-hp5w-3hxx-vmwf) and to a SQL injection, so a range that admitted those
92
+ versions — the previous `^3.37.0` did — let an install satisfy this plugin's requirements while
93
+ remaining exploitable. The API surface allows lower: the Google OAuth endpoint imports `jwtSign`
94
+ and `getFieldsToSign` from the `payload` barrel, and a missing named export fails the whole
95
+ `payload.config.ts` rather than just that endpoint, but both have been exported since 3.37 and
96
+ every symbol this package imports was verified present in 3.79.1. CI builds and tests against
97
+ the 3.88 line; 3.79.1 to 3.87 are permitted on that verification, not on a test run.
90
98
  - `@payloadcms/next` and `next` — the 13 admin views are registered unless you pass
91
99
  `skipViews: true`, and each imports `DefaultTemplate` from `@payloadcms/next/templates` plus
92
100
  `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.)
101
+ `lucide-react` is **no longer a peer dependency**. The 19 icons the admin views and the portal
102
+ widget used are inlined as SVG in `src/views/shared/icons.tsx` and `src/portal/icons.tsx`, under
103
+ Lucide's ISC licence. Nothing to install; if your app already uses `lucide-react` for its own UI,
104
+ it is unaffected.
96
105
 
97
106
  **AI runtime dependency** — the AI features load `@anthropic-ai/sdk` through a runtime
98
107
  `require`/dynamic `import` and it is marked `external` at build time. It is intentionally not a
@@ -143,12 +152,13 @@ provider, the sender addresses, the SLA targets and the runtime feature flags.
143
152
  ## Mounting the client portal
144
153
 
145
154
  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
155
+ constraint, not an oversight: `src/portal/auth/layout.tsx` and four of its pages import
147
156
  `@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.
157
+ renders its own `<html>`/`<body>` (lines 13 and 24) as a Next.js **root layout**. Neither can be
158
+ resolved from `node_modules`, so there is no `./portal` export and `plugin.ts` registers no Next
159
+ route — adding one would break at import time in every host app.
150
160
 
151
- The sources are published in the tarball (`files` includes `src`), so copy them out of
161
+ The sources are published in the tarball (`files` includes `src`, 36 files), so copy them out of
152
162
  `node_modules` into a route group of your own `app/` directory. Two directories have to be
153
163
  renamed on the way in — the shipped names are plain folders, not Next.js route syntax — and
154
164
  `layout.tsx` has to sit at the **group** level, because it renders its own `<html>`/`<body>`
@@ -190,14 +200,13 @@ What your app must provide for those pages to work:
190
200
  | Requirement | Why |
191
201
  |---|---|
192
202
  | 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. |
203
+ | **Tailwind CSS** | 29 of the 32 portal components are styled with Tailwind utility classes only. Without Tailwind the portal renders unstyled. |
194
204
  | `@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
205
  | `NEXT_PUBLIC_SERVER_URL` | Email and portal links are built from it. |
196
206
  | `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. |
197
207
 
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.
208
+ No icon library is needed: `src/portal/icons.tsx` ships the five SVG icons `LiveChat.tsx` uses, and
209
+ it is copied along with the rest of the portal.
201
210
 
202
211
  The `(auth)` layout mounts two widgets. `ChatbotWidget` is always rendered; `ChatWidget` — the
203
212
  floating live-chat FAB — returns `null` unless the host app sets
@@ -215,6 +224,7 @@ plugin upgrades do not update it. Diff it against
215
224
  |---|---|---|---|
216
225
  | `features` | `SupportFeatures` | see below | Build-time feature flags — decide which collections, endpoints and views exist. |
217
226
  | `rateLimitStore` | `RateLimitStore \| 'payload'` | in-memory | Shared storage for the endpoint rate limits. `'payload'` adds the `support-rate-limits` collection. |
227
+ | `retention` | `{ authLogsDays?, emailLogsDays?, cron?, queue? } \| false` | **not set — no task is registered** | Opt-in automatic purge of the two journals, run as a Payload Jobs task. Pass `{}` to accept the defaults (`180` / `365` days, `0 30 3 * * *`, queue `default`). `0` on either value disables that journal's purge. Left unset, or `false`, registers nothing. See [Personal data, retention and uninstall](#personal-data-retention-and-uninstall). |
218
228
  | `ticketNumber` | `{ prefix?, padding? }` | `{ prefix: 'TK-', padding: 4 }` | Sequential ticket-number formatting, backed by an atomic counter. |
219
229
  | `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
230
  | `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`. |
@@ -355,6 +365,46 @@ of conditions (field, `equals` / `not_equals` / `contains`, value) and a list of
355
365
  (`set_status`, `set_priority`, `set_category`, `assign`, `add_tag`). For example: trigger
356
366
  `ticket_created`, condition `category equals bug`, action `set_priority = urgent`.
357
367
 
368
+ ## Database and updates
369
+
370
+ - This plugin **adds collections to your Payload config; it does not own your schema**. Your app
371
+ does, and it is your app that generates and runs the migrations.
372
+ - Payload gives a plugin no way to ship them. `payload migrate` reads exactly one directory —
373
+ `payload.db.migrationDir`, resolved from the **host application's** cwd
374
+ (`payload/dist/database/migrations/readMigrationFiles.js` and `findMigrationDir.js`). A migration
375
+ file published inside a package is never discovered; it is dead code.
376
+ - **In development**, `push` synchronises the schema on its own. Nothing else to do.
377
+ - **In production**, run `payload migrate:create` then `payload migrate` — or set `prodMigrations`
378
+ on the adapter. Never `push`: the adapter skips it as soon as `NODE_ENV=production`, and mixing a
379
+ dev-pushed database with migrations makes `@payloadcms/drizzle` warn about **data loss**.
380
+ - Every release of this plugin states in its [Upgrading](#upgrading) section whether it changes the
381
+ schema. Between 2.0.1 and 5.0.0, none did. The automatic log purge introduced after 5.0.0 does —
382
+ see the table below.
383
+
384
+ ### Options that change the schema
385
+
386
+ Two families of options decide which tables exist. Flipping one on a deployed application is a
387
+ schema change like any other: generate a migration and run it, in the same deploy.
388
+
389
+ | Option | Collection it adds or removes |
390
+ |---|---|
391
+ | `retention` (when you pass it) | Payload's own `payload-jobs` collection and `payload-jobs-stats` global — **only if your app had no jobs at all**. Declaring a task is what turns Payload's queue on, and that is why the option is opt-in: a support plugin has no business adding two tables to every install. Leave it unset and purge from your own cron against the admin endpoint instead. |
392
+ | `rateLimitStore: 'payload'` | `support-rate-limits` |
393
+ | `features.authLogs` | `auth-logs` |
394
+ | `features.timeTracking` | `time-entries` |
395
+ | `features.emailTracking` | `email-logs` |
396
+ | `features.webhooks` | `webhook-endpoints` |
397
+ | `features.sla` | `sla-policies` |
398
+ | `features.macros` | `macros` |
399
+ | `features.customStatuses` | `ticket-statuses` |
400
+ | `features.chat` | `chat-messages` |
401
+ | `features.pendingEmails` | `pending-emails` |
402
+ | `features.ai` | `client-summaries` |
403
+
404
+ Turning a flag **off** does not drop the table; the collection simply stops being registered, and
405
+ its rows become unreachable through Payload while still occupying the database. Drop it explicitly
406
+ if that is what you want — see [Uninstall](#uninstall).
407
+
358
408
  ## Deployment adapters
359
409
 
360
410
  The plugin owns the generic support workflow; anything provider-specific stays in your application
@@ -407,7 +457,7 @@ to stay enabled for the endpoint to be registered.
407
457
  |---|---|---|---|---|
408
458
  | `GET` | `/support/search` | staff | — | Global search over tickets, messages, clients and the knowledge base. |
409
459
  | `GET` | `/support/kb/search` | public | — | Knowledge-base search (used by the portal and the chatbot). |
410
- | `GET` | `/support/statuses` | authenticated | `customStatuses` | Ticket statuses, sorted. |
460
+ | `GET` | `/support/statuses` | staff or support-client | `customStatuses` | Ticket statuses, sorted. Mirrors `ticket-statuses.access.read`: a principal from any other auth collection of your app gets 403, not the taxonomy. |
411
461
  | `POST` | `/support/bulk-action` | staff | `bulkActions` | Apply one action to many tickets. |
412
462
  | `POST` | `/support/merge-tickets` | staff | `merge` | Merge a source ticket into a target. |
413
463
  | `POST` | `/support/split-ticket` | staff | `splitTicket` | Extract a message into a new ticket. |
@@ -430,7 +480,7 @@ to stay enabled for the endpoint to be registered.
430
480
  | Method | Path | Access | Purpose |
431
481
  |---|---|---|---|
432
482
  | `POST` | `/support/login` | public, rate-limited | Portal password login. Sets an `HttpOnly` cookie; the JWT is never returned in JSON. |
433
- | `POST` | `/support/2fa` | public, rate-limited | Email second factor. `action: 'send'` requires the short-lived `challenge` returned by `/support/login` (or by the Google callback) alongside `requires2FA` without it an anonymous caller could burn a victim's send quota and lock them out. `action: 'verify'` takes the code. |
483
+ | `POST` | `/support/2fa` | public, rate-limited | Email second factor. **Both** `action: 'send'` and `action: 'verify'` require the short-lived `challenge` returned by `/support/login` (or by the Google callback) alongside `requires2FA`; without it, an anonymous caller who merely knew an address could burn the victim's send quota *or* their 5 verification attempts and lock them out of the only route that clears the 2FA gate. Every accepted `send` returns a refreshed `challenge` — keep the latest one and pass it to `verify` along with the code. |
434
484
  | `POST` | `/support/oauth/google` | public | Google sign-in, optionally restricted by `allowedEmailDomains`. |
435
485
  | `GET` | `/support/email-stats` | **staff**, rate-limited | Email pipeline aggregate. Was reachable by any authenticated session before 3.0.1. |
436
486
  | `GET` | `/support/export-data` | client | GDPR data export. |
@@ -495,7 +545,7 @@ await fetch('/api/support/ai-agent', {
495
545
  | `GET`/`POST` | `/support/settings` | staff | — | Read and write the support settings, including the runtime feature flags. |
496
546
  | `GET`/`POST` | `/support/round-robin-config` | staff | `roundRobin` | Read and toggle round-robin assignment. |
497
547
  | `GET` | `/support/push/vapid-public-key` | public | — | VAPID public key for the browser subscription. |
498
- | `POST` | `/support/push/subscribe` | staff | — | Register an agent's push subscription. |
548
+ | `POST` | `/support/push/subscribe` | staff | — | Register an agent's push subscription. The `endpoint` must be an `https://` URL on a public host — private, loopback and link-local targets are refused with 400 (SSRF), at write time and again at send time. |
499
549
  | `GET` | `/support/auto-close` | `x-cron-secret` | `autoClose` | Remind, then close, inactive tickets. |
500
550
  | `POST` | `/support/process-snooze` | `x-cron-secret` | `snooze` | Wake snoozed tickets. |
501
551
  | `POST` | `/support/process-scheduled` | `x-cron-secret` | `scheduledReplies` | Release scheduled replies. |
@@ -614,16 +664,17 @@ import type { SupportPluginConfig, SupportFeatures } from '@consilioweb/payload-
614
664
  | Requirement | Range | Source |
615
665
  |---|---|---|
616
666
  | Node.js | `>=20.9.0` | `engines.node` |
617
- | Payload | `^3.37.0` | `peerDependencies.payload` |
618
- | `@payloadcms/next` | `^3.37.0` | `peerDependencies` |
667
+ | Payload | `^3.79.1` | `peerDependencies.payload` |
668
+ | `@payloadcms/next` | `^3.79.1` | `peerDependencies` |
619
669
  | Next.js | `^15.2.9 \|\| ^16.0.0` | `peerDependencies` |
620
670
  | React / React DOM | `^19.0.0` | `peerDependencies` |
621
- | `lucide-react` | `>=0.300.0` | `peerDependencies` |
622
671
  | `@anthropic-ai/sdk` | any recent version | Runtime-only, install it yourself if you use the AI features |
623
672
  | `@payloadcms/richtext-lexical` | any 3.x | Only if you mount the portal FAQ page |
624
673
 
625
- Installing on Node 18, React 18, Next 14 or Payload < 3.37 warns, and fails outright under
626
- `engine-strict` or a strict peer resolver.
674
+ Installing on Node 18, React 18, Next 14 or Payload < 3.79.1 warns, and fails outright under
675
+ `engine-strict` or a strict peer resolver. Payload below 3.79.1 is additionally vulnerable to a
676
+ pre-authentication account takeover (GHSA-hp5w-3hxx-vmwf) and to a SQL injection: upgrade rather
677
+ than force the install.
627
678
 
628
679
  ## Security
629
680
 
@@ -657,7 +708,9 @@ Installing on Node 18, React 18, Next 14 or Payload < 3.37 warns, and fails outr
657
708
  - **Bounded inbound email** payloads and attachments.
658
709
  - **Bounded outbound mail** — ticket transfers and collaborator invitations are capped per user over
659
710
  a long window, not only per ticket (a client can create tickets at will).
660
- - **SSRF-guarded outbound webhooks** — scheme, literal host, resolved address and every redirect hop.
711
+ - **SSRF-guarded outbound requests** — scheme, literal host, resolved address and every redirect hop
712
+ for webhooks; scheme, literal host and resolved address for Web Push subscription endpoints, which
713
+ `web-push` would otherwise hand host and port straight to `https.request`.
661
714
  - **2FA also enforced on the Google OAuth path**, which mints its session outside `payload.login`
662
715
  and therefore outside the `beforeLogin` hook.
663
716
 
@@ -687,6 +740,106 @@ does:
687
740
  | Client time totals | The CRM sums the per-ticket `totalTimeMinutes` rollup instead of scanning `time-entries`. |
688
741
  | AI synthesis | Persisted on the ticket (`aiSummary`, `aiSummaryGeneratedAt`) and reused until forced. |
689
742
 
743
+ ## Personal data, retention and uninstall
744
+
745
+ This plugin is a support desk: most of what it stores is personal data, and you are the controller
746
+ of it. This section is what you need to fill in your record of processing activities.
747
+
748
+ ### What the plugin stores
749
+
750
+ It registers **up to 25 collections** (14 always, 11 behind a feature flag — see
751
+ [Options that change the schema](#options-that-change-the-schema)). The ones holding personal data:
752
+
753
+ | Collection | Personal data it holds |
754
+ |---|---|
755
+ | `support-clients` | Identity, email, phone, company, hashed password, Google id, 2FA state, notification preferences |
756
+ | `tickets` | Subject and description written by the client, assignment, billing amounts |
757
+ | `ticket-messages` | Full conversation, internal notes, attachments, sender addresses |
758
+ | `ticket-collaborators` | Email of every invited person, whether or not they have an account |
759
+ | `ticket-activity-log` | Who did what and when, on every ticket |
760
+ | `satisfaction-surveys`, `ticket-feedback` | Rating and free-text comment, attributed |
761
+ | `chat-messages` | Live-chat transcripts |
762
+ | `pending-emails` | Raw inbound emails, sender address, attachments |
763
+ | `auth-logs` | Login attempts: email, normalized IP, user agent |
764
+ | `email-logs` | Sender and recipient addresses, subjects, delivery and open events |
765
+ | `client-summaries` | **Derived** data: an AI profile of the client, computed from their history |
766
+ | `push-subscriptions` | Agent browser endpoints and user agent |
767
+ | `time-entries` | Which agent spent how long on what |
768
+ | `notification-queue` | Pending notifications addressed to a client |
769
+
770
+ Two things live **outside** those collections:
771
+
772
+ - **Attachments are stored in your own `media` collection**, because that is the `relationTo` of the
773
+ attachment fields on `ticket-messages` and `pending-emails`. They are not the plugin's to manage,
774
+ but `POST /support/delete-account` does delete the files attached to a deleted client's tickets.
775
+ - **Three `payload-preferences` keys** are written by the plugin: `support-settings` (instance
776
+ settings), `support-user-prefs-<userId>` (one row per agent) and `support-round-robin` (assignment
777
+ cursor). None of them holds client data, but the per-agent key carries a staff account id.
778
+
779
+ ### Retention
780
+
781
+ Only the two journals are purged automatically. Everything else lives as long as the ticket it
782
+ belongs to, and is deleted when the client account is.
783
+
784
+ | Data | Default | How to change it |
785
+ |---|---|---|
786
+ | `auth-logs` | **180 days** | `retention.authLogsDays`. CNIL guidance accepts six months for connection logs; do not shorten it "to be safe" — these are the records you investigate an intrusion with. |
787
+ | `email-logs` | **365 days** | `retention.emailLogsDays` |
788
+ | Everything else | Until the ticket or the account is deleted | `POST /support/delete-account` |
789
+
790
+ **The purge is opt-in: nothing is registered until you pass `retention`.** Pass `retention: {}` to
791
+ accept the defaults, or override any of the four keys. Once enabled it runs as a Payload Jobs task
792
+ (`support-purge-logs`), by default daily at 03:30, so it survives a serverless deploy and does not
793
+ run N times in parallel behind N instances. It refuses a retention of `0` — that value disables the
794
+ journal's purge. The manual, admin-only
795
+ `DELETE /api/support/purge-logs?collection=auth-logs&days=0` still wipes a journal entirely; that
796
+ one is a deliberate one-off action, never a schedule.
797
+
798
+ Why opt-in, when a retention policy nobody enables is exactly how the manual purge endpoint ended
799
+ up never being called: declaring the task **enables Payload's job queue**, which adds the
800
+ `payload-jobs` collection and the `payload-jobs-stats` global — two tables your app may not have
801
+ had. A support plugin should not impose a schema change on every install, and defaulting it on
802
+ would buy nothing anyway, since the task only fires when you also run a job runner. If you enable
803
+ it, generate a migration for those two. If you would rather not, leave `retention` unset and call
804
+ the purge endpoint from your own cron — the retention periods below still describe what you should
805
+ be deleting.
806
+
807
+ Payload's job runner has to be running for the task to fire — `jobs.autoRun` in your config, or a
808
+ cron hitting `/api/payload-jobs/run`. If you do not run it, nothing is purged and the endpoint is
809
+ your only lever.
810
+
811
+ ### Cookies and trackers
812
+
813
+ `features.emailTracking` is **on by default** and inserts a 1×1 open-tracking pixel in outgoing
814
+ notification emails (`GET /support/track-open`). Under article 82 of the French *Loi Informatique
815
+ et Libertés* that is a tracker and needs consent unless it falls in an exemption; it is your call,
816
+ not the plugin's. It writes no IP address and requires a valid HMAC, but it does record that a
817
+ given recipient opened a given email, at a given time. Set `features: { emailTracking: false }` to
818
+ turn the pixel — and the `email-logs` collection — off entirely.
819
+
820
+ ### Data subject requests
821
+
822
+ | Right | Endpoint |
823
+ |---|---|
824
+ | Access (art. 15) and portability (art. 20) | `GET /support/export-data`. The JSON is split in two: `providedByYou` (art. 20) and `derivedData` (art. 15 — the AI client summary, which portability does not cover). |
825
+ | Erasure (art. 17) | `POST /support/delete-account`. Deletes the account and every row above that names it, plus the attachments on its tickets, in a single transaction. |
826
+
827
+ ### Uninstall
828
+
829
+ Removing the plugin from `payload.config.ts` stops the collections from being registered; it does
830
+ **not** drop their tables, and the rows stay in the database.
831
+
832
+ ```bash
833
+ npx support-uninstall # dry run: lists what would be deleted
834
+ npx support-uninstall --confirm # deletes the rows through the Payload local API
835
+ npx support-uninstall --confirm --keep-data # only removes the payload-preferences keys
836
+ ```
837
+
838
+ The script deletes rows, not tables: run `payload migrate:create` after it and apply the migration
839
+ to drop the now-unused tables. It never touches your `media` collection — attachments uploaded
840
+ through the support desk are indistinguishable from your other uploads once the messages are gone,
841
+ so it lists them as a manual decision instead of guessing.
842
+
690
843
  ## Troubleshooting
691
844
 
692
845
  **`useServerFunctions must be used within ServerFunctionsProvider`** — align every `@payloadcms/*`
@@ -698,10 +851,12 @@ inserts) and add `busyTimeout: 10000` to the SQLite adapter.
698
851
  **Admin views do not load** — regenerate the import map (`pnpm payload generate:importmap`). In a
699
852
  headless context, pass `skipViews: true`.
700
853
 
701
- **New fields or collections missing in production** — standalone builds do not run migrations. Push
702
- the schema before deploying, including `googleId`, `twoFactorVerifiedAt`, `slaPausedAt`, `nps`,
703
- `mentions`, and the `notification-queue`, `automation-rules`, `ticket-collaborators`,
704
- `support-counters` collections.
854
+ **New fields or collections missing in production** — a production build never synchronises the
855
+ schema: the adapter skips `push` as soon as `NODE_ENV=production`. Generate a migration in
856
+ development (`payload migrate:create`) and run `payload migrate` on the target, or set
857
+ `prodMigrations` on the adapter. Do **not** try to `push` in production, and do not migrate a
858
+ database that was previously dev-pushed without reading the data-loss warning
859
+ `@payloadcms/drizzle` prints. See [Database and updates](#database-and-updates).
705
860
 
706
861
  **`Cannot find module '@anthropic-ai/sdk'`** — install it in your app; the plugin does not bundle or
707
862
  declare it.
@@ -723,11 +878,84 @@ type but not implemented: at runtime everything that is not `ollama` goes to Ant
723
878
  (`SupportPluginConfig`, `SupportFeatures`, `TicketData`, …). All four export subpaths ship
724
879
  declarations.
725
880
 
726
- **How do I migrate the schema in production?** — in standalone mode the schema is not
727
- auto-migrated: generate and push it before deploying. See [Troubleshooting](#troubleshooting).
881
+ **How do I migrate the schema in production?** — `payload migrate:create` in development, commit
882
+ the generated file, then `payload migrate` on the target (or `prodMigrations` on the adapter).
883
+ `push` is a development mechanism and is skipped in production. This plugin ships no migrations of
884
+ its own and cannot: `payload migrate` only ever reads your application's migration directory. See
885
+ [Database and updates](#database-and-updates).
728
886
 
729
887
  ## Upgrading
730
888
 
889
+ ### 5.x → 6.0
890
+
891
+ Two changes need an action; everything else in 6.0 is additive.
892
+
893
+ **`GET /support/export-data` changed shape.** `profile`, `tickets`, `messages` and `surveys` now
894
+ sit under `providedByYou`, and a `derivedData` section carries what the service inferred rather
895
+ than what the person supplied — the distinction GDPR article 20 (portability, provided data) and
896
+ article 15 (access, all data) actually make. If anything of yours parses that response, read the
897
+ new keys. The endpoint is the only one whose shape moved.
898
+
899
+ **`lucide-react` is no longer a peer dependency.** The twenty icons are inlined, so the package no
900
+ longer requires you to install an icon library. Nothing breaks if you keep it — it is simply not
901
+ requested any more. Remove it from your own dependencies only if nothing else of yours uses it.
902
+
903
+ Two smaller notes. The exported `V` colour tokens changed value: `amber`, `orange`, `green` (and
904
+ the `yellow` alias) are darker, and `textSecondary` moved off `--theme-elevation-500`, which failed
905
+ WCAG AA as a foreground. If you render your own UI with `V`, expect a visual shift, not a break.
906
+ And the log retention purge is **opt-in**: it registers nothing unless you pass `retention`. See
907
+ [Personal data, retention and uninstall](#personal-data-retention-and-uninstall) for why, and for
908
+ what to run instead if you would rather purge from your own cron.
909
+
910
+ ### 4.x → 5.0
911
+
912
+ **No schema change: this release only touches access rules and hooks. Do not generate a
913
+ migration.** The field set of 5.0.0 is identical to 4.0.0's.
914
+
915
+ Data actions to take:
916
+
917
+ 1. **Audit your access logs for `<adminRoute>/support/*` hits from accounts that are neither agents
918
+ nor `support-clients`.** Until 5.0.0 the thirteen admin views only tested `!req.user`, so any
919
+ authenticated account of any auth collection reached the admin chrome and the client config
920
+ Payload builds for an authenticated request. The support data itself never travelled — the views
921
+ fetch through `/api/support/*`, which was guarded — but the shape of your CMS did.
922
+ 2. **Re-subscribe any agent whose Web Push endpoint is not `https://` on a public host.**
923
+ `POST /support/push/subscribe` now answers `400` for those, and `sendPushToUser` skips rows
924
+ already stored that fail the same check. Agents who subscribed through a local tunnel or an
925
+ `http://` origin stop receiving notifications until they subscribe again.
926
+ 3. **Expect the rate-limit counters to restart once.** Keys are now normalized IP literals, so rows
927
+ already in `support-rate-limits` no longer match. This is a one-off reset, not a data loss.
928
+ 4. **A custom portal must forward `challenge`** from every `POST /support/2fa {action:'send'}`
929
+ reply to the following `verify`; the bundled portal already does.
930
+
931
+ ### 3.x → 4.0
932
+
933
+ **No schema change: this release only touches access rules and hooks. Do not generate a
934
+ migration.** The field set of 4.0.0 is identical to 3.0.0's.
935
+
936
+ Data actions to take:
937
+
938
+ 1. **Audit every `webhook-endpoints` row and re-point the ones that are not `https://` on a public
939
+ host.** 4.0.0 validates the URL at save time only, so a row saved earlier stays editable and
940
+ still shows in the admin — it simply stops firing, and the dispatcher records `lastStatus: 0` on
941
+ it. Nothing tells you which rows are in that state, so list them:
942
+ `payload.find({ collection: 'webhook-endpoints' })` and look for `http://`, a private, loopback
943
+ or link-local host. `SUPPORT_ALLOW_INSECURE_WEBHOOKS=1` re-allows `http://` for local
944
+ development only.
945
+ 2. **Review the collaborator rows created before 4.0.0.** A row with no explicit `role` now defaults
946
+ to `viewer`, which is read-only: an invitee who used to post messages stops being able to. Set
947
+ their row to `collaborator` to restore it.
948
+ 3. **Expect the rate-limit counters to restart once.** `RateLimiter` now namespaces every key and
949
+ keys authenticated callers as `<collection>:<id>`, so rows already in `support-rate-limits` no
950
+ longer match. One-off reset.
951
+ 4. **Drop `cookieState` from a hand-written Google OAuth button.** `{ action: 'login' }` now sets an
952
+ `HttpOnly` `support-oauth-state` cookie; the callback must let the browser carry it
953
+ (`credentials: 'include'` cross-origin).
954
+ 5. **A dashboard calling `GET /support/email-stats` with a portal client session now gets `403`** —
955
+ it is staff-only.
956
+ 6. **If your app replaces `config.custom` wholesale after the plugin runs**, it strips
957
+ `custom.supportStaffCollection` and the settings reads lose the staff slug they compare against.
958
+
731
959
  ### 2.x → 3.0
732
960
 
733
961
  1. **Rotate the secret of every webhook endpoint.** The legacy transport sent it in clear in
@@ -741,8 +969,9 @@ auto-migrated: generate and push it before deploying. See [Troubleshooting](#tro
741
969
  recomputing it — so a stale total keeps over-billing until something touches a time entry on that
742
970
  ticket.
743
971
  5. **Import `./views` and `./components/TicketConversation` with ESM** — their CJS builds are gone.
744
- 6. Check your platform: Node 20.9+, React 19, Next 15.2.9+, Payload 3.37+, and install
745
- `lucide-react` explicitly if you were relying on it being optional.
972
+ 6. Check your platform: Node 20.9+, React 19, Next 15.2.9+, Payload 3.79.1+, and install
973
+ `lucide-react` explicitly if you were relying on it being optional. (It has since stopped being
974
+ a peer dependency altogether — the icons are inlined.)
746
975
 
747
976
  ### 1.x → 2.0
748
977
 
@@ -763,7 +992,7 @@ auto-migrated: generate and push it before deploying. See [Troubleshooting](#tro
763
992
  3. Commit your changes (`git commit -m 'feat: add AmazingFeature'`).
764
993
  4. Push the branch and open a Pull Request.
765
994
 
766
- Run the checks before submitting — the suite is 179 vitest tests:
995
+ Run the checks before submitting — the suite is 391 vitest tests:
767
996
 
768
997
  ```bash
769
998
  pnpm typecheck && pnpm test && pnpm build
@@ -781,6 +1010,14 @@ If this plugin saves you time, consider [buying me a coffee](https://buymeacoffe
781
1010
 
782
1011
  MIT — see [LICENSE](LICENSE).
783
1012
 
1013
+ **Runtime dependencies.** The package has exactly three, declared in `package.json`: `pdfkit`
1014
+ (^0.19.1, MIT), `sanitize-html` (^2.17.7, MIT) and `web-push` (^3.6.7, **MPL-2.0**). MPL-2.0 is the
1015
+ only reciprocal licence in that set, and its copyleft is per **file**, not per work: §3.3 lets a
1016
+ Larger Work be distributed under other terms. `payload-support` neither modifies nor bundles
1017
+ `web-push` — it imports it as an external dependency, which you can check in `dist/index.js:5` and
1018
+ `dist/index.cjs:7` — so the obligation is already met by npm shipping `web-push` with its own
1019
+ licence. The licence of this plugin is unaffected: it stays MIT.
1020
+
784
1021
  Built and maintained by [ConsilioWEB](https://consilioweb.fr) ·
785
1022
  [Issues](https://github.com/pOwn3d/payload-support/issues) ·
786
1023
  [npm](https://www.npmjs.com/package/@consilioweb/payload-support)
@@ -255,7 +255,6 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
255
255
  fontSize: "14px",
256
256
  lineHeight: 1.6,
257
257
  color: "#1f2937",
258
- outline: "none",
259
258
  overflowY: "auto"
260
259
  }
261
260
  }
@@ -268,6 +267,7 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
268
267
  type: "file",
269
268
  accept: "image/*",
270
269
  multiple: true,
270
+ "aria-label": "Inserer une image",
271
271
  onChange: handleFileChange,
272
272
  style: { display: "none" }
273
273
  }
@@ -80,7 +80,7 @@ const RewriteDropdown = ({ disabled, loading, onSelect }) => {
80
80
  },
81
81
  children: [
82
82
  /* @__PURE__ */ jsx("span", { style: { fontSize: 12, fontWeight: 600, color: "var(--theme-text, #111)" }, children: style.label }),
83
- /* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "var(--theme-elevation-500, #6b7280)" }, children: style.desc })
83
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "var(--theme-elevation-650, #6b7280)" }, children: style.desc })
84
84
  ]
85
85
  },
86
86
  style.id
@@ -31,6 +31,7 @@ function AISummaryPanel({
31
31
  /* @__PURE__ */ jsx(
32
32
  "button",
33
33
  {
34
+ type: "button",
34
35
  onClick: () => {
35
36
  setShowAiSummary(!showAiSummary);
36
37
  if (!showAiSummary && !aiSummary) handleAiGenerate();
@@ -58,6 +59,7 @@ function AISummaryPanel({
58
59
  /* @__PURE__ */ jsx(
59
60
  "button",
60
61
  {
62
+ type: "button",
61
63
  onClick: handleAiGenerate,
62
64
  disabled: aiGenerating,
63
65
  style: { ...s.outlineBtn("#17807c", aiGenerating), fontSize: "11px", padding: "4px 10px" },
@@ -67,6 +69,7 @@ function AISummaryPanel({
67
69
  aiSummary && !aiGenerating && /* @__PURE__ */ jsx(
68
70
  "button",
69
71
  {
72
+ type: "button",
70
73
  onClick: handleAiSave,
71
74
  disabled: aiSaving || aiSaved,
72
75
  style: { ...s.btn(aiSaved ? "#16a34a" : "#2563eb", aiSaving), fontSize: "11px", padding: "4px 10px" },
@@ -21,6 +21,7 @@ function MergePanel({
21
21
  "input",
22
22
  {
23
23
  type: "text",
24
+ "aria-label": "Numero du ticket cible",
24
25
  value: mergeTarget,
25
26
  onChange: (e) => {
26
27
  setMergeTarget(e.target.value);
@@ -31,7 +32,7 @@ function MergePanel({
31
32
  style: { ...s.input, width: "130px" }
32
33
  }
33
34
  ),
34
- /* @__PURE__ */ jsx("button", { onClick: handleMergeLookup, style: { ...s.outlineBtn("#ec4899"), fontSize: "12px", padding: "6px 14px" }, children: "Rechercher" }),
35
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: handleMergeLookup, style: { ...s.outlineBtn("#ec4899"), fontSize: "12px", padding: "6px 14px" }, children: "Rechercher" }),
35
36
  mergeError && /* @__PURE__ */ jsx("span", { style: { fontSize: "12px", color: "#be185d", fontWeight: 600 }, children: mergeError })
36
37
  ] }),
37
38
  mergeTargetInfo && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "center" }, children: [
@@ -40,7 +41,7 @@ function MergePanel({
40
41
  " \u2014 ",
41
42
  mergeTargetInfo.subject
42
43
  ] }),
43
- /* @__PURE__ */ jsx("button", { onClick: handleMerge, disabled: merging, style: { ...s.btn("#ec4899", merging), color: "#fff", fontSize: "12px", padding: "6px 14px" }, children: merging ? "Fusion..." : "Confirmer la fusion" })
44
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: handleMerge, disabled: merging, style: { ...s.btn("#ec4899", merging), color: "#fff", fontSize: "12px", padding: "6px 14px" }, children: merging ? "Fusion..." : "Confirmer la fusion" })
44
45
  ] })
45
46
  ] });
46
47
  }
@@ -63,6 +64,7 @@ function ExtMessagePanel({
63
64
  /* @__PURE__ */ jsx(
64
65
  "textarea",
65
66
  {
67
+ "aria-label": "Contenu du message recu",
66
68
  value: extMsgBody,
67
69
  onChange: (e) => setExtMsgBody(e.target.value),
68
70
  placeholder: "Coller le contenu du message re\xE7u...",
@@ -71,7 +73,7 @@ function ExtMessagePanel({
71
73
  }
72
74
  ),
73
75
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap", marginBottom: "10px" }, children: [
74
- /* @__PURE__ */ jsxs("select", { value: extMsgAuthor, onChange: (e) => setExtMsgAuthor(e.target.value), style: { ...s.input, fontSize: "12px" }, children: [
76
+ /* @__PURE__ */ jsxs("select", { "aria-label": "Auteur du message recu", value: extMsgAuthor, onChange: (e) => setExtMsgAuthor(e.target.value), style: { ...s.input, fontSize: "12px" }, children: [
75
77
  /* @__PURE__ */ jsx("option", { value: "client", children: "Envoy\xE9 par le client" }),
76
78
  /* @__PURE__ */ jsx("option", { value: "admin", children: "Envoy\xE9 par le support" })
77
79
  ] }),
@@ -79,19 +81,20 @@ function ExtMessagePanel({
79
81
  "input",
80
82
  {
81
83
  type: "datetime-local",
84
+ "aria-label": "Date du message recu",
82
85
  value: extMsgDate,
83
86
  onChange: (e) => setExtMsgDate(e.target.value),
84
87
  style: { ...s.input, fontSize: "12px" }
85
88
  }
86
89
  ),
87
- /* @__PURE__ */ jsx("input", { ref: extFileInputRef, type: "file", multiple: true, onChange: handleExtFileChange, style: { display: "none" }, accept: "image/*,.pdf,.doc,.docx,.txt,.zip" }),
90
+ /* @__PURE__ */ jsx("input", { ref: extFileInputRef, type: "file", multiple: true, "aria-label": "Joindre des fichiers au message recu", onChange: handleExtFileChange, style: { display: "none" }, accept: "image/*,.pdf,.doc,.docx,.txt,.zip" }),
88
91
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => extFileInputRef.current?.click(), style: { ...s.ghostBtn("#6b7280"), fontSize: "12px", padding: "6px 12px" }, children: "+ PJ" })
89
92
  ] }),
90
93
  extMsgFiles.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginBottom: "10px", display: "flex", gap: "6px", flexWrap: "wrap" }, children: extMsgFiles.map((file, i) => /* @__PURE__ */ jsxs("span", { style: { ...s.badge("#f1f5f9", "#374151"), display: "inline-flex", alignItems: "center", gap: "4px" }, children: [
91
94
  file.name,
92
95
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setExtMsgFiles((prev) => prev.filter((_, idx) => idx !== i)), style: { border: "none", background: "none", color: "#ef4444", fontWeight: 700, cursor: "pointer", fontSize: "14px", lineHeight: 1 }, children: "\xD7" })
93
96
  ] }, i)) }),
94
- /* @__PURE__ */ jsx("button", { onClick: handleSendExtMsg, disabled: sendingExtMsg || !extMsgBody.trim(), style: s.btn("#1d2b4d", sendingExtMsg || !extMsgBody.trim()), children: sendingExtMsg ? "Ajout..." : "Ajouter (sans notification)" })
97
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: handleSendExtMsg, disabled: sendingExtMsg || !extMsgBody.trim(), style: s.btn("#1d2b4d", sendingExtMsg || !extMsgBody.trim()), children: sendingExtMsg ? "Ajout..." : "Ajouter (sans notification)" })
95
98
  ] });
96
99
  }
97
100
  const REMINDER_DELAYS = [
@@ -157,6 +160,7 @@ function ReminderPanel({
157
160
  /* @__PURE__ */ jsx(
158
161
  "button",
159
162
  {
163
+ type: "button",
160
164
  onClick: handleSendReminder,
161
165
  disabled: reminderSending || !clientEmail,
162
166
  style: { ...s.btn(C.orange, reminderSending || !clientEmail), color: "#fff", fontSize: "13px", padding: "8px 18px" },
@@ -169,13 +173,14 @@ function SnoozePanel({ snoozeSaving, handleSnooze }) {
169
173
  return /* @__PURE__ */ jsxs("div", { style: { padding: "14px 18px", borderRadius: "8px", backgroundColor: "#eef8f7", border: "1px solid #b8dcda", marginBottom: "14px" }, children: [
170
174
  /* @__PURE__ */ jsx("h4", { style: { fontSize: "13px", fontWeight: 600, marginBottom: "10px", color: "#5b21b6" }, children: "Snooze \u2014 masquer temporairement" }),
171
175
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap", alignItems: "center" }, children: [
172
- /* @__PURE__ */ jsx("button", { onClick: () => handleSnooze(1), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "1 jour" }),
173
- /* @__PURE__ */ jsx("button", { onClick: () => handleSnooze(3), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "3 jours" }),
174
- /* @__PURE__ */ jsx("button", { onClick: () => handleSnooze(7), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "1 semaine" }),
176
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => handleSnooze(1), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "1 jour" }),
177
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => handleSnooze(3), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "3 jours" }),
178
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => handleSnooze(7), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "1 semaine" }),
175
179
  /* @__PURE__ */ jsx(
176
180
  "input",
177
181
  {
178
182
  type: "datetime-local",
183
+ "aria-label": "Snoozer jusqu'a une date precise",
179
184
  onChange: (e) => {
180
185
  if (e.target.value) handleSnooze(null, e.target.value);
181
186
  },
@@ -113,6 +113,7 @@ function ClientHistory({
113
113
  /* @__PURE__ */ jsx(
114
114
  "textarea",
115
115
  {
116
+ "aria-label": "Notes internes sur le client",
116
117
  value: clientNotes,
117
118
  onChange: (e) => onNotesChange(e.target.value),
118
119
  rows: 3,
@@ -83,6 +83,7 @@ function SingleCodeBlock({ lang, code }) {
83
83
  /* @__PURE__ */ jsx(
84
84
  "button",
85
85
  {
86
+ type: "button",
86
87
  onClick: handleCopy,
87
88
  style: {
88
89
  background: "none",