@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.
- package/README.md +272 -35
- package/dist/components/RichTextEditor/index.js +1 -1
- package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
- package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
- package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
- package/dist/components/TicketConversation/components/QuickActions.js +10 -8
- package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
- package/dist/components/TicketConversation/index.d.ts +10 -0
- package/dist/components/TicketConversation/index.js +25 -7
- package/dist/index.cjs +421 -73
- package/dist/index.d.cts +127 -38
- package/dist/index.d.ts +127 -38
- package/dist/index.js +415 -72
- package/dist/styles/BillingView.module.scss +11 -11
- package/dist/styles/ChatView.module.scss +7 -7
- package/dist/styles/CommandPalette.module.scss +1 -1
- package/dist/styles/CrmView.module.scss +9 -9
- package/dist/styles/EmailTracking.module.scss +6 -6
- package/dist/styles/ImportConversation.module.scss +5 -5
- package/dist/styles/Logs.module.scss +5 -5
- package/dist/styles/NewTicket.module.scss +2 -2
- package/dist/styles/PendingEmails.module.scss +13 -13
- package/dist/styles/SupportDashboard.module.scss +24 -8
- package/dist/styles/TicketDetail.module.scss +20 -20
- package/dist/styles/TicketInbox.module.scss +6 -6
- package/dist/styles/TicketingSettings.module.scss +10 -10
- package/dist/styles/TimeDashboard.module.scss +6 -6
- package/dist/styles/_tokens.scss +3 -1
- package/dist/utils/db.d.ts +34 -0
- package/dist/utils/readSettings.d.ts +90 -0
- package/dist/views/BillingView/client.js +15 -8
- package/dist/views/BillingView/index.js +4 -4
- package/dist/views/ChatView/client.js +2 -0
- package/dist/views/ChatView/index.js +4 -4
- package/dist/views/CrmView/client.js +2 -0
- package/dist/views/CrmView/index.js +4 -4
- package/dist/views/EmailTrackingView/client.js +23 -11
- package/dist/views/EmailTrackingView/index.js +4 -4
- package/dist/views/ImportConversationView/client.js +1 -0
- package/dist/views/ImportConversationView/index.js +4 -4
- package/dist/views/LogsView/client.js +1 -1
- package/dist/views/LogsView/index.js +4 -2
- package/dist/views/NewTicketView/client.js +20 -13
- package/dist/views/NewTicketView/index.js +4 -2
- package/dist/views/PendingEmailsView/client.js +9 -3
- package/dist/views/PendingEmailsView/index.js +4 -4
- package/dist/views/SupportDashboardView/client.js +17 -19
- package/dist/views/SupportDashboardView/index.js +4 -4
- package/dist/views/TicketDetailView/client.js +23 -11
- package/dist/views/TicketDetailView/index.js +4 -4
- package/dist/views/TicketInboxView/client.js +52 -26
- package/dist/views/TicketInboxView/index.js +4 -2
- package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/dist/views/TicketingSettingsView/client.js +44 -26
- package/dist/views/TicketingSettingsView/index.js +4 -4
- package/dist/views/TimeDashboardView/client.js +10 -7
- package/dist/views/TimeDashboardView/index.js +4 -4
- package/dist/views/shared/ErrorBoundary.d.ts +36 -1
- package/dist/views/shared/ErrorBoundary.js +59 -28
- package/dist/views/shared/adminTokens.d.ts +15 -5
- package/dist/views/shared/adminTokens.js +23 -7
- package/dist/views/shared/icons.d.ts +49 -0
- package/dist/views/shared/icons.js +92 -0
- package/dist/views/shared/locales/en.json +15 -1
- package/dist/views/shared/locales/fr.json +15 -1
- package/dist/views/shared/viewAccess.d.ts +29 -0
- package/dist/views/shared/viewAccess.js +24 -0
- package/package.json +31 -21
- package/scripts/uninstall-data.mjs +178 -0
- package/scripts/uninstall.mjs +184 -0
- package/src/collections/Tickets.ts +7 -0
- package/src/collections/TimeEntries.ts +11 -3
- package/src/components/RichTextEditor/index.tsx +1 -1
- package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
- package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
- package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
- package/src/components/TicketConversation/index.tsx +41 -7
- package/src/endpoints/auth-2fa.ts +53 -8
- package/src/endpoints/capabilities.ts +2 -4
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/delete-account.ts +191 -47
- package/src/endpoints/export-data.ts +104 -4
- package/src/endpoints/import-conversation.ts +2 -2
- package/src/endpoints/login.ts +13 -3
- package/src/endpoints/purge-logs.ts +6 -15
- package/src/endpoints/push.ts +14 -1
- package/src/endpoints/statuses.ts +17 -0
- package/src/index.ts +2 -0
- package/src/plugin.ts +51 -1
- package/src/portal/LiveChat.tsx +1 -1
- package/src/portal/auth/profile/page.tsx +3 -2
- package/src/portal/icons.tsx +91 -0
- package/src/portal/login/page.tsx +14 -4
- package/src/styles/BillingView.module.scss +11 -11
- package/src/styles/ChatView.module.scss +7 -7
- package/src/styles/CommandPalette.module.scss +1 -1
- package/src/styles/CrmView.module.scss +9 -9
- package/src/styles/EmailTracking.module.scss +6 -6
- package/src/styles/ImportConversation.module.scss +5 -5
- package/src/styles/Logs.module.scss +5 -5
- package/src/styles/NewTicket.module.scss +2 -2
- package/src/styles/PendingEmails.module.scss +13 -13
- package/src/styles/SupportDashboard.module.scss +24 -8
- package/src/styles/TicketDetail.module.scss +20 -20
- package/src/styles/TicketInbox.module.scss +6 -6
- package/src/styles/TicketingSettings.module.scss +10 -10
- package/src/styles/TimeDashboard.module.scss +6 -6
- package/src/styles/_tokens.scss +3 -1
- package/src/types.ts +20 -0
- package/src/utils/push.ts +22 -0
- package/src/utils/rateLimiter.ts +98 -0
- package/src/utils/retention.ts +111 -0
- package/src/utils/slugs.ts +15 -0
- package/src/utils/twoFactorChallenge.ts +6 -1
- package/src/utils/urlSafety.ts +36 -1
- package/src/views/BillingView/client.tsx +17 -8
- package/src/views/BillingView/index.tsx +4 -4
- package/src/views/ChatView/client.tsx +2 -0
- package/src/views/ChatView/index.tsx +4 -4
- package/src/views/CrmView/client.tsx +2 -0
- package/src/views/CrmView/index.tsx +4 -4
- package/src/views/EmailTrackingView/client.tsx +18 -7
- package/src/views/EmailTrackingView/index.tsx +4 -4
- package/src/views/ImportConversationView/client.tsx +1 -0
- package/src/views/ImportConversationView/index.tsx +4 -4
- package/src/views/LogsView/client.tsx +1 -1
- package/src/views/LogsView/index.tsx +4 -2
- package/src/views/NewTicketView/client.tsx +22 -13
- package/src/views/NewTicketView/index.tsx +4 -2
- package/src/views/PendingEmailsView/client.tsx +9 -3
- package/src/views/PendingEmailsView/index.tsx +4 -4
- package/src/views/SupportDashboardView/client.tsx +11 -5
- package/src/views/SupportDashboardView/index.tsx +4 -4
- package/src/views/TicketDetailView/client.tsx +25 -11
- package/src/views/TicketDetailView/index.tsx +4 -4
- package/src/views/TicketInboxView/client.tsx +47 -7
- package/src/views/TicketInboxView/index.tsx +4 -2
- package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/src/views/TicketingSettingsView/client.tsx +201 -144
- package/src/views/TicketingSettingsView/index.tsx +4 -4
- package/src/views/TimeDashboardView/client.tsx +10 -7
- package/src/views/TimeDashboardView/index.tsx +4 -4
- package/src/views/shared/ErrorBoundary.tsx +95 -31
- package/src/views/shared/adminTokens.ts +28 -11
- package/src/views/shared/icons.tsx +146 -0
- package/src/views/shared/locales/en.json +15 -1
- package/src/views/shared/locales/fr.json +15 -1
- package/src/views/shared/viewAccess.ts +73 -0
- package/src/types/lucide-react.d.ts +0 -42
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@consilioweb/payload-support)
|
|
6
6
|
[](LICENSE)
|
|
7
|
-
[](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
|
|
83
|
+
**Peer dependencies** — all five are required, none is optional:
|
|
82
84
|
|
|
83
85
|
```bash
|
|
84
|
-
pnpm add payload@^3.
|
|
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.
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
|
149
|
-
`node_modules`, so there is no `./portal` export and `plugin.ts` registers no Next
|
|
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
|
|
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** |
|
|
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
|
-
|
|
199
|
-
|
|
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` |
|
|
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'`
|
|
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.
|
|
618
|
-
| `@payloadcms/next` | `^3.
|
|
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.
|
|
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
|
|
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** —
|
|
702
|
-
|
|
703
|
-
`
|
|
704
|
-
`
|
|
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?** —
|
|
727
|
-
|
|
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.
|
|
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
|
|
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-
|
|
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
|
},
|