@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
|
@@ -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
|
+
}
|
|
@@ -1,22 +1,56 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect } from 'react'
|
|
2
2
|
import frTranslations from '../locales/fr.json'
|
|
3
3
|
import enTranslations from '../locales/en.json'
|
|
4
|
+
import frViewTranslations from '../../../views/shared/locales/fr.json'
|
|
5
|
+
import enViewTranslations from '../../../views/shared/locales/en.json'
|
|
4
6
|
|
|
5
7
|
type Locale = 'fr' | 'en'
|
|
6
8
|
|
|
7
9
|
const LOCALE_STORAGE_KEY = 'support_locale'
|
|
8
10
|
const DEFAULT_LOCALE: Locale = 'fr'
|
|
9
11
|
|
|
12
|
+
function mergeTranslations(
|
|
13
|
+
base: Record<string, unknown>,
|
|
14
|
+
supplement: Record<string, unknown>,
|
|
15
|
+
): Record<string, unknown> {
|
|
16
|
+
const merged = { ...base }
|
|
17
|
+
|
|
18
|
+
for (const [key, value] of Object.entries(supplement)) {
|
|
19
|
+
const current = merged[key]
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
current != null
|
|
23
|
+
&& value != null
|
|
24
|
+
&& typeof current === 'object'
|
|
25
|
+
&& typeof value === 'object'
|
|
26
|
+
&& !Array.isArray(current)
|
|
27
|
+
&& !Array.isArray(value)
|
|
28
|
+
) {
|
|
29
|
+
merged[key] = mergeTranslations(
|
|
30
|
+
current as Record<string, unknown>,
|
|
31
|
+
value as Record<string, unknown>,
|
|
32
|
+
)
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (current === undefined || (typeof current !== 'object' && typeof value === 'object')) {
|
|
37
|
+
merged[key] = value
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return merged
|
|
42
|
+
}
|
|
43
|
+
|
|
10
44
|
const translations: Record<Locale, Record<string, unknown>> = {
|
|
11
|
-
fr: frTranslations,
|
|
12
|
-
en: enTranslations,
|
|
45
|
+
fr: mergeTranslations(frTranslations, frViewTranslations),
|
|
46
|
+
en: mergeTranslations(enTranslations, enViewTranslations),
|
|
13
47
|
}
|
|
14
48
|
|
|
15
49
|
/**
|
|
16
50
|
* Resolve a dot-notation key from a nested object.
|
|
17
51
|
* e.g. getNestedValue(obj, 'ticket.status.open') => 'Ouvert'
|
|
18
52
|
*/
|
|
19
|
-
function getNestedValue(obj: Record<string, unknown>, path: string): string {
|
|
53
|
+
export function getNestedValue(obj: Record<string, unknown>, path: string): string {
|
|
20
54
|
const keys = path.split('.')
|
|
21
55
|
let current: unknown = obj
|
|
22
56
|
|
|
@@ -113,4 +147,4 @@ export function useTranslation() {
|
|
|
113
147
|
}
|
|
114
148
|
|
|
115
149
|
export type { Locale }
|
|
116
|
-
export { DEFAULT_LOCALE, LOCALE_STORAGE_KEY }
|
|
150
|
+
export { DEFAULT_LOCALE, LOCALE_STORAGE_KEY, translations }
|
|
@@ -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) {
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
SUPPORT_SETTINGS_PREF_KEY as PREF_KEY,
|
|
6
|
+
invalidateSupportSettingsCache,
|
|
7
|
+
mergeSupportSettings,
|
|
8
|
+
readSupportSettingsState,
|
|
9
|
+
} from '../utils/readSettings'
|
|
10
|
+
import { stripProjectedFeatures } from '../utils/features'
|
|
5
11
|
|
|
6
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Round-robin used to live in its own `support-round-robin` preference row,
|
|
14
|
+
* while the admin UI toggled a `roundRobin` flag in `localStorage` — the two
|
|
15
|
+
* never met, so the toggle had no effect on assignment.
|
|
16
|
+
*
|
|
17
|
+
* Both endpoints below now read and write `settings.features.roundRobin`, the
|
|
18
|
+
* same value `/api/support/settings` exposes and `Tickets`' auto-assign hook
|
|
19
|
+
* reads. The legacy row is still honoured on read until the first save (see
|
|
20
|
+
* `readSupportSettingsState`), so an install that had it enabled keeps it.
|
|
21
|
+
*/
|
|
7
22
|
|
|
8
23
|
/**
|
|
9
24
|
* GET /api/support/round-robin-config — Get round-robin enabled status
|
|
@@ -18,18 +33,9 @@ export function createRoundRobinConfigGetEndpoint(slugs: CollectionSlugs): Endpo
|
|
|
18
33
|
|
|
19
34
|
requireAdmin(req, slugs)
|
|
20
35
|
|
|
21
|
-
const
|
|
22
|
-
where: { key: { equals: PREF_KEY } },
|
|
23
|
-
limit: 1,
|
|
24
|
-
depth: 0,
|
|
25
|
-
overrideAccess: true,
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const enabled = prefs.docs.length > 0
|
|
29
|
-
? (prefs.docs[0].value as { enabled?: boolean })?.enabled === true
|
|
30
|
-
: false
|
|
36
|
+
const { settings } = await readSupportSettingsState(payload)
|
|
31
37
|
|
|
32
|
-
return Response.json({ enabled })
|
|
38
|
+
return Response.json({ enabled: settings.features.roundRobin })
|
|
33
39
|
} catch (error) {
|
|
34
40
|
const authResponse = handleAuthError(error)
|
|
35
41
|
if (authResponse) return authResponse
|
|
@@ -55,19 +61,21 @@ export function createRoundRobinConfigPostEndpoint(slugs: CollectionSlugs): Endp
|
|
|
55
61
|
|
|
56
62
|
const { enabled } = (await req.json!()) as { enabled: boolean }
|
|
57
63
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
})
|
|
64
|
+
const { settings: current } = await readSupportSettingsState(payload)
|
|
65
|
+
const merged = mergeSupportSettings(
|
|
66
|
+
{ features: { ...current.features, roundRobin: !!enabled } },
|
|
67
|
+
current,
|
|
68
|
+
)
|
|
64
69
|
|
|
65
70
|
await payload.db.upsert({
|
|
66
71
|
collection: 'payload-preferences',
|
|
67
72
|
data: {
|
|
68
73
|
key: PREF_KEY,
|
|
69
74
|
user: { relationTo: req.user!.collection, value: req.user!.id },
|
|
70
|
-
value: {
|
|
75
|
+
value: {
|
|
76
|
+
...merged,
|
|
77
|
+
features: stripProjectedFeatures(merged.features),
|
|
78
|
+
} as unknown as Record<string, unknown>,
|
|
71
79
|
},
|
|
72
80
|
req: { payload, user: req.user } as any,
|
|
73
81
|
where: {
|
|
@@ -79,6 +87,8 @@ export function createRoundRobinConfigPostEndpoint(slugs: CollectionSlugs): Endp
|
|
|
79
87
|
},
|
|
80
88
|
})
|
|
81
89
|
|
|
90
|
+
invalidateSupportSettingsCache()
|
|
91
|
+
|
|
82
92
|
return Response.json({ enabled: !!enabled })
|
|
83
93
|
} catch (error) {
|
|
84
94
|
const authResponse = handleAuthError(error)
|