@consilioweb/payload-support 2.0.1 → 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 +636 -402
- 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/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/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 +5 -4
- 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/sla.d.ts +14 -0
- package/dist/views.d.ts +13 -13
- package/package.json +16 -31
- 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/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/TicketDetailView/client.tsx +2 -2
- package/src/views/TicketInboxView/client.tsx +10 -5
- package/src/views/TicketingSettingsView/client.tsx +8 -6
- package/src/views/shared/config.ts +14 -80
- package/src/views/shared/index.ts +3 -1
- 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 -101
- 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__/translations.test.ts +0 -67
- package/src/__tests__/webhookSecurity.test.ts +0 -58
|
@@ -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
|
],
|
|
@@ -1,45 +1,69 @@
|
|
|
1
|
-
import type { CollectionConfig, CollectionAfterChangeHook } from 'payload'
|
|
1
|
+
import type { CollectionConfig, CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
|
2
|
+
import type { Payload } from 'payload'
|
|
2
3
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
4
|
|
|
4
5
|
// ─── Hooks ───────────────────────────────────────────────
|
|
5
6
|
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
async function recalculateTicketTime(
|
|
8
|
+
payload: Payload,
|
|
9
|
+
slugs: CollectionSlugs,
|
|
10
|
+
ticketId: number | string,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
// Sum all time entries for this ticket, paginating to avoid loading all at once
|
|
13
|
+
let totalMinutes = 0
|
|
14
|
+
let page = 1
|
|
15
|
+
let hasMore = true
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
const
|
|
17
|
+
while (hasMore) {
|
|
18
|
+
const entries = await payload.find({
|
|
19
|
+
collection: slugs.timeEntries,
|
|
20
|
+
where: { ticket: { equals: ticketId } },
|
|
21
|
+
limit: 100,
|
|
22
|
+
page,
|
|
23
|
+
depth: 0,
|
|
24
|
+
overrideAccess: true,
|
|
25
|
+
select: { duration: true },
|
|
26
|
+
})
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
let hasMore = true
|
|
28
|
+
for (const entry of entries.docs) {
|
|
29
|
+
totalMinutes += ((entry.duration as number) || 0)
|
|
30
|
+
}
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
where: { ticket: { equals: ticketId } },
|
|
22
|
-
limit: 100,
|
|
23
|
-
page,
|
|
24
|
-
depth: 0,
|
|
25
|
-
overrideAccess: true,
|
|
26
|
-
select: { duration: true },
|
|
27
|
-
})
|
|
32
|
+
hasMore = entries.hasNextPage ?? false
|
|
33
|
+
page++
|
|
34
|
+
}
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
await payload.update({
|
|
37
|
+
collection: slugs.tickets,
|
|
38
|
+
id: ticketId,
|
|
39
|
+
data: { totalTimeMinutes: totalMinutes },
|
|
40
|
+
overrideAccess: true,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
function createRecalculateTicketTime(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
45
|
+
return async ({ doc, req }) => {
|
|
46
|
+
if (!doc.ticket) return
|
|
47
|
+
const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
|
|
48
|
+
await recalculateTicketTime(req.payload, slugs, ticketId)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
36
51
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Deleting a time entry has to re-roll the ticket total too. Without this the
|
|
54
|
+
* rollup only ever grows: `tickets.totalTimeMinutes` is what the invoice endpoint
|
|
55
|
+
* and the CRM view bill on, so a removed entry stayed billed forever.
|
|
56
|
+
*/
|
|
57
|
+
function createRecalculateTicketTimeOnDelete(slugs: CollectionSlugs): CollectionAfterDeleteHook {
|
|
58
|
+
return async ({ doc, req }) => {
|
|
59
|
+
if (!doc?.ticket) return
|
|
60
|
+
const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
|
|
61
|
+
try {
|
|
62
|
+
await recalculateTicketTime(req.payload, slugs, ticketId)
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// The entry is already gone; never fail the delete on the rollup refresh.
|
|
65
|
+
console.error('[support] Failed to recalculate ticket time after delete:', err)
|
|
66
|
+
}
|
|
43
67
|
}
|
|
44
68
|
}
|
|
45
69
|
|
|
@@ -105,6 +129,7 @@ export function createTimeEntriesCollection(slugs: CollectionSlugs): CollectionC
|
|
|
105
129
|
],
|
|
106
130
|
hooks: {
|
|
107
131
|
afterChange: [createRecalculateTicketTime(slugs)],
|
|
132
|
+
afterDelete: [createRecalculateTicketTimeOnDelete(slugs)],
|
|
108
133
|
},
|
|
109
134
|
access: {
|
|
110
135
|
create: ({ req }) => req.user?.collection === slugs.users,
|
|
@@ -1,82 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Ticketing feature flags — re-export of the shared implementation.
|
|
3
|
+
*
|
|
4
|
+
* The flags used to be defined (twice, byte-for-byte) here and in
|
|
5
|
+
* `views/shared/config.ts`, each reading `localStorage` directly. The single
|
|
6
|
+
* implementation now lives in `utils/features.ts` and is backed by the server;
|
|
7
|
+
* both modules are kept as import sites so existing paths keep working.
|
|
5
8
|
*/
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
emailTracking: boolean
|
|
17
|
-
/** Canned responses: quick reply templates */
|
|
18
|
-
canned: boolean
|
|
19
|
-
/** Ticket merge: combine two tickets into one */
|
|
20
|
-
merge: boolean
|
|
21
|
-
/** Snooze: temporarily hide a ticket */
|
|
22
|
-
snooze: boolean
|
|
23
|
-
/** External messages: add messages received outside the system */
|
|
24
|
-
externalMessages: boolean
|
|
25
|
-
/** Client history: past tickets, projects, notes sidebar */
|
|
26
|
-
clientHistory: boolean
|
|
27
|
-
/** Activity log: audit trail of actions on the ticket */
|
|
28
|
-
activityLog: boolean
|
|
29
|
-
/** Split ticket: extract a message into a new ticket */
|
|
30
|
-
splitTicket: boolean
|
|
31
|
-
/** Scheduled replies: send a message at a future date */
|
|
32
|
-
scheduledReplies: boolean
|
|
33
|
-
/** Auto-close: automatically resolve inactive tickets */
|
|
34
|
-
autoClose: boolean
|
|
35
|
-
/** Auto-close delay in days (used by auto-close cron) */
|
|
36
|
-
autoCloseDays: number
|
|
37
|
-
/** Round-robin: distribute new tickets evenly among agents */
|
|
38
|
-
roundRobin: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Default features — all enabled */
|
|
42
|
-
export const DEFAULT_FEATURES: TicketingFeatures = {
|
|
43
|
-
timeTracking: true,
|
|
44
|
-
ai: true,
|
|
45
|
-
satisfaction: true,
|
|
46
|
-
chat: true,
|
|
47
|
-
emailTracking: true,
|
|
48
|
-
canned: true,
|
|
49
|
-
merge: true,
|
|
50
|
-
snooze: true,
|
|
51
|
-
externalMessages: true,
|
|
52
|
-
clientHistory: true,
|
|
53
|
-
activityLog: true,
|
|
54
|
-
splitTicket: true,
|
|
55
|
-
scheduledReplies: true,
|
|
56
|
-
autoClose: true,
|
|
57
|
-
autoCloseDays: 7,
|
|
58
|
-
roundRobin: false,
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const STORAGE_KEY = 'ticketing_features'
|
|
62
|
-
|
|
63
|
-
/** Read features from localStorage (falls back to defaults) */
|
|
64
|
-
export function getFeatures(): TicketingFeatures {
|
|
65
|
-
if (typeof window === 'undefined') return DEFAULT_FEATURES
|
|
66
|
-
try {
|
|
67
|
-
const stored = localStorage.getItem(STORAGE_KEY)
|
|
68
|
-
if (stored) {
|
|
69
|
-
const parsed = JSON.parse(stored)
|
|
70
|
-
return { ...DEFAULT_FEATURES, ...parsed }
|
|
71
|
-
}
|
|
72
|
-
} catch { /* ignore */ }
|
|
73
|
-
return DEFAULT_FEATURES
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Save features to localStorage */
|
|
77
|
-
export function saveFeatures(features: TicketingFeatures): void {
|
|
78
|
-
if (typeof window === 'undefined') return
|
|
79
|
-
try {
|
|
80
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(features))
|
|
81
|
-
} catch { /* ignore */ }
|
|
82
|
-
}
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_TICKETING_FEATURES,
|
|
11
|
+
DEFAULT_TICKETING_FEATURES as DEFAULT_FEATURES,
|
|
12
|
+
FEATURES_CACHE_KEY,
|
|
13
|
+
fetchFeatures,
|
|
14
|
+
getFeatures,
|
|
15
|
+
normalizeFeatures,
|
|
16
|
+
saveFeatures,
|
|
17
|
+
} from '../../utils/features'
|
|
18
|
+
export type { TicketingFeatures } from '../../utils/features'
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
fetchFeatures,
|
|
6
|
+
getFeatures,
|
|
7
|
+
hasAdoptedLegacyFeatures,
|
|
8
|
+
markLegacyFeaturesAdopted,
|
|
9
|
+
readLegacyFeatures,
|
|
10
|
+
saveFeatures as persistFeatures,
|
|
11
|
+
writeFeaturesCache,
|
|
12
|
+
type TicketingFeatures,
|
|
13
|
+
} from '../../../utils/features'
|
|
14
|
+
|
|
15
|
+
export interface UseFeaturesResult {
|
|
16
|
+
/** Current flags — cached values on first paint, server values once loaded. */
|
|
17
|
+
features: TicketingFeatures
|
|
18
|
+
/** True until the first server round-trip settles. */
|
|
19
|
+
loading: boolean
|
|
20
|
+
/** True when the server could not be reached and cached/default values are shown. */
|
|
21
|
+
offline: boolean
|
|
22
|
+
/** Replace the flags locally without persisting (for a settings form). */
|
|
23
|
+
setFeatures: (features: TicketingFeatures) => void
|
|
24
|
+
/** Persist the flags server-side. Resolves to false when the write failed. */
|
|
25
|
+
save: (features: TicketingFeatures) => Promise<boolean>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read the ticketing feature flags from the server, with the local cache as a
|
|
30
|
+
* fallback so a screen never blocks on the settings call.
|
|
31
|
+
*
|
|
32
|
+
* Also performs the one-shot migration of pre-2.1 installs: when this browser
|
|
33
|
+
* still holds flags in `localStorage` and the server has never had any saved,
|
|
34
|
+
* the local values are pushed up once and become the shared truth. Afterwards
|
|
35
|
+
* the server always wins and `localStorage` is only a cache.
|
|
36
|
+
*/
|
|
37
|
+
export function useFeatures(): UseFeaturesResult {
|
|
38
|
+
const [features, setFeatures] = useState<TicketingFeatures>(() => getFeatures())
|
|
39
|
+
const [loading, setLoading] = useState(true)
|
|
40
|
+
const [offline, setOffline] = useState(false)
|
|
41
|
+
// Migration must run once per mount at most, even under StrictMode double-effects.
|
|
42
|
+
const migrationAttempted = useRef(false)
|
|
43
|
+
// Set as soon as the caller edits the flags, so a slow first load cannot
|
|
44
|
+
// overwrite a toggle the admin just made in the settings form.
|
|
45
|
+
const dirty = useRef(false)
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const controller = new AbortController()
|
|
49
|
+
let cancelled = false
|
|
50
|
+
|
|
51
|
+
void (async () => {
|
|
52
|
+
const result = await fetchFeatures(controller.signal)
|
|
53
|
+
if (cancelled) return
|
|
54
|
+
|
|
55
|
+
// Fresh server + legacy browser values => adopt them instead of silently
|
|
56
|
+
// resetting the admin's configuration to the defaults.
|
|
57
|
+
if (!result.offline && !result.configured && !migrationAttempted.current && !dirty.current) {
|
|
58
|
+
migrationAttempted.current = true
|
|
59
|
+
const legacy = readLegacyFeatures()
|
|
60
|
+
if (legacy) {
|
|
61
|
+
const ok = await persistFeatures(legacy)
|
|
62
|
+
if (cancelled) return
|
|
63
|
+
if (ok) {
|
|
64
|
+
markLegacyFeaturesAdopted()
|
|
65
|
+
setFeatures(legacy)
|
|
66
|
+
setLoading(false)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!result.offline && !hasAdoptedLegacyFeatures()) {
|
|
73
|
+
// Server already configured (by this admin elsewhere, or another admin):
|
|
74
|
+
// the server wins, and there is nothing left to migrate here.
|
|
75
|
+
markLegacyFeaturesAdopted()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!dirty.current) setFeatures(result.features)
|
|
79
|
+
setOffline(result.offline)
|
|
80
|
+
setLoading(false)
|
|
81
|
+
})()
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
cancelled = true
|
|
85
|
+
controller.abort()
|
|
86
|
+
}
|
|
87
|
+
}, [])
|
|
88
|
+
|
|
89
|
+
const setLocalFeatures = useCallback((next: TicketingFeatures) => {
|
|
90
|
+
dirty.current = true
|
|
91
|
+
setFeatures(next)
|
|
92
|
+
// Keep the cache aligned with what is on screen, so a reload before saving
|
|
93
|
+
// does not flash the previous values.
|
|
94
|
+
writeFeaturesCache(next)
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
const save = useCallback(async (next: TicketingFeatures) => {
|
|
98
|
+
const ok = await persistFeatures(next)
|
|
99
|
+
if (ok) {
|
|
100
|
+
markLegacyFeaturesAdopted()
|
|
101
|
+
dirty.current = false
|
|
102
|
+
setFeatures(next)
|
|
103
|
+
setOffline(false)
|
|
104
|
+
}
|
|
105
|
+
return ok
|
|
106
|
+
}, [])
|
|
107
|
+
|
|
108
|
+
return { features, loading, offline, setFeatures: setLocalFeatures, save }
|
|
109
|
+
}
|
|
@@ -21,14 +21,14 @@ import { useTicketActions } from './hooks/useTicketActions'
|
|
|
21
21
|
import { useReply } from './hooks/useReply'
|
|
22
22
|
import { useAI } from './hooks/useAI'
|
|
23
23
|
import { useDocumentIdFromUrl } from './hooks/useDocumentIdFromUrl'
|
|
24
|
-
import {
|
|
24
|
+
import { useFeatures } from './hooks/useFeatures'
|
|
25
25
|
import { SkeletonText } from './SkeletonText'
|
|
26
26
|
import { RewriteDropdown } from './RewriteDropdown'
|
|
27
27
|
import '../../styles/theme.css'
|
|
28
28
|
|
|
29
29
|
const TicketConversation: React.FC = () => {
|
|
30
30
|
const { id } = useDocumentIdFromUrl()
|
|
31
|
-
const
|
|
31
|
+
const { features } = useFeatures()
|
|
32
32
|
const [messages, setMessages] = useState<Message[]>([])
|
|
33
33
|
const [timeEntries, setTimeEntries] = useState<TimeEntry[]>([])
|
|
34
34
|
const [client, setClient] = useState<ClientInfo | null>(null)
|
package/src/endpoints/ai.ts
CHANGED
|
@@ -3,12 +3,13 @@ import type { CollectionSlugs } from '../utils/slugs'
|
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
4
|
import { readSupportSettings, type SupportSettings } from '../utils/readSettings'
|
|
5
5
|
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
6
|
+
import { resolveOllamaBaseUrl } from '../utils/aiProvider'
|
|
6
7
|
|
|
7
8
|
async function getClient(aiSettings: SupportSettings['ai']) {
|
|
8
9
|
const moduleName = '@anthropic-ai/sdk'
|
|
9
10
|
const { default: Anthropic } = await import(moduleName)
|
|
10
11
|
if (aiSettings.provider === 'ollama') {
|
|
11
|
-
const baseURL =
|
|
12
|
+
const baseURL = resolveOllamaBaseUrl()
|
|
12
13
|
return new Anthropic({ apiKey: 'ollama', baseURL })
|
|
13
14
|
}
|
|
14
15
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
|
@@ -3,12 +3,13 @@ import type { CollectionSlugs } from '../utils/slugs'
|
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
4
|
import { readSupportSettings, type SupportSettings } from '../utils/readSettings'
|
|
5
5
|
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
6
|
+
import { resolveOllamaBaseUrl } from '../utils/aiProvider'
|
|
6
7
|
|
|
7
8
|
async function getClient(aiSettings: SupportSettings['ai']) {
|
|
8
9
|
const moduleName = '@anthropic-ai/sdk'
|
|
9
10
|
const { default: Anthropic } = await import(moduleName)
|
|
10
11
|
if (aiSettings.provider === 'ollama') {
|
|
11
|
-
const baseURL =
|
|
12
|
+
const baseURL = resolveOllamaBaseUrl()
|
|
12
13
|
return new Anthropic({ apiKey: 'ollama', baseURL })
|
|
13
14
|
}
|
|
14
15
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { escapeHtml } from '../utils/emailTemplate'
|
|
4
|
-
import {
|
|
4
|
+
import { dispatchWebhook } from '../utils/webhookDispatcher'
|
|
5
5
|
import { readSupportSettings } from '../utils/readSettings'
|
|
6
6
|
import { dbFind, dbFindByID, dbUpdate } from '../utils/db'
|
|
7
7
|
import { verifySecret } from '../utils/webhookSecurity'
|
|
@@ -112,13 +112,18 @@ export function createProcessScheduledEndpoint(slugs: CollectionSlugs): Endpoint
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
// Fire webhook for the now-sent reply
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
dispatchWebhook(
|
|
116
|
+
{
|
|
117
|
+
ticketId,
|
|
118
|
+
messageId: msg.id,
|
|
119
|
+
authorType: msg.authorType,
|
|
120
|
+
scheduled: true,
|
|
121
|
+
body: msg.body?.length > 500 ? msg.body.slice(0, 500) + '...' : msg.body,
|
|
122
|
+
},
|
|
123
|
+
'ticket_replied',
|
|
124
|
+
payload,
|
|
125
|
+
slugs,
|
|
126
|
+
)
|
|
122
127
|
|
|
123
128
|
results.processed++
|
|
124
129
|
} catch (err) {
|