@consilioweb/payload-support 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +656 -355
- package/dist/components/RichTextEditor/index.d.ts +19 -0
- package/dist/components/TicketConversation/RewriteDropdown.d.ts +6 -0
- package/dist/components/TicketConversation/SkeletonText.d.ts +4 -0
- package/dist/components/TicketConversation/components/AISummaryPanel.d.ts +14 -0
- package/dist/components/TicketConversation/components/ActionPanels.d.ts +47 -0
- package/dist/components/TicketConversation/components/ActivityLog.d.ts +5 -0
- package/dist/components/TicketConversation/components/ClientBar.d.ts +5 -0
- package/dist/components/TicketConversation/components/ClientHistory.d.ts +24 -0
- package/dist/components/TicketConversation/components/CodeBlock.d.ts +26 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.d.ts +12 -0
- package/dist/components/TicketConversation/components/QuickActions.d.ts +32 -0
- package/dist/components/TicketConversation/components/TicketHeader.d.ts +19 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.d.ts +24 -0
- package/dist/components/TicketConversation/config.d.ts +10 -0
- package/dist/components/TicketConversation/config.js +1 -40
- package/dist/components/TicketConversation/constants.d.ts +56 -0
- package/dist/components/TicketConversation/context.d.ts +53 -0
- package/dist/components/TicketConversation/hooks/useAI.d.ts +21 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.d.ts +3 -0
- package/dist/components/TicketConversation/hooks/useFeatures.d.ts +23 -0
- package/dist/components/TicketConversation/hooks/useFeatures.js +61 -0
- package/dist/components/TicketConversation/hooks/useMessageActions.d.ts +20 -0
- package/dist/components/TicketConversation/hooks/useReply.d.ts +27 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.d.ts +58 -0
- package/dist/components/TicketConversation/hooks/useTimeTracking.d.ts +18 -0
- package/dist/components/TicketConversation/hooks/useTranslation.d.ts +33 -0
- package/dist/components/TicketConversation/hooks/useTranslation.js +22 -3
- package/dist/components/TicketConversation/index.d.ts +4 -0
- package/dist/components/TicketConversation/index.js +2 -2
- package/dist/components/TicketConversation/types.d.ts +56 -0
- package/dist/components/TicketConversation/utils.d.ts +2 -0
- package/dist/index.cjs +392 -320
- package/dist/index.d.cts +100 -3
- package/dist/index.d.ts +100 -3
- package/dist/index.js +388 -321
- package/dist/utils/features.d.ts +123 -0
- package/dist/utils/features.js +147 -0
- package/dist/views/BillingView/client.d.ts +2 -0
- package/dist/views/BillingView/index.d.ts +4 -0
- package/dist/views/ChatView/client.d.ts +2 -0
- package/dist/views/ChatView/client.js +9 -9
- package/dist/views/ChatView/index.d.ts +4 -0
- package/dist/views/CrmView/client.d.ts +2 -0
- package/dist/views/CrmView/client.js +6 -9
- package/dist/views/CrmView/index.d.ts +4 -0
- package/dist/views/EmailTrackingView/client.d.ts +2 -0
- package/dist/views/EmailTrackingView/index.d.ts +4 -0
- package/dist/views/ImportConversationView/client.d.ts +2 -0
- package/dist/views/ImportConversationView/index.d.ts +4 -0
- package/dist/views/LogsView/client.d.ts +2 -0
- package/dist/views/LogsView/index.d.ts +4 -0
- package/dist/views/NewTicketView/client.d.ts +2 -0
- package/dist/views/NewTicketView/index.d.ts +4 -0
- package/dist/views/PendingEmailsView/client.d.ts +2 -0
- package/dist/views/PendingEmailsView/index.d.ts +4 -0
- package/dist/views/SupportDashboardView/client.d.ts +2 -0
- package/dist/views/SupportDashboardView/client.js +1 -1
- package/dist/views/SupportDashboardView/index.d.ts +4 -0
- package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
- package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
- package/dist/views/TicketDetailView/client.d.ts +2 -0
- package/dist/views/TicketDetailView/client.js +2 -2
- package/dist/views/TicketDetailView/constants.d.ts +13 -0
- package/dist/views/TicketDetailView/helpers.d.ts +2 -0
- package/dist/views/TicketDetailView/index.d.ts +4 -0
- package/dist/views/TicketDetailView/types.d.ts +40 -0
- package/dist/views/TicketInboxView/client.d.ts +2 -0
- package/dist/views/TicketInboxView/client.js +5 -5
- package/dist/views/TicketInboxView/index.d.ts +4 -0
- package/dist/views/TicketingSettingsView/client.d.ts +2 -0
- package/dist/views/TicketingSettingsView/client.js +6 -5
- package/dist/views/TicketingSettingsView/index.d.ts +4 -0
- package/dist/views/TimeDashboardView/client.d.ts +2 -0
- package/dist/views/TimeDashboardView/index.d.ts +4 -0
- package/dist/views/shared/AdminViewHeader.d.ts +13 -0
- package/dist/views/shared/ErrorBoundary.d.ts +17 -0
- package/dist/views/shared/Skeleton.d.ts +21 -0
- package/dist/views/shared/StatusPill.d.ts +8 -0
- package/dist/views/shared/adminTokens.d.ts +19 -0
- package/dist/views/shared/config.d.ts +8 -0
- package/dist/views/shared/config.js +1 -40
- package/dist/views/shared/dateLocale.d.ts +5 -0
- package/dist/views/shared/index.d.ts +12 -0
- package/dist/views/shared/index.js +2 -1
- package/dist/views/shared/locales/en.json +708 -0
- package/dist/views/shared/locales/fr.json +708 -0
- package/dist/views/shared/sla.d.ts +14 -0
- package/dist/views.d.ts +13 -13
- package/package.json +26 -41
- package/src/collections/SupportClients.ts +29 -0
- package/src/collections/TicketCollaborators.ts +6 -1
- package/src/collections/TicketMessages.ts +46 -24
- package/src/collections/Tickets.ts +117 -79
- package/src/collections/TimeEntries.ts +57 -32
- package/src/components/TicketConversation/config.ts +16 -80
- package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +38 -4
- package/src/components/TicketConversation/index.tsx +2 -2
- package/src/endpoints/ai.ts +2 -1
- package/src/endpoints/client-intelligence.ts +2 -1
- package/src/endpoints/process-scheduled.ts +13 -8
- package/src/endpoints/round-robin-config.ts +30 -20
- package/src/endpoints/settings.ts +28 -77
- package/src/index.ts +6 -2
- package/src/portal/LiveChat.tsx +33 -22
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
- package/src/types/lucide-react.d.ts +10 -2
- package/src/utils/aiAgent.ts +2 -1
- package/src/utils/aiProvider.ts +21 -0
- package/src/utils/features.ts +297 -0
- package/src/utils/fireWebhooks.ts +6 -0
- package/src/utils/generateTicketSynthesis.ts +2 -1
- package/src/utils/index.ts +4 -2
- package/src/utils/readSettings.ts +82 -9
- package/src/utils/webhookDispatcher.ts +19 -7
- package/src/views/ChatView/client.tsx +13 -9
- package/src/views/CrmView/client.tsx +10 -11
- package/src/views/SupportDashboardView/client.tsx +1 -1
- package/src/views/TicketDetailView/client.tsx +2 -2
- package/src/views/TicketInboxView/client.tsx +10 -5
- package/src/views/TicketingSettingsView/client.tsx +9 -7
- package/src/views/shared/config.ts +14 -80
- package/src/views/shared/index.ts +3 -1
- package/src/views/shared/locales/en.json +2 -2
- package/src/views/shared/locales/fr.json +2 -2
- package/dist/components/RichTextEditor/index.cjs +0 -279
- package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
- package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
- package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
- package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
- package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
- package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
- package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
- package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
- package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
- package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
- package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
- package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
- package/dist/components/TicketConversation/config.cjs +0 -44
- package/dist/components/TicketConversation/constants.cjs +0 -127
- package/dist/components/TicketConversation/context.cjs +0 -13
- package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
- package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
- package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
- package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
- package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
- package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -78
- package/dist/components/TicketConversation/index.cjs +0 -1114
- package/dist/components/TicketConversation/types.cjs +0 -2
- package/dist/components/TicketConversation/utils.cjs +0 -27
- package/dist/views/BillingView/client.cjs +0 -469
- package/dist/views/BillingView/index.cjs +0 -34
- package/dist/views/ChatView/client.cjs +0 -301
- package/dist/views/ChatView/index.cjs +0 -34
- package/dist/views/CrmView/client.cjs +0 -458
- package/dist/views/CrmView/index.cjs +0 -34
- package/dist/views/EmailTrackingView/client.cjs +0 -171
- package/dist/views/EmailTrackingView/index.cjs +0 -34
- package/dist/views/ImportConversationView/client.cjs +0 -238
- package/dist/views/ImportConversationView/index.cjs +0 -34
- package/dist/views/LogsView/client.cjs +0 -148
- package/dist/views/LogsView/index.cjs +0 -32
- package/dist/views/NewTicketView/client.cjs +0 -213
- package/dist/views/NewTicketView/index.cjs +0 -32
- package/dist/views/PendingEmailsView/client.cjs +0 -471
- package/dist/views/PendingEmailsView/index.cjs +0 -34
- package/dist/views/SupportDashboardView/client.cjs +0 -437
- package/dist/views/SupportDashboardView/index.cjs +0 -34
- package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
- package/dist/views/TicketDetailView/client.cjs +0 -1412
- package/dist/views/TicketDetailView/constants.cjs +0 -20
- package/dist/views/TicketDetailView/helpers.cjs +0 -23
- package/dist/views/TicketDetailView/index.cjs +0 -34
- package/dist/views/TicketDetailView/types.cjs +0 -2
- package/dist/views/TicketInboxView/client.cjs +0 -359
- package/dist/views/TicketInboxView/index.cjs +0 -32
- package/dist/views/TicketingSettingsView/client.cjs +0 -740
- package/dist/views/TicketingSettingsView/index.cjs +0 -34
- package/dist/views/TimeDashboardView/client.cjs +0 -180
- package/dist/views/TimeDashboardView/index.cjs +0 -34
- package/dist/views/shared/AdminViewHeader.cjs +0 -68
- package/dist/views/shared/ErrorBoundary.cjs +0 -55
- package/dist/views/shared/Skeleton.cjs +0 -77
- package/dist/views/shared/StatusPill.cjs +0 -47
- package/dist/views/shared/adminTokens.cjs +0 -34
- package/dist/views/shared/config.cjs +0 -44
- package/dist/views/shared/dateLocale.cjs +0 -5
- package/dist/views/shared/index.cjs +0 -72
- package/dist/views/shared/sla.cjs +0 -37
- package/dist/views.cjs +0 -86
- package/dist/views.d.cts +0 -13
- package/src/__tests__/aiSummaryRendering.test.ts +0 -21
- package/src/__tests__/authResponses.test.ts +0 -69
- package/src/__tests__/capabilities.test.ts +0 -71
- package/src/__tests__/generateTrackingToken.test.ts +0 -67
- package/src/__tests__/integration/ai-agent.test.ts +0 -46
- package/src/__tests__/integration/auth.test.ts +0 -43
- package/src/__tests__/integration/automation-rules.test.ts +0 -68
- package/src/__tests__/integration/buildTestPayload.ts +0 -52
- package/src/__tests__/integration/channels.test.ts +0 -52
- package/src/__tests__/integration/core-behaviors.test.ts +0 -81
- package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
- package/src/__tests__/integration/digest.test.ts +0 -48
- package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
- package/src/__tests__/integration/isolation.test.ts +0 -81
- package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
- package/src/__tests__/integration/nps.test.ts +0 -37
- package/src/__tests__/integration/per-team-sla.test.ts +0 -44
- package/src/__tests__/integration/push.test.ts +0 -75
- package/src/__tests__/integration/sanitization.test.ts +0 -35
- package/src/__tests__/integration/sla-pause.test.ts +0 -39
- package/src/__tests__/integration/smoke.test.ts +0 -26
- package/src/__tests__/integration/snooze.test.ts +0 -68
- package/src/__tests__/integration/teams.test.ts +0 -38
- package/src/__tests__/rateLimiter.test.ts +0 -83
- package/src/__tests__/sanitizeHtml.test.ts +0 -165
- package/src/__tests__/sla.test.ts +0 -258
- package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
- package/src/__tests__/webhookSecurity.test.ts +0 -58
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type SlaState = 'ok' | 'warn' | 'breach' | 'none' | 'met';
|
|
2
|
+
export interface SlaInput {
|
|
3
|
+
slaFirstResponseDue?: string | null;
|
|
4
|
+
slaFirstResponseBreached?: boolean | null;
|
|
5
|
+
firstResponseAt?: string | null;
|
|
6
|
+
slaResolutionDue?: string | null;
|
|
7
|
+
slaResolutionBreached?: boolean | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function computeSlaState(t: SlaInput, now?: Date): {
|
|
10
|
+
state: SlaState;
|
|
11
|
+
remainingMs: number | null;
|
|
12
|
+
due: string | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function formatSlaRemaining(remainingMs: number | null): string;
|
package/dist/views.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export { default as TicketInboxView } from './views/TicketInboxView/index';
|
|
2
|
-
export { default as TicketDetailView } from './views/TicketDetailView/index';
|
|
3
|
-
export { default as SupportDashboardView } from './views/SupportDashboardView/index';
|
|
4
|
-
export { default as NewTicketView } from './views/NewTicketView/index';
|
|
5
|
-
export { default as TicketingSettingsView } from './views/TicketingSettingsView/index';
|
|
6
|
-
export { default as LogsView } from './views/LogsView/index';
|
|
7
|
-
export { default as ChatView } from './views/ChatView/index';
|
|
8
|
-
export { default as CrmView } from './views/CrmView/index';
|
|
9
|
-
export { default as PendingEmailsView } from './views/PendingEmailsView/index';
|
|
10
|
-
export { default as EmailTrackingView } from './views/EmailTrackingView/index';
|
|
11
|
-
export { default as BillingView } from './views/BillingView/index';
|
|
12
|
-
export { default as TimeDashboardView } from './views/TimeDashboardView/index';
|
|
13
|
-
export { default as ImportConversationView } from './views/ImportConversationView/index';
|
|
1
|
+
export { default as TicketInboxView } from './views/TicketInboxView/index.js';
|
|
2
|
+
export { default as TicketDetailView } from './views/TicketDetailView/index.js';
|
|
3
|
+
export { default as SupportDashboardView } from './views/SupportDashboardView/index.js';
|
|
4
|
+
export { default as NewTicketView } from './views/NewTicketView/index.js';
|
|
5
|
+
export { default as TicketingSettingsView } from './views/TicketingSettingsView/index.js';
|
|
6
|
+
export { default as LogsView } from './views/LogsView/index.js';
|
|
7
|
+
export { default as ChatView } from './views/ChatView/index.js';
|
|
8
|
+
export { default as CrmView } from './views/CrmView/index.js';
|
|
9
|
+
export { default as PendingEmailsView } from './views/PendingEmailsView/index.js';
|
|
10
|
+
export { default as EmailTrackingView } from './views/EmailTrackingView/index.js';
|
|
11
|
+
export { default as BillingView } from './views/BillingView/index.js';
|
|
12
|
+
export { default as TimeDashboardView } from './views/TimeDashboardView/index.js';
|
|
13
|
+
export { default as ImportConversationView } from './views/ImportConversationView/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consilioweb/payload-support",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -31,14 +31,11 @@
|
|
|
31
31
|
"import": {
|
|
32
32
|
"types": "./dist/views.d.ts",
|
|
33
33
|
"default": "./dist/views.js"
|
|
34
|
-
},
|
|
35
|
-
"require": {
|
|
36
|
-
"types": "./dist/views.d.cts",
|
|
37
|
-
"default": "./dist/views.cjs"
|
|
38
34
|
}
|
|
39
35
|
},
|
|
40
36
|
"./components/TicketConversation": {
|
|
41
37
|
"import": {
|
|
38
|
+
"types": "./dist/components/TicketConversation/index.d.ts",
|
|
42
39
|
"default": "./dist/components/TicketConversation/index.js"
|
|
43
40
|
}
|
|
44
41
|
}
|
|
@@ -46,18 +43,12 @@
|
|
|
46
43
|
"files": [
|
|
47
44
|
"dist",
|
|
48
45
|
"src",
|
|
46
|
+
"!src/__tests__",
|
|
47
|
+
"!src/**/*.test.ts",
|
|
48
|
+
"!src/**/*.test.tsx",
|
|
49
49
|
"README.md",
|
|
50
50
|
"LICENSE"
|
|
51
51
|
],
|
|
52
|
-
"scripts": {
|
|
53
|
-
"build": "tsup",
|
|
54
|
-
"typecheck": "tsc --noEmit",
|
|
55
|
-
"prepublishOnly": "npm run build",
|
|
56
|
-
"test": "vitest run",
|
|
57
|
-
"test:watch": "vitest",
|
|
58
|
-
"test:e2e": "playwright test",
|
|
59
|
-
"test:e2e:ui": "playwright test --ui"
|
|
60
|
-
},
|
|
61
52
|
"keywords": [
|
|
62
53
|
"payload",
|
|
63
54
|
"payload-cms",
|
|
@@ -94,43 +85,28 @@
|
|
|
94
85
|
"author": "ConsilioWEB <contact@consilioweb.fr> (https://consilioweb.fr)",
|
|
95
86
|
"license": "MIT",
|
|
96
87
|
"peerDependencies": {
|
|
97
|
-
"@payloadcms/next": "^3.
|
|
98
|
-
"@payloadcms/translations": "^3.0.0",
|
|
99
|
-
"@payloadcms/ui": "^3.0.0",
|
|
88
|
+
"@payloadcms/next": "^3.37.0",
|
|
100
89
|
"lucide-react": ">=0.300.0",
|
|
101
|
-
"next": "^
|
|
102
|
-
"payload": "^3.
|
|
103
|
-
"react": "^
|
|
104
|
-
"react-dom": "^
|
|
105
|
-
},
|
|
106
|
-
"peerDependenciesMeta": {
|
|
107
|
-
"@payloadcms/ui": {
|
|
108
|
-
"optional": true
|
|
109
|
-
},
|
|
110
|
-
"@payloadcms/translations": {
|
|
111
|
-
"optional": true
|
|
112
|
-
},
|
|
113
|
-
"@payloadcms/next": {
|
|
114
|
-
"optional": true
|
|
115
|
-
},
|
|
116
|
-
"lucide-react": {
|
|
117
|
-
"optional": true
|
|
118
|
-
},
|
|
119
|
-
"next": {
|
|
120
|
-
"optional": true
|
|
121
|
-
}
|
|
90
|
+
"next": "^15.2.9 || ^16.0.0",
|
|
91
|
+
"payload": "^3.37.0",
|
|
92
|
+
"react": "^19.0.0",
|
|
93
|
+
"react-dom": "^19.0.0"
|
|
122
94
|
},
|
|
123
95
|
"engines": {
|
|
124
|
-
"node": ">=
|
|
96
|
+
"node": ">=20.9.0"
|
|
97
|
+
},
|
|
98
|
+
"publishConfig": {
|
|
99
|
+
"access": "public",
|
|
100
|
+
"provenance": true
|
|
125
101
|
},
|
|
126
102
|
"devDependencies": {
|
|
127
103
|
"@payloadcms/db-sqlite": "^3.86.0",
|
|
128
|
-
"@playwright/test": "^1.49.0",
|
|
129
104
|
"@payloadcms/graphql": "^3.86.0",
|
|
130
105
|
"@payloadcms/next": "^3.86.0",
|
|
131
106
|
"@payloadcms/richtext-lexical": "^3.86.0",
|
|
132
107
|
"@payloadcms/translations": "^3.86.0",
|
|
133
108
|
"@payloadcms/ui": "^3.86.0",
|
|
109
|
+
"@playwright/test": "^1.49.0",
|
|
134
110
|
"@types/pdfkit": "^0.17.6",
|
|
135
111
|
"@types/react": "^19.0.0",
|
|
136
112
|
"@types/sanitize-html": "^2.16.1",
|
|
@@ -140,6 +116,7 @@
|
|
|
140
116
|
"payload": "^3.86.0",
|
|
141
117
|
"react": "^19.2.7",
|
|
142
118
|
"react-dom": "^19.2.7",
|
|
119
|
+
"sharp": "0.34.5",
|
|
143
120
|
"tsup": "^8.0.0",
|
|
144
121
|
"typescript": "^5.5.0",
|
|
145
122
|
"vitest": "^4.1.9"
|
|
@@ -148,5 +125,13 @@
|
|
|
148
125
|
"pdfkit": "^0.19.1",
|
|
149
126
|
"sanitize-html": "^2.17.5",
|
|
150
127
|
"web-push": "^3.6.7"
|
|
128
|
+
},
|
|
129
|
+
"scripts": {
|
|
130
|
+
"build": "tsup && tsc -p tsconfig.types.json && node scripts/copy-subpath-types.mjs",
|
|
131
|
+
"typecheck": "tsc --noEmit",
|
|
132
|
+
"test": "vitest run",
|
|
133
|
+
"test:watch": "vitest",
|
|
134
|
+
"test:e2e": "playwright test",
|
|
135
|
+
"test:e2e:ui": "playwright test --ui"
|
|
151
136
|
}
|
|
152
|
-
}
|
|
137
|
+
}
|
|
@@ -91,12 +91,33 @@ function createEnforce2FA(slugs: CollectionSlugs): CollectionBeforeLoginHook {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Field-level write guard for the commercial / security fields.
|
|
96
|
+
*
|
|
97
|
+
* `access.update` on this collection intentionally lets a support-client PATCH
|
|
98
|
+
* their OWN document (the portal profile page writes firstName, phone, the
|
|
99
|
+
* notification toggles and the 2FA switch). Payload persists every other field of
|
|
100
|
+
* that same PATCH too, so without a field guard a client could self-promote
|
|
101
|
+
* (`tier`), rewrite the sales notes, or — worst — set `googleId`, which the Google
|
|
102
|
+
* OAuth callback resolves BEFORE the email and would hand them another account.
|
|
103
|
+
*
|
|
104
|
+
* Internal writers (2FA endpoints, OAuth linking, beforeLogin) all go through
|
|
105
|
+
* `overrideAccess: true`, which bypasses field access, so they are unaffected.
|
|
106
|
+
* `twoFactorEnabled` is deliberately NOT locked: it is a legitimate self-service
|
|
107
|
+
* toggle on the portal profile page.
|
|
108
|
+
*/
|
|
109
|
+
function createAdminOnlyFieldUpdate(slugs: CollectionSlugs) {
|
|
110
|
+
return ({ req }: { req: { user?: { collection?: string } | null } }) =>
|
|
111
|
+
req.user?.collection === slugs.users
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
// ─── Collection factory ──────────────────────────────────
|
|
95
115
|
|
|
96
116
|
export function createSupportClientsCollection(
|
|
97
117
|
slugs: CollectionSlugs,
|
|
98
118
|
options?: { capabilities?: SupportCapabilities },
|
|
99
119
|
): CollectionConfig {
|
|
120
|
+
const adminOnlyFieldUpdate = createAdminOnlyFieldUpdate(slugs)
|
|
100
121
|
return {
|
|
101
122
|
slug: slugs.supportClients,
|
|
102
123
|
labels: {
|
|
@@ -226,6 +247,7 @@ export function createSupportClientsCollection(
|
|
|
226
247
|
access: {
|
|
227
248
|
// Admin-only: never serialize commercial notes onto the client's own doc.
|
|
228
249
|
read: ({ req }) => req.user?.collection === slugs.users,
|
|
250
|
+
update: adminOnlyFieldUpdate,
|
|
229
251
|
},
|
|
230
252
|
admin: {
|
|
231
253
|
description: 'Notes commerciales (visible uniquement par les admins)',
|
|
@@ -235,6 +257,7 @@ export function createSupportClientsCollection(
|
|
|
235
257
|
name: 'tier',
|
|
236
258
|
type: 'select',
|
|
237
259
|
label: 'Plan',
|
|
260
|
+
access: { update: adminOnlyFieldUpdate },
|
|
238
261
|
options: [
|
|
239
262
|
{ label: 'Free', value: 'free' },
|
|
240
263
|
{ label: 'Basic', value: 'basic' },
|
|
@@ -248,6 +271,7 @@ export function createSupportClientsCollection(
|
|
|
248
271
|
type: 'relationship',
|
|
249
272
|
relationTo: slugs.users,
|
|
250
273
|
label: 'Agent referent',
|
|
274
|
+
access: { update: adminOnlyFieldUpdate },
|
|
251
275
|
admin: { position: 'sidebar' },
|
|
252
276
|
},
|
|
253
277
|
{
|
|
@@ -263,17 +287,20 @@ export function createSupportClientsCollection(
|
|
|
263
287
|
{
|
|
264
288
|
name: 'twoFactorCode',
|
|
265
289
|
type: 'text',
|
|
290
|
+
access: { update: adminOnlyFieldUpdate },
|
|
266
291
|
admin: { hidden: true },
|
|
267
292
|
},
|
|
268
293
|
{
|
|
269
294
|
name: 'twoFactorExpiry',
|
|
270
295
|
type: 'date',
|
|
296
|
+
access: { update: adminOnlyFieldUpdate },
|
|
271
297
|
admin: { hidden: true },
|
|
272
298
|
},
|
|
273
299
|
{
|
|
274
300
|
// Set by /support/2fa "verify"; gates login via the beforeLogin hook.
|
|
275
301
|
name: 'twoFactorVerifiedAt',
|
|
276
302
|
type: 'date',
|
|
303
|
+
access: { update: adminOnlyFieldUpdate },
|
|
277
304
|
admin: { hidden: true },
|
|
278
305
|
},
|
|
279
306
|
{
|
|
@@ -281,6 +308,7 @@ export function createSupportClientsCollection(
|
|
|
281
308
|
name: 'googleId',
|
|
282
309
|
type: 'text',
|
|
283
310
|
index: true,
|
|
311
|
+
access: { update: adminOnlyFieldUpdate },
|
|
284
312
|
admin: { hidden: true },
|
|
285
313
|
},
|
|
286
314
|
{
|
|
@@ -352,6 +380,7 @@ export function createSupportClientsCollection(
|
|
|
352
380
|
access: {
|
|
353
381
|
// Admin-only: never serialize internal notes onto the client's own doc.
|
|
354
382
|
read: ({ req }) => req.user?.collection === slugs.users,
|
|
383
|
+
update: adminOnlyFieldUpdate,
|
|
355
384
|
},
|
|
356
385
|
admin: {
|
|
357
386
|
description: 'Visible uniquement par les admins',
|
|
@@ -32,7 +32,12 @@ export function createTicketCollaboratorsCollection(slugs: CollectionSlugs): Col
|
|
|
32
32
|
}
|
|
33
33
|
return false
|
|
34
34
|
},
|
|
35
|
-
|
|
35
|
+
// Staff only. This collection is the source of truth for
|
|
36
|
+
// resolveAccessibleTicketIds(), which widens `read` on tickets, messages
|
|
37
|
+
// and the activity log — so letting any authenticated principal POST here
|
|
38
|
+
// let a support client grant themselves access to any ticket by id.
|
|
39
|
+
// The /invite endpoint is unaffected: it writes with overrideAccess: true.
|
|
40
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
36
41
|
update: () => false,
|
|
37
42
|
delete: ({ req }) => req.user?.collection === slugs.users,
|
|
38
43
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CollectionConfig, CollectionBeforeChangeHook, CollectionAfterChangeHook, Where } from 'payload'
|
|
2
|
+
import { APIError } from 'payload'
|
|
2
3
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
4
|
import type { SupportCapabilities } from '../types'
|
|
4
5
|
import { escapeHtml, emailWrapper, emailButton, emailQuote, emailParagraph, emailRichContent, emailTrackingPixel } from '../utils/emailTemplate'
|
|
5
|
-
import { fireWebhooks } from '../utils/fireWebhooks'
|
|
6
6
|
import { createAdminNotification } from '../utils/adminNotification'
|
|
7
7
|
import { dispatchWebhook } from '../utils/webhookDispatcher'
|
|
8
8
|
import { readSupportSettings } from '../utils/readSettings'
|
|
@@ -13,6 +13,38 @@ import { queueClientNotification } from '../utils/notificationQueue'
|
|
|
13
13
|
import { sendPushToUser } from '../utils/push'
|
|
14
14
|
import { dbFind, dbCreate, dbFindByID } from '../utils/db'
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Cross-tenant write guard for support-clients.
|
|
18
|
+
*
|
|
19
|
+
* `access.create` only checks WHICH collection the actor belongs to, so any
|
|
20
|
+
* authenticated support-client could POST a message onto an arbitrary ticket id.
|
|
21
|
+
* That is not just a data-integrity issue: the `afterChange` notification hooks
|
|
22
|
+
* would then mail the injected body to the real ticket owner from the legitimate
|
|
23
|
+
* support address (phishing over a trusted channel).
|
|
24
|
+
*
|
|
25
|
+
* We reuse `resolveAccessibleTicketIds()` — the same boundary `access.read` uses —
|
|
26
|
+
* so read and write scopes cannot drift apart. Staff writes and every internal
|
|
27
|
+
* `overrideAccess` write are untouched: they carry no `support-clients` user.
|
|
28
|
+
*/
|
|
29
|
+
function createRestrictClientTicketTarget(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
30
|
+
return async ({ data, operation, req, originalDoc }) => {
|
|
31
|
+
if (req.user?.collection !== slugs.supportClients) return data
|
|
32
|
+
|
|
33
|
+
const raw = (data as { ticket?: unknown }).ticket
|
|
34
|
+
?? (operation === 'update' ? (originalDoc as { ticket?: unknown } | undefined)?.ticket : undefined)
|
|
35
|
+
const targetId = raw && typeof raw === 'object' ? (raw as { id?: unknown }).id : raw
|
|
36
|
+
if (targetId === undefined || targetId === null || targetId === '') {
|
|
37
|
+
throw new APIError('Ticket cible requis.', 400)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const accessible = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id)
|
|
41
|
+
if (!accessible.some((id) => String(id) === String(targetId))) {
|
|
42
|
+
throw new APIError('Ticket inaccessible.', 403)
|
|
43
|
+
}
|
|
44
|
+
return data
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
16
48
|
function createAssignAuthor(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
17
49
|
return async ({ data, operation, req }) => {
|
|
18
50
|
if (operation === 'create' && req.user?.collection === slugs.supportClients) {
|
|
@@ -300,26 +332,12 @@ function createNotifyAdminOnClientMessage(slugs: CollectionSlugs, notificationSl
|
|
|
300
332
|
}
|
|
301
333
|
}
|
|
302
334
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (doc.isInternal) return doc
|
|
310
|
-
|
|
311
|
-
const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
|
|
312
|
-
fireWebhooks(req.payload, slugs, 'ticket_replied', {
|
|
313
|
-
ticketId,
|
|
314
|
-
messageId: doc.id,
|
|
315
|
-
authorType: doc.authorType,
|
|
316
|
-
body: doc.body?.length > 500 ? doc.body.slice(0, 500) + '...' : doc.body,
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
return doc
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Single outbound-webhook dispatcher for replies. See the matching comment in
|
|
337
|
+
* Tickets.ts: a second, unsigned dispatcher used to fire on this exact hook with
|
|
338
|
+
* the same guards, doubling every delivery. The `body` excerpt of the legacy
|
|
339
|
+
* payload is carried over so existing consumers do not regress.
|
|
340
|
+
*/
|
|
323
341
|
function createDispatchWebhookOnReply(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
324
342
|
return async ({ doc, operation, req }) => {
|
|
325
343
|
if (operation !== 'create') return doc
|
|
@@ -328,7 +346,12 @@ function createDispatchWebhookOnReply(slugs: CollectionSlugs): CollectionAfterCh
|
|
|
328
346
|
|
|
329
347
|
const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
|
|
330
348
|
dispatchWebhook(
|
|
331
|
-
{
|
|
349
|
+
{
|
|
350
|
+
ticketId,
|
|
351
|
+
messageId: doc.id,
|
|
352
|
+
authorType: doc.authorType,
|
|
353
|
+
body: doc.body?.length > 500 ? doc.body.slice(0, 500) + '...' : doc.body,
|
|
354
|
+
},
|
|
332
355
|
'ticket_replied',
|
|
333
356
|
req.payload,
|
|
334
357
|
slugs,
|
|
@@ -534,7 +557,7 @@ export function createTicketMessagesCollection(slugs: CollectionSlugs, options?:
|
|
|
534
557
|
] : []),
|
|
535
558
|
],
|
|
536
559
|
hooks: {
|
|
537
|
-
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
560
|
+
beforeChange: [createRestrictClientTicketTarget(slugs), createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
538
561
|
afterChange: [
|
|
539
562
|
...(options?.capabilities?.sms ? [createSmsNotification(slugs, options.capabilities.sms)] : []),
|
|
540
563
|
createAutoUpdateStatus(slugs),
|
|
@@ -543,7 +566,6 @@ export function createTicketMessagesCollection(slugs: CollectionSlugs, options?:
|
|
|
543
566
|
createCheckSlaOnReply(slugs, notificationSlug),
|
|
544
567
|
createSyncTicketReplyToChat(slugs),
|
|
545
568
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
546
|
-
createFireMessageWebhooks(slugs),
|
|
547
569
|
createDispatchWebhookOnReply(slugs),
|
|
548
570
|
createNotifyMentions(slugs),
|
|
549
571
|
...(options?.capabilities?.threadCleanup ? [createThreadCleanup(options.capabilities.threadCleanup)] : []),
|
|
@@ -3,13 +3,13 @@ import type {
|
|
|
3
3
|
CollectionBeforeChangeHook,
|
|
4
4
|
CollectionAfterChangeHook,
|
|
5
5
|
CollectionBeforeDeleteHook,
|
|
6
|
+
CollectionBeforeValidateHook,
|
|
6
7
|
Field,
|
|
7
8
|
Where,
|
|
8
9
|
} from 'payload'
|
|
9
10
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
10
11
|
import type { SupportCapabilities } from '../types'
|
|
11
12
|
import { escapeHtml, emailWrapper, emailParagraph, emailButton } from '../utils/emailTemplate'
|
|
12
|
-
import { fireWebhooks } from '../utils/fireWebhooks'
|
|
13
13
|
import { createAdminNotification } from '../utils/adminNotification'
|
|
14
14
|
import { dispatchWebhook } from '../utils/webhookDispatcher'
|
|
15
15
|
import { readSupportSettings } from '../utils/readSettings'
|
|
@@ -96,13 +96,52 @@ function createAssignTicketNumber(
|
|
|
96
96
|
|
|
97
97
|
function createAssignClientOnCreate(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
98
98
|
return async ({ data, operation, req }) => {
|
|
99
|
-
|
|
99
|
+
// Unconditional, NOT `if (!data.client)`: a support-client posting an explicit
|
|
100
|
+
// `client` id would otherwise open a ticket in someone else's name.
|
|
101
|
+
if (operation === 'create' && req.user?.collection === slugs.supportClients) {
|
|
100
102
|
data.client = req.user.id
|
|
101
103
|
}
|
|
102
104
|
return data
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Fields a support-client is allowed to submit when opening a ticket from the
|
|
110
|
+
* portal. Everything else (billing, SLA, assignment, counters, AI bookkeeping,
|
|
111
|
+
* `client`, `source`) is server-owned.
|
|
112
|
+
*/
|
|
113
|
+
const CLIENT_CREATABLE_TICKET_FIELDS = ['subject', 'category', 'priority', 'project'] as const
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Allow-list the payload of a client-initiated ticket creation.
|
|
117
|
+
*
|
|
118
|
+
* `access.create` grants any authenticated support-client the right to create a
|
|
119
|
+
* ticket, and Payload happily persists every field it is handed — including
|
|
120
|
+
* `paymentStatus: 'paid'`, `billable: false`, `assignedTo` or `slaPolicy`, all of
|
|
121
|
+
* which the billing endpoints read back as trusted input.
|
|
122
|
+
*
|
|
123
|
+
* Runs as `beforeValidate` so the later `beforeChange` hooks (ticket number
|
|
124
|
+
* allocation, round-robin assignment) can still set the server-owned fields they
|
|
125
|
+
* own. Update is already covered by `createRestrictClientUpdates`.
|
|
126
|
+
*/
|
|
127
|
+
function createRestrictClientCreate(slugs: CollectionSlugs): CollectionBeforeValidateHook {
|
|
128
|
+
return async ({ data, operation, req }) => {
|
|
129
|
+
if (operation !== 'create') return data
|
|
130
|
+
if (req.user?.collection !== slugs.supportClients) return data
|
|
131
|
+
|
|
132
|
+
const incoming = (data ?? {}) as Record<string, unknown>
|
|
133
|
+
const sanitized: Record<string, unknown> = {}
|
|
134
|
+
for (const field of CLIENT_CREATABLE_TICKET_FIELDS) {
|
|
135
|
+
if (incoming[field] !== undefined) sanitized[field] = incoming[field]
|
|
136
|
+
}
|
|
137
|
+
// Clients open tickets; they do not get to pick the initial workflow state.
|
|
138
|
+
sanitized.status = 'open'
|
|
139
|
+
sanitized.client = req.user.id
|
|
140
|
+
sanitized.source = 'portal'
|
|
141
|
+
return sanitized
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
106
145
|
const autoPaidAt: CollectionBeforeChangeHook = async ({ data, operation, originalDoc }) => {
|
|
107
146
|
if (operation !== 'update') return data
|
|
108
147
|
if (data.paymentStatus === 'paid' && originalDoc?.paymentStatus !== 'paid' && !data.paidAt) {
|
|
@@ -133,16 +172,10 @@ function createAutoAssignAdmin(slugs: CollectionSlugs): CollectionBeforeChangeHo
|
|
|
133
172
|
const { payload } = req
|
|
134
173
|
let roundRobinEnabled = false
|
|
135
174
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
depth: 0,
|
|
141
|
-
overrideAccess: true,
|
|
142
|
-
})
|
|
143
|
-
if (prefs.docs.length > 0) {
|
|
144
|
-
roundRobinEnabled = (prefs.docs[0].value as { enabled?: boolean })?.enabled === true
|
|
145
|
-
}
|
|
175
|
+
// Single source of truth: the `features` block of the support settings,
|
|
176
|
+
// which also falls back to the legacy `support-round-robin` row.
|
|
177
|
+
const settings = await readSupportSettings(payload)
|
|
178
|
+
roundRobinEnabled = settings.features.roundRobin === true
|
|
146
179
|
} catch (err) { console.warn('[support] Failed to read round-robin preferences:', err) }
|
|
147
180
|
|
|
148
181
|
const admins = await payload.find({
|
|
@@ -384,62 +417,6 @@ function createNotifyClientOnResolve(slugs: CollectionSlugs): CollectionAfterCha
|
|
|
384
417
|
}
|
|
385
418
|
}
|
|
386
419
|
|
|
387
|
-
function createFireTicketWebhooks(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
388
|
-
return async ({ doc, previousDoc, operation, req }) => {
|
|
389
|
-
const { payload } = req
|
|
390
|
-
|
|
391
|
-
if (operation === 'create') {
|
|
392
|
-
// ticket_created
|
|
393
|
-
fireWebhooks(payload, slugs, 'ticket_created', {
|
|
394
|
-
id: doc.id,
|
|
395
|
-
ticketNumber: doc.ticketNumber,
|
|
396
|
-
subject: doc.subject,
|
|
397
|
-
status: doc.status,
|
|
398
|
-
priority: doc.priority,
|
|
399
|
-
category: doc.category,
|
|
400
|
-
})
|
|
401
|
-
return doc
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (operation === 'update' && previousDoc) {
|
|
405
|
-
// ticket_resolved
|
|
406
|
-
if (previousDoc.status !== doc.status && doc.status === 'resolved') {
|
|
407
|
-
fireWebhooks(payload, slugs, 'ticket_resolved', {
|
|
408
|
-
id: doc.id,
|
|
409
|
-
ticketNumber: doc.ticketNumber,
|
|
410
|
-
subject: doc.subject,
|
|
411
|
-
previousStatus: previousDoc.status,
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
// Invalidate client summary cache so it refreshes on next view
|
|
415
|
-
const clientId = typeof doc.client === 'object' ? doc.client?.id : doc.client
|
|
416
|
-
if (clientId) {
|
|
417
|
-
payload.update({
|
|
418
|
-
collection: 'client-summaries',
|
|
419
|
-
where: { client: { equals: clientId } },
|
|
420
|
-
data: { generatedAt: new Date(0).toISOString() }, // Force cache expiry
|
|
421
|
-
overrideAccess: true,
|
|
422
|
-
}).catch(() => { /* silent — collection might not exist yet */ })
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// ticket_assigned
|
|
427
|
-
const oldAssigned = typeof previousDoc.assignedTo === 'object' ? previousDoc.assignedTo?.id : previousDoc.assignedTo
|
|
428
|
-
const newAssigned = typeof doc.assignedTo === 'object' ? doc.assignedTo?.id : doc.assignedTo
|
|
429
|
-
if (newAssigned && oldAssigned !== newAssigned) {
|
|
430
|
-
fireWebhooks(payload, slugs, 'ticket_assigned', {
|
|
431
|
-
id: doc.id,
|
|
432
|
-
ticketNumber: doc.ticketNumber,
|
|
433
|
-
subject: doc.subject,
|
|
434
|
-
assignedTo: newAssigned,
|
|
435
|
-
})
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return doc
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
420
|
function createNotifyAdminOnNewTicket(slugs: CollectionSlugs, notificationSlug: string): CollectionAfterChangeHook {
|
|
444
421
|
return async ({ doc, operation, req }) => {
|
|
445
422
|
if (operation !== 'create') return doc
|
|
@@ -462,24 +439,82 @@ function createNotifyAdminOnNewTicket(slugs: CollectionSlugs, notificationSlug:
|
|
|
462
439
|
}
|
|
463
440
|
}
|
|
464
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Single outbound-webhook dispatcher for tickets.
|
|
444
|
+
*
|
|
445
|
+
* Until 2.x two dispatchers were registered side by side on these very hooks with
|
|
446
|
+
* identical guards, so every subscriber received TWO POSTs per event, with two
|
|
447
|
+
* incompatible body shapes and two auth schemes — the legacy one shipping the
|
|
448
|
+
* endpoint secret in clear in `X-Webhook-Secret`, which defeated the HMAC of the
|
|
449
|
+
* other. Only the HMAC path remains; its payloads absorb the legacy fields
|
|
450
|
+
* (`id`, `status`, `priority`, `category`, `previousStatus`, `assignedTo`) so
|
|
451
|
+
* consumers of either shape keep working.
|
|
452
|
+
*/
|
|
465
453
|
function createDispatchWebhookOnTicket(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
466
454
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
455
|
+
const { payload } = req
|
|
456
|
+
|
|
467
457
|
if (operation === 'create') {
|
|
468
458
|
dispatchWebhook(
|
|
469
|
-
{
|
|
459
|
+
{
|
|
460
|
+
ticketId: doc.id,
|
|
461
|
+
id: doc.id,
|
|
462
|
+
ticketNumber: doc.ticketNumber,
|
|
463
|
+
subject: doc.subject,
|
|
464
|
+
status: doc.status,
|
|
465
|
+
priority: doc.priority,
|
|
466
|
+
category: doc.category,
|
|
467
|
+
},
|
|
470
468
|
'ticket_created',
|
|
471
|
-
|
|
469
|
+
payload,
|
|
472
470
|
slugs,
|
|
473
471
|
)
|
|
472
|
+
return doc
|
|
474
473
|
}
|
|
475
474
|
|
|
476
|
-
if (operation === 'update' && previousDoc
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
475
|
+
if (operation === 'update' && previousDoc) {
|
|
476
|
+
if (previousDoc.status !== doc.status && doc.status === 'resolved') {
|
|
477
|
+
dispatchWebhook(
|
|
478
|
+
{
|
|
479
|
+
ticketId: doc.id,
|
|
480
|
+
id: doc.id,
|
|
481
|
+
ticketNumber: doc.ticketNumber,
|
|
482
|
+
subject: doc.subject,
|
|
483
|
+
previousStatus: previousDoc.status,
|
|
484
|
+
},
|
|
485
|
+
'ticket_resolved',
|
|
486
|
+
payload,
|
|
487
|
+
slugs,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
// Invalidate client summary cache so it refreshes on next view
|
|
491
|
+
const clientId = typeof doc.client === 'object' ? doc.client?.id : doc.client
|
|
492
|
+
if (clientId) {
|
|
493
|
+
payload.update({
|
|
494
|
+
collection: 'client-summaries',
|
|
495
|
+
where: { client: { equals: clientId } },
|
|
496
|
+
data: { generatedAt: new Date(0).toISOString() }, // Force cache expiry
|
|
497
|
+
overrideAccess: true,
|
|
498
|
+
}).catch(() => { /* silent — collection might not exist yet */ })
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const oldAssigned = typeof previousDoc.assignedTo === 'object' ? previousDoc.assignedTo?.id : previousDoc.assignedTo
|
|
503
|
+
const newAssigned = typeof doc.assignedTo === 'object' ? doc.assignedTo?.id : doc.assignedTo
|
|
504
|
+
if (newAssigned && oldAssigned !== newAssigned) {
|
|
505
|
+
dispatchWebhook(
|
|
506
|
+
{
|
|
507
|
+
ticketId: doc.id,
|
|
508
|
+
id: doc.id,
|
|
509
|
+
ticketNumber: doc.ticketNumber,
|
|
510
|
+
subject: doc.subject,
|
|
511
|
+
assignedTo: newAssigned,
|
|
512
|
+
},
|
|
513
|
+
'ticket_assigned',
|
|
514
|
+
payload,
|
|
515
|
+
slugs,
|
|
516
|
+
)
|
|
517
|
+
}
|
|
483
518
|
}
|
|
484
519
|
|
|
485
520
|
return doc
|
|
@@ -626,7 +661,10 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
626
661
|
{ label: 'Paiement partiel', value: 'partial' },
|
|
627
662
|
{ label: 'Payé', value: 'paid' },
|
|
628
663
|
],
|
|
629
|
-
|
|
664
|
+
// `slugs.users`, not the literal 'users': the users collection slug is
|
|
665
|
+
// configurable, and a hard-coded one silently opens this field to everybody
|
|
666
|
+
// on a host that renamed it.
|
|
667
|
+
access: { update: ({ req }) => req.user?.collection === slugs.users },
|
|
630
668
|
admin: { width: '50%', condition: (data) => !!data?.invoice },
|
|
631
669
|
},
|
|
632
670
|
{
|
|
@@ -890,6 +928,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
890
928
|
{ name: 'totalTimeMinutes', type: 'number', defaultValue: 0, label: 'Temps total (minutes)', admin: { readOnly: true, position: 'sidebar' } },
|
|
891
929
|
],
|
|
892
930
|
hooks: {
|
|
931
|
+
beforeValidate: [createRestrictClientCreate(slugs)],
|
|
893
932
|
beforeChange: [
|
|
894
933
|
createAssignTicketNumber(slugs, options?.ticketNumber),
|
|
895
934
|
createAssignClientOnCreate(slugs),
|
|
@@ -910,7 +949,6 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
910
949
|
createNotifyClientOnResolve(slugs),
|
|
911
950
|
createTicketStatusEmail(slugs),
|
|
912
951
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
913
|
-
createFireTicketWebhooks(slugs),
|
|
914
952
|
createDispatchWebhookOnTicket(slugs),
|
|
915
953
|
createApplyAutomationRules(slugs),
|
|
916
954
|
],
|