@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,14 +1,25 @@
|
|
|
1
1
|
import type { Payload } from 'payload'
|
|
2
2
|
import { dbFind } from './db'
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_TICKETING_FEATURES,
|
|
5
|
+
normalizeFeatures,
|
|
6
|
+
projectAutoClose,
|
|
7
|
+
type TicketingFeatures,
|
|
8
|
+
} from './features'
|
|
3
9
|
|
|
4
|
-
const
|
|
10
|
+
export const SUPPORT_SETTINGS_PREF_KEY = 'support-settings'
|
|
11
|
+
const PREF_KEY = SUPPORT_SETTINGS_PREF_KEY
|
|
5
12
|
const USER_PREFS_KEY_PREFIX = 'support-user-prefs'
|
|
13
|
+
/** Pre-2.1 standalone round-robin row — read as a fallback, then folded into `features`. */
|
|
14
|
+
const LEGACY_ROUND_ROBIN_KEY = 'support-round-robin'
|
|
6
15
|
|
|
7
16
|
export interface SupportSettings {
|
|
8
17
|
email: { fromAddress: string; fromName: string; replyToAddress: string }
|
|
9
18
|
ai: { provider: string; model: string; enableSentiment: boolean; enableSynthesis: boolean; enableSuggestion: boolean; enableRewrite: boolean }
|
|
10
19
|
sla: { firstResponseMinutes: number; resolutionMinutes: number; businessHoursOnly: boolean; escalationEmail: string }
|
|
11
20
|
autoClose: { enabled: boolean; daysBeforeClose: number; reminderDaysBefore: number }
|
|
21
|
+
/** Ticketing feature flags — server-authoritative since 2.1 (used to be per-browser localStorage). */
|
|
22
|
+
features: TicketingFeatures
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
export interface UserPrefs {
|
|
@@ -21,6 +32,7 @@ export const DEFAULT_SETTINGS: SupportSettings = {
|
|
|
21
32
|
ai: { provider: 'anthropic', model: 'claude-haiku-4-5-20251001', enableSentiment: true, enableSynthesis: true, enableSuggestion: true, enableRewrite: true },
|
|
22
33
|
sla: { firstResponseMinutes: 120, resolutionMinutes: 1440, businessHoursOnly: true, escalationEmail: '' },
|
|
23
34
|
autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 },
|
|
35
|
+
features: { ...DEFAULT_TICKETING_FEATURES },
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
export const DEFAULT_USER_PREFS: UserPrefs = {
|
|
@@ -32,38 +44,99 @@ export const DEFAULT_USER_PREFS: UserPrefs = {
|
|
|
32
44
|
// (the afterChange hook chain). The settings doc changes rarely, so a short TTL
|
|
33
45
|
// + explicit invalidation on save (endpoints/settings.ts) avoids redundant DB
|
|
34
46
|
// reads of the same `payload-preferences` row.
|
|
35
|
-
let settingsCache: { value:
|
|
47
|
+
let settingsCache: { value: SupportSettingsState; ts: number } | null = null
|
|
36
48
|
const SETTINGS_TTL_MS = 60_000
|
|
37
49
|
|
|
50
|
+
export interface SupportSettingsState {
|
|
51
|
+
settings: SupportSettings
|
|
52
|
+
/**
|
|
53
|
+
* False when the preference row carries no `features` object yet — a
|
|
54
|
+
* pre-2.1 install, or a fresh one. Clients use it to decide whether their
|
|
55
|
+
* legacy `localStorage` flags should seed the server.
|
|
56
|
+
*/
|
|
57
|
+
featuresConfigured: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
/** Invalidate the settings cache — call right after writing support settings. */
|
|
39
61
|
export function invalidateSupportSettingsCache(): void {
|
|
40
62
|
settingsCache = null
|
|
41
63
|
}
|
|
42
64
|
|
|
43
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Merge a stored preference value onto the defaults.
|
|
67
|
+
* Exported for the settings endpoint, which must not re-implement the merge.
|
|
68
|
+
*/
|
|
69
|
+
export function mergeSupportSettings(
|
|
70
|
+
stored: Partial<SupportSettings> | undefined | null,
|
|
71
|
+
base: SupportSettings = DEFAULT_SETTINGS,
|
|
72
|
+
): SupportSettings {
|
|
73
|
+
const autoClose = { ...base.autoClose, ...stored?.autoClose }
|
|
74
|
+
return {
|
|
75
|
+
email: { ...base.email, ...stored?.email },
|
|
76
|
+
ai: { ...base.ai, ...stored?.ai },
|
|
77
|
+
sla: { ...base.sla, ...stored?.sla },
|
|
78
|
+
autoClose,
|
|
79
|
+
// `autoClose` / `autoCloseDays` are projections of the block above, so they
|
|
80
|
+
// are recomputed here rather than trusted from whatever was persisted.
|
|
81
|
+
features: projectAutoClose(
|
|
82
|
+
normalizeFeatures({ ...base.features, ...(stored?.features as object | undefined) }),
|
|
83
|
+
autoClose,
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function readSupportSettingsState(payload: Payload): Promise<SupportSettingsState> {
|
|
44
89
|
if (settingsCache && Date.now() - settingsCache.ts < SETTINGS_TTL_MS) {
|
|
45
90
|
return settingsCache.value
|
|
46
91
|
}
|
|
47
|
-
let value:
|
|
92
|
+
let value: SupportSettingsState = {
|
|
93
|
+
settings: mergeSupportSettings(null),
|
|
94
|
+
featuresConfigured: false,
|
|
95
|
+
}
|
|
48
96
|
try {
|
|
49
97
|
const prefs = await dbFind(payload, 'payload-preferences', {
|
|
50
98
|
where: { key: { equals: PREF_KEY } },
|
|
99
|
+
// The upsert is scoped per admin user, so several rows can share the key.
|
|
100
|
+
// Sorting makes "last write wins" deterministic instead of arbitrary.
|
|
101
|
+
sort: '-updatedAt',
|
|
51
102
|
limit: 1, depth: 0, overrideAccess: true,
|
|
52
103
|
})
|
|
53
104
|
if (prefs.docs.length > 0) {
|
|
54
105
|
const stored = prefs.docs[0].value as Partial<SupportSettings>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
106
|
+
const featuresConfigured = !!stored.features && typeof stored.features === 'object'
|
|
107
|
+
const settings = mergeSupportSettings(stored)
|
|
108
|
+
|
|
109
|
+
// Pre-2.1 installs enabled round-robin through its own preference row.
|
|
110
|
+
// Honour it until the first save writes `features` — after that the
|
|
111
|
+
// legacy row is ignored, and both write paths land in `features`.
|
|
112
|
+
if (!featuresConfigured) {
|
|
113
|
+
settings.features.roundRobin = await readLegacyRoundRobin(payload)
|
|
60
114
|
}
|
|
115
|
+
|
|
116
|
+
value = { settings, featuresConfigured }
|
|
61
117
|
}
|
|
62
118
|
} catch { /* fallback to defaults */ }
|
|
63
119
|
settingsCache = { value, ts: Date.now() }
|
|
64
120
|
return value
|
|
65
121
|
}
|
|
66
122
|
|
|
123
|
+
export async function readSupportSettings(payload: Payload): Promise<SupportSettings> {
|
|
124
|
+
return (await readSupportSettingsState(payload)).settings
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function readLegacyRoundRobin(payload: Payload): Promise<boolean> {
|
|
128
|
+
try {
|
|
129
|
+
const prefs = await dbFind(payload, 'payload-preferences', {
|
|
130
|
+
where: { key: { equals: LEGACY_ROUND_ROBIN_KEY } },
|
|
131
|
+
limit: 1, depth: 0, overrideAccess: true,
|
|
132
|
+
})
|
|
133
|
+
if (prefs.docs.length > 0) {
|
|
134
|
+
return (prefs.docs[0].value as { enabled?: boolean })?.enabled === true
|
|
135
|
+
}
|
|
136
|
+
} catch { /* ignore — defaults to disabled */ }
|
|
137
|
+
return DEFAULT_TICKETING_FEATURES.roundRobin
|
|
138
|
+
}
|
|
139
|
+
|
|
67
140
|
export async function readUserPrefs(payload: Payload, userId: string | number): Promise<UserPrefs> {
|
|
68
141
|
try {
|
|
69
142
|
const key = `${USER_PREFS_KEY_PREFIX}-${userId}`
|
|
@@ -3,7 +3,16 @@ import type { BasePayload } from 'payload'
|
|
|
3
3
|
import type { CollectionSlugs } from './slugs'
|
|
4
4
|
import { dbFind, dbUpdate } from './db'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Outbound webhook events. Must stay in sync with the `events` options of the
|
|
8
|
+
* webhook-endpoints collection.
|
|
9
|
+
*/
|
|
10
|
+
export type WebhookEvent =
|
|
11
|
+
| 'ticket_created'
|
|
12
|
+
| 'ticket_resolved'
|
|
13
|
+
| 'ticket_replied'
|
|
14
|
+
| 'ticket_assigned'
|
|
15
|
+
| 'sla_breached'
|
|
7
16
|
|
|
8
17
|
/**
|
|
9
18
|
* Dispatch outbound webhooks for a given event.
|
|
@@ -21,9 +30,12 @@ export function dispatchWebhook(
|
|
|
21
30
|
event: WebhookEvent,
|
|
22
31
|
payload: BasePayload,
|
|
23
32
|
slugs: CollectionSlugs,
|
|
24
|
-
): void {
|
|
25
|
-
// Fire and forget
|
|
26
|
-
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
// Fire and forget for callers (hooks never await it); the promise is returned so
|
|
35
|
+
// tests — and any caller that wants to — can wait for delivery to settle.
|
|
36
|
+
const done = _dispatch(data, event, payload, slugs)
|
|
37
|
+
void done
|
|
38
|
+
return done
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
async function _dispatch(
|
|
@@ -49,9 +61,9 @@ async function _dispatch(
|
|
|
49
61
|
|
|
50
62
|
const body = JSON.stringify({ event, data, timestamp: new Date().toISOString() })
|
|
51
63
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
await Promise.allSettled(
|
|
65
|
+
endpoints.map((endpoint) => _sendToEndpoint(endpoint as any, body, payload, slugs)),
|
|
66
|
+
)
|
|
55
67
|
} catch (err) {
|
|
56
68
|
console.error(`[webhook] Failed to fetch endpoints for event ${event}:`, err)
|
|
57
69
|
}
|
|
@@ -40,6 +40,11 @@ export const ChatViewClient: React.FC = () => {
|
|
|
40
40
|
const sessionsPollTimeout = useRef<NodeJS.Timeout>(undefined)
|
|
41
41
|
const messagesPollInterval = useRef(3000)
|
|
42
42
|
const messagesPollTimeout = useRef<NodeJS.Timeout>(undefined)
|
|
43
|
+
// Serialized snapshot of the last sessions payload. The change detection MUST
|
|
44
|
+
// live outside the setSessions updater: React skips the updater entirely on the
|
|
45
|
+
// eager-state path, so a flag assigned inside it stops being written after the
|
|
46
|
+
// first render and the adaptive backoff then climbs to its ceiling and stays there.
|
|
47
|
+
const sessionsSnapshotRef = useRef<string>('')
|
|
43
48
|
|
|
44
49
|
// Fetch sessions list
|
|
45
50
|
const [sessionExpired, setSessionExpired] = useState(false)
|
|
@@ -50,15 +55,14 @@ export const ChatViewClient: React.FC = () => {
|
|
|
50
55
|
if (res.status === 401 || res.status === 403) { setSessionExpired(true); return }
|
|
51
56
|
if (res.ok) {
|
|
52
57
|
const data = await res.json()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
})
|
|
58
|
+
const newActive = data.active || []
|
|
59
|
+
const newClosed = data.closed || []
|
|
60
|
+
const snapshot = JSON.stringify([newActive, newClosed])
|
|
61
|
+
if (snapshot !== sessionsSnapshotRef.current) {
|
|
62
|
+
sessionsSnapshotRef.current = snapshot
|
|
63
|
+
hadChanges = true
|
|
64
|
+
setSessions({ active: newActive, closed: newClosed })
|
|
65
|
+
}
|
|
62
66
|
}
|
|
63
67
|
} catch { /* ignore */ }
|
|
64
68
|
setLoading(false)
|
|
@@ -144,26 +144,25 @@ export const CrmClient: React.FC = () => {
|
|
|
144
144
|
const fetchDetail = useCallback(async (clientId: number) => {
|
|
145
145
|
setDetailLoading(true)
|
|
146
146
|
try {
|
|
147
|
-
const [clientRes, ticketsRes, projectsRes
|
|
147
|
+
const [clientRes, ticketsRes, projectsRes] = await Promise.all([
|
|
148
148
|
fetch(`/api/support-clients/${clientId}?depth=0`),
|
|
149
149
|
fetch(`/api/tickets?where[client][equals]=${clientId}&sort=-createdAt&limit=20&depth=0`),
|
|
150
150
|
fetch(`/api/projects?where[client][equals]=${clientId}&depth=0`),
|
|
151
|
-
fetch(`/api/time-entries?limit=0&depth=0`),
|
|
152
151
|
])
|
|
153
152
|
|
|
154
153
|
const client = clientRes.ok ? await clientRes.json() : null
|
|
155
154
|
const tickets = ticketsRes.ok ? (await ticketsRes.json()).docs || [] : []
|
|
156
155
|
const projects = projectsRes.ok ? (await projectsRes.json()).docs || [] : []
|
|
157
156
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
157
|
+
// Sum the per-ticket rollup maintained by the TimeEntries afterChange /
|
|
158
|
+
// afterDelete hooks. This used to download the ENTIRE time-entries table
|
|
159
|
+
// (via limit=0, which does not mean "no document") only to keep the rows whose
|
|
160
|
+
// ticket is one of the 20 listed here — same result, O(1) payload instead of
|
|
161
|
+
// O(all time entries ever recorded).
|
|
162
|
+
const totalTimeMinutes = tickets.reduce(
|
|
163
|
+
(sum: number, t: { totalTimeMinutes?: number }) => sum + (t.totalTimeMinutes || 0),
|
|
164
|
+
0,
|
|
165
|
+
)
|
|
167
166
|
|
|
168
167
|
const openTickets = tickets.filter((t: { status: string }) =>
|
|
169
168
|
['open', 'waiting_client'].includes(t.status),
|
|
@@ -540,7 +540,7 @@ export const SupportDashboardClient: React.FC = () => {
|
|
|
540
540
|
|
|
541
541
|
{/* CSAT ring */}
|
|
542
542
|
<div className={styles.panel}>
|
|
543
|
-
<h2 className={styles.panelTitle}>{t('dashboard.
|
|
543
|
+
<h2 className={styles.panelTitle}>{t('dashboard.csatTitle')}</h2>
|
|
544
544
|
<CSATRing score={stats.satisfactionAvg} count={stats.satisfactionCount} />
|
|
545
545
|
</div>
|
|
546
546
|
</div>
|
|
@@ -6,7 +6,7 @@ import Link from 'next/link'
|
|
|
6
6
|
import { RichTextEditor, type RichTextEditorHandle } from '../../components/RichTextEditor/index'
|
|
7
7
|
import { hasCodeBlocks, MessageWithCodeBlocks, CodeBlockRendererHtml } from '../../components/TicketConversation/components/CodeBlock'
|
|
8
8
|
import { CodeBlockInserter } from '../../components/TicketConversation/components/CodeBlockInserter'
|
|
9
|
-
import {
|
|
9
|
+
import { useFeatures } from '../../components/TicketConversation/hooks/useFeatures'
|
|
10
10
|
import { computeSlaState, formatSlaRemaining } from '../shared/sla'
|
|
11
11
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
12
12
|
import { DATE_LOCALE } from '../shared/dateLocale'
|
|
@@ -21,7 +21,7 @@ export const TicketDetailClient: React.FC = () => {
|
|
|
21
21
|
const { t } = useTranslation()
|
|
22
22
|
const searchParams = useSearchParams()
|
|
23
23
|
const ticketId = searchParams.get('id')
|
|
24
|
-
const features =
|
|
24
|
+
const { features } = useFeatures()
|
|
25
25
|
const editorRef = useRef<RichTextEditorHandle>(null)
|
|
26
26
|
const threadEndRef = useRef<HTMLDivElement>(null)
|
|
27
27
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
@@ -165,12 +165,17 @@ export const TicketInboxClient: React.FC = () => {
|
|
|
165
165
|
try {
|
|
166
166
|
// Exclude snoozed tickets from the counts too (mirrors the list query).
|
|
167
167
|
const sn = `where[and][0][or][0][snoozeUntil][exists]=false&where[and][0][or][1][snoozeUntil][less_than_equal]=${encodeURIComponent(new Date().toISOString())}`
|
|
168
|
+
// `/count` is a real SQL COUNT. The previous limit=0 idiom did NOT mean
|
|
169
|
+
// "no document": Payload disables pagination for limit=0, skips the count
|
|
170
|
+
// query and derives totalDocs from the rows it just materialised — so each
|
|
171
|
+
// of these five counters used to read the whole tickets table (relations
|
|
172
|
+
// included) just to display a badge.
|
|
168
173
|
const [all, openRes, waiting, resolved, breach] = await Promise.all([
|
|
169
|
-
fetch(`/api/tickets
|
|
170
|
-
fetch(`/api/tickets?
|
|
171
|
-
fetch(`/api/tickets?
|
|
172
|
-
fetch(`/api/tickets?
|
|
173
|
-
fetch(`/api/tickets?
|
|
174
|
+
fetch(`/api/tickets/count?${sn}`, { credentials: 'include' }),
|
|
175
|
+
fetch(`/api/tickets/count?where[status][equals]=open&${sn}`, { credentials: 'include' }),
|
|
176
|
+
fetch(`/api/tickets/count?where[status][equals]=waiting_client&${sn}`, { credentials: 'include' }),
|
|
177
|
+
fetch(`/api/tickets/count?where[status][equals]=resolved&${sn}`, { credentials: 'include' }),
|
|
178
|
+
fetch(`/api/tickets/count?where[or][0][slaFirstResponseBreached][equals]=true&where[or][1][slaResolutionBreached][equals]=true&${sn}`, { credentials: 'include' }),
|
|
174
179
|
])
|
|
175
180
|
const [a, o, w, r, b] = await Promise.all([all.json(), openRes.json(), waiting.json(), resolved.json(), breach.json()])
|
|
176
181
|
setCounts({
|
|
@@ -4,7 +4,8 @@ import React, { useState, useEffect } from 'react'
|
|
|
4
4
|
import { Settings, Mail, Bot, Clock, Timer, Globe, FileSignature } from 'lucide-react'
|
|
5
5
|
import { V, btnStyle } from '../shared/adminTokens'
|
|
6
6
|
import { AdminViewHeader } from '../shared/AdminViewHeader'
|
|
7
|
-
import {
|
|
7
|
+
import { DEFAULT_FEATURES, type TicketingFeatures } from '../../components/TicketConversation/config'
|
|
8
|
+
import { useFeatures } from '../../components/TicketConversation/hooks/useFeatures'
|
|
8
9
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
9
10
|
import ts from '../../styles/TicketingSettings.module.scss'
|
|
10
11
|
|
|
@@ -301,7 +302,7 @@ export const TicketingSettingsClient: React.FC = () => {
|
|
|
301
302
|
const { t } = useTranslation()
|
|
302
303
|
const FEATURE_LIST = React.useMemo(() => getFeatureList(t), [t])
|
|
303
304
|
const CATEGORIES = React.useMemo(() => getCategories(t), [t])
|
|
304
|
-
const
|
|
305
|
+
const { features, setFeatures, save: saveFeatures } = useFeatures()
|
|
305
306
|
const [settings, setSettings] = useState<AllSettings>(DEFAULT_SETTINGS)
|
|
306
307
|
const [signature, setSignature] = useState('')
|
|
307
308
|
const [saved, setSaved] = useState(false)
|
|
@@ -355,15 +356,16 @@ export const TicketingSettingsClient: React.FC = () => {
|
|
|
355
356
|
|
|
356
357
|
const handleSave = async () => {
|
|
357
358
|
setSaving(true)
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
//
|
|
359
|
+
// Feature flags and the other blocks share one preference row, so the two
|
|
360
|
+
// writes are sequenced: running them in parallel would make the second
|
|
361
|
+
// request merge onto settings read before the first one landed.
|
|
362
|
+
const featuresOk = await saveFeatures(features)
|
|
361
363
|
const [settingsOk, prefsOk] = await Promise.all([
|
|
362
364
|
saveSettingsToAPI(settings),
|
|
363
365
|
saveUserPrefs({ locale: settings.locale.language, signature }),
|
|
364
366
|
])
|
|
365
367
|
setSaving(false)
|
|
366
|
-
if (settingsOk && prefsOk) {
|
|
368
|
+
if (featuresOk && settingsOk && prefsOk) {
|
|
367
369
|
setSaved(true)
|
|
368
370
|
setTimeout(() => setSaved(false), 3000)
|
|
369
371
|
}
|
|
@@ -413,7 +415,7 @@ export const TicketingSettingsClient: React.FC = () => {
|
|
|
413
415
|
* SECTION 1 — Feature Flags
|
|
414
416
|
* ======================================== */}
|
|
415
417
|
<CollapsibleSection
|
|
416
|
-
title={t('settingsView.
|
|
418
|
+
title={t('settingsView.featuresTitle')}
|
|
417
419
|
icon={<Settings size={16} />}
|
|
418
420
|
color={V.blue}
|
|
419
421
|
badge={
|
|
@@ -1,82 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Ticketing feature flags — re-export of the shared implementation.
|
|
3
|
+
*
|
|
4
|
+
* See `utils/features.ts`. This module is kept so that
|
|
5
|
+
* `views/shared/index.ts` and existing view imports keep resolving.
|
|
5
6
|
*/
|
|
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
|
-
}
|
|
7
|
+
export {
|
|
8
|
+
DEFAULT_TICKETING_FEATURES,
|
|
9
|
+
DEFAULT_TICKETING_FEATURES as DEFAULT_FEATURES,
|
|
10
|
+
FEATURES_CACHE_KEY,
|
|
11
|
+
fetchFeatures,
|
|
12
|
+
getFeatures,
|
|
13
|
+
normalizeFeatures,
|
|
14
|
+
saveFeatures,
|
|
15
|
+
} from '../../utils/features'
|
|
16
|
+
export type { TicketingFeatures } from '../../utils/features'
|
|
@@ -2,8 +2,10 @@ export { AdminErrorBoundary } from './ErrorBoundary'
|
|
|
2
2
|
export { V, btnStyle } from './adminTokens'
|
|
3
3
|
export { AdminViewHeader } from './AdminViewHeader'
|
|
4
4
|
export { Skeleton, SkeletonText, SkeletonCard, SkeletonTable, SkeletonDashboard } from './Skeleton'
|
|
5
|
-
export { getFeatures, saveFeatures, DEFAULT_FEATURES } from './config'
|
|
5
|
+
export { getFeatures, saveFeatures, fetchFeatures, normalizeFeatures, DEFAULT_FEATURES } from './config'
|
|
6
6
|
export type { TicketingFeatures } from './config'
|
|
7
|
+
export { useFeatures } from '../../components/TicketConversation/hooks/useFeatures'
|
|
8
|
+
export type { UseFeaturesResult } from '../../components/TicketConversation/hooks/useFeatures'
|
|
7
9
|
export { StatusPill } from './StatusPill'
|
|
8
10
|
export type { StatusPillProps, TicketStatusVariant } from './StatusPill'
|
|
9
11
|
export { computeSlaState, formatSlaRemaining } from './sla'
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
"activeTickets": "Active tickets",
|
|
267
267
|
"noActiveTickets": "No active tickets",
|
|
268
268
|
"volume7days": "Volume — last 7 days",
|
|
269
|
-
"
|
|
269
|
+
"csatTitle": "Client satisfaction (CSAT)",
|
|
270
270
|
"newTicketAction": "+ New ticket",
|
|
271
271
|
"pendingEmails": "Pending emails",
|
|
272
272
|
"crm": "CRM Clients",
|
|
@@ -599,7 +599,7 @@
|
|
|
599
599
|
"settingsView": {
|
|
600
600
|
"configTitle": "Support module configuration",
|
|
601
601
|
"intro": "Manage features, email configuration, AI and support module preferences.",
|
|
602
|
-
"
|
|
602
|
+
"featuresTitle": "Features",
|
|
603
603
|
"featuresDescription": "Enable or disable support module features.",
|
|
604
604
|
"reset": "Reset",
|
|
605
605
|
"saved": "✓ Saved",
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
"activeTickets": "Tickets actifs",
|
|
267
267
|
"noActiveTickets": "Aucun ticket actif",
|
|
268
268
|
"volume7days": "Volume — 7 derniers jours",
|
|
269
|
-
"
|
|
269
|
+
"csatTitle": "Satisfaction client (CSAT)",
|
|
270
270
|
"newTicketAction": "+ Nouveau ticket",
|
|
271
271
|
"pendingEmails": "Emails en attente",
|
|
272
272
|
"crm": "CRM Clients",
|
|
@@ -599,7 +599,7 @@
|
|
|
599
599
|
"settingsView": {
|
|
600
600
|
"configTitle": "Configuration du module Support",
|
|
601
601
|
"intro": "Gérez les fonctionnalités, la configuration email, l'IA et les préférences du module de support.",
|
|
602
|
-
"
|
|
602
|
+
"featuresTitle": "Fonctionnalités",
|
|
603
603
|
"featuresDescription": "Activez ou désactivez les fonctionnalités du module de support.",
|
|
604
604
|
"reset": "Réinitialiser",
|
|
605
605
|
"saved": "✓ Sauvegardé",
|