@consilioweb/payload-support 0.5.1
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/LICENSE +21 -0
- package/README.md +525 -0
- package/dist/client.cjs +7 -0
- package/dist/client.d.cts +3 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +5 -0
- package/dist/index.cjs +7766 -0
- package/dist/index.d.cts +384 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.js +7730 -0
- package/dist/views.d.cts +30 -0
- package/dist/views.d.ts +30 -0
- package/package.json +131 -0
- package/src/client.ts +1 -0
- package/src/collections/AuthLogs.ts +65 -0
- package/src/collections/CannedResponses.ts +69 -0
- package/src/collections/ChatMessages.ts +98 -0
- package/src/collections/EmailLogs.ts +94 -0
- package/src/collections/KnowledgeBase.ts +99 -0
- package/src/collections/Macros.ts +98 -0
- package/src/collections/PendingEmails.ts +122 -0
- package/src/collections/SatisfactionSurveys.ts +98 -0
- package/src/collections/SlaPolicies.ts +123 -0
- package/src/collections/SupportClients.ts +210 -0
- package/src/collections/TicketActivityLog.ts +81 -0
- package/src/collections/TicketMessages.ts +364 -0
- package/src/collections/TicketStatuses.ts +108 -0
- package/src/collections/Tickets.ts +704 -0
- package/src/collections/TimeEntries.ts +105 -0
- package/src/collections/WebhookEndpoints.ts +96 -0
- package/src/collections/index.ts +16 -0
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
- package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
- package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
- package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
- package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
- package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
- package/src/components/TicketConversation/config.ts +82 -0
- package/src/components/TicketConversation/constants.ts +74 -0
- package/src/components/TicketConversation/context.ts +63 -0
- package/src/components/TicketConversation/hooks/useAI.ts +180 -0
- package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
- package/src/components/TicketConversation/hooks/useReply.ts +190 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
- package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
- package/src/components/TicketConversation/index.tsx +1110 -0
- package/src/components/TicketConversation/locales/en.json +878 -0
- package/src/components/TicketConversation/locales/fr.json +878 -0
- package/src/components/TicketConversation/types.ts +54 -0
- package/src/components/TicketConversation/utils.ts +25 -0
- package/src/endpoints/admin-chat-stream.ts +238 -0
- package/src/endpoints/admin-chat.ts +263 -0
- package/src/endpoints/admin-stats.ts +200 -0
- package/src/endpoints/ai.ts +199 -0
- package/src/endpoints/apply-macro.ts +144 -0
- package/src/endpoints/auth-2fa.ts +163 -0
- package/src/endpoints/auto-close.ts +175 -0
- package/src/endpoints/billing.ts +167 -0
- package/src/endpoints/bulk-action.ts +103 -0
- package/src/endpoints/chat-stream.ts +127 -0
- package/src/endpoints/chat.ts +188 -0
- package/src/endpoints/chatbot.ts +113 -0
- package/src/endpoints/delete-account.ts +129 -0
- package/src/endpoints/email-stats.ts +109 -0
- package/src/endpoints/export-csv.ts +84 -0
- package/src/endpoints/export-data.ts +104 -0
- package/src/endpoints/import-conversation.ts +307 -0
- package/src/endpoints/index.ts +154 -0
- package/src/endpoints/login.ts +92 -0
- package/src/endpoints/merge-clients.ts +132 -0
- package/src/endpoints/merge-tickets.ts +137 -0
- package/src/endpoints/oauth-google.ts +179 -0
- package/src/endpoints/pending-emails-process.ts +224 -0
- package/src/endpoints/presence.ts +104 -0
- package/src/endpoints/process-scheduled.ts +144 -0
- package/src/endpoints/purge-logs.ts +58 -0
- package/src/endpoints/resend-notification.ts +99 -0
- package/src/endpoints/round-robin-config.ts +92 -0
- package/src/endpoints/satisfaction.ts +93 -0
- package/src/endpoints/search.ts +106 -0
- package/src/endpoints/seed-kb.ts +153 -0
- package/src/endpoints/settings.ts +144 -0
- package/src/endpoints/signature.ts +93 -0
- package/src/endpoints/sla-check.ts +124 -0
- package/src/endpoints/split-ticket.ts +131 -0
- package/src/endpoints/statuses.ts +45 -0
- package/src/endpoints/track-open.ts +154 -0
- package/src/endpoints/typing.ts +101 -0
- package/src/endpoints/user-prefs.ts +125 -0
- package/src/hooks/checkSLA.ts +414 -0
- package/src/hooks/ticketStatusEmail.ts +182 -0
- package/src/index.ts +51 -0
- package/src/plugin.ts +157 -0
- package/src/portal/LiveChat.tsx +1353 -0
- package/src/portal/auth/ChatWidget.tsx +350 -0
- package/src/portal/auth/ChatbotWidget.tsx +285 -0
- package/src/portal/auth/SupportHeader.tsx +409 -0
- package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
- package/src/portal/auth/dashboard/page.tsx +84 -0
- package/src/portal/auth/faq/FAQSearch.tsx +117 -0
- package/src/portal/auth/faq/page.tsx +199 -0
- package/src/portal/auth/layout.tsx +61 -0
- package/src/portal/auth/profile/page.tsx +705 -0
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
- package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
- package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
- package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
- package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
- package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
- package/src/portal/auth/tickets/detail/page.tsx +738 -0
- package/src/portal/auth/tickets/new/page.tsx +515 -0
- package/src/portal/forgot-password/page.tsx +114 -0
- package/src/portal/layout.tsx +26 -0
- package/src/portal/locales/en.json +374 -0
- package/src/portal/locales/fr.json +374 -0
- package/src/portal/login/page.tsx +351 -0
- package/src/portal/page.tsx +162 -0
- package/src/portal/register/page.tsx +281 -0
- package/src/portal/reset-password/page.tsx +152 -0
- package/src/styles/BillingView.module.scss +311 -0
- package/src/styles/ChatView.module.scss +438 -0
- package/src/styles/CommandPalette.module.scss +160 -0
- package/src/styles/CrmView.module.scss +554 -0
- package/src/styles/EmailTracking.module.scss +238 -0
- package/src/styles/ImportConversation.module.scss +267 -0
- package/src/styles/Layout.module.scss +55 -0
- package/src/styles/Logs.module.scss +164 -0
- package/src/styles/NewTicket.module.scss +143 -0
- package/src/styles/PendingEmails.module.scss +629 -0
- package/src/styles/SupportDashboard.module.scss +649 -0
- package/src/styles/TicketDetail.module.scss +1043 -0
- package/src/styles/TicketInbox.module.scss +296 -0
- package/src/styles/TicketingSettings.module.scss +358 -0
- package/src/styles/TimeDashboard.module.scss +287 -0
- package/src/styles/_tokens.scss +78 -0
- package/src/styles/theme.css +633 -0
- package/src/types.ts +255 -0
- package/src/utils/adminNotification.ts +38 -0
- package/src/utils/auth.ts +46 -0
- package/src/utils/emailTemplate.ts +343 -0
- package/src/utils/fireWebhooks.ts +84 -0
- package/src/utils/index.ts +22 -0
- package/src/utils/rateLimiter.ts +52 -0
- package/src/utils/readSettings.ts +67 -0
- package/src/utils/slugs.ts +54 -0
- package/src/utils/webhookDispatcher.ts +120 -0
- package/src/views/BillingView/client.tsx +137 -0
- package/src/views/BillingView/index.tsx +33 -0
- package/src/views/ChatView/client.tsx +294 -0
- package/src/views/ChatView/index.tsx +33 -0
- package/src/views/CrmView/client.tsx +206 -0
- package/src/views/CrmView/index.tsx +33 -0
- package/src/views/EmailTrackingView/client.tsx +124 -0
- package/src/views/EmailTrackingView/index.tsx +33 -0
- package/src/views/ImportConversationView/client.tsx +133 -0
- package/src/views/ImportConversationView/index.tsx +33 -0
- package/src/views/LogsView/client.tsx +151 -0
- package/src/views/LogsView/index.tsx +30 -0
- package/src/views/NewTicketView/client.tsx +227 -0
- package/src/views/NewTicketView/index.tsx +30 -0
- package/src/views/PendingEmailsView/client.tsx +177 -0
- package/src/views/PendingEmailsView/index.tsx +33 -0
- package/src/views/SupportDashboardView/client.tsx +424 -0
- package/src/views/SupportDashboardView/index.tsx +33 -0
- package/src/views/TicketDetailView/client.tsx +775 -0
- package/src/views/TicketDetailView/index.tsx +33 -0
- package/src/views/TicketInboxView/client.tsx +313 -0
- package/src/views/TicketInboxView/index.tsx +30 -0
- package/src/views/TicketingSettingsView/client.tsx +866 -0
- package/src/views/TicketingSettingsView/index.tsx +33 -0
- package/src/views/TimeDashboardView/client.tsx +144 -0
- package/src/views/TimeDashboardView/index.tsx +33 -0
- package/src/views/shared/AdminViewHeader.tsx +69 -0
- package/src/views/shared/ErrorBoundary.tsx +68 -0
- package/src/views/shared/Skeleton.tsx +125 -0
- package/src/views/shared/adminTokens.ts +37 -0
- package/src/views/shared/config.ts +82 -0
- package/src/views/shared/index.ts +6 -0
- package/src/views.ts +16 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CollectionConfig,
|
|
3
|
+
CollectionBeforeChangeHook,
|
|
4
|
+
CollectionAfterChangeHook,
|
|
5
|
+
CollectionBeforeDeleteHook,
|
|
6
|
+
Field,
|
|
7
|
+
} from 'payload'
|
|
8
|
+
import type { CollectionSlugs } from '../utils/slugs'
|
|
9
|
+
import { escapeHtml } from '../utils/emailTemplate'
|
|
10
|
+
import { fireWebhooks } from '../utils/fireWebhooks'
|
|
11
|
+
import { createAdminNotification } from '../utils/adminNotification'
|
|
12
|
+
import { dispatchWebhook } from '../utils/webhookDispatcher'
|
|
13
|
+
import { readSupportSettings } from '../utils/readSettings'
|
|
14
|
+
import { createTicketStatusEmail } from '../hooks/ticketStatusEmail'
|
|
15
|
+
import { createAssignSlaDeadlines, createCheckSlaOnResolve } from '../hooks/checkSLA'
|
|
16
|
+
|
|
17
|
+
// ─── Hooks ───────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function createAssignTicketNumber(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
20
|
+
return async ({ data, operation, req }) => {
|
|
21
|
+
if (operation === 'create') {
|
|
22
|
+
// Retry loop to handle unique constraint collisions (e.g. concurrent inserts in SQLite).
|
|
23
|
+
let retries = 3
|
|
24
|
+
while (retries > 0) {
|
|
25
|
+
try {
|
|
26
|
+
const countResult = await req.payload.count({
|
|
27
|
+
collection: slugs.tickets,
|
|
28
|
+
overrideAccess: true,
|
|
29
|
+
})
|
|
30
|
+
const baseNumber = countResult.totalDocs + 1
|
|
31
|
+
data.ticketNumber = `TK-${String(baseNumber).padStart(4, '0')}`
|
|
32
|
+
|
|
33
|
+
// Double-check: if this number already exists, append a timestamp suffix
|
|
34
|
+
const existing = await req.payload.find({
|
|
35
|
+
collection: slugs.tickets,
|
|
36
|
+
where: { ticketNumber: { equals: data.ticketNumber } },
|
|
37
|
+
limit: 1,
|
|
38
|
+
depth: 0,
|
|
39
|
+
overrideAccess: true,
|
|
40
|
+
})
|
|
41
|
+
if (existing.docs.length > 0) {
|
|
42
|
+
const suffix = Date.now() % 10000
|
|
43
|
+
data.ticketNumber = `TK-${String(baseNumber + suffix).padStart(4, '0')}`
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
} catch (error: any) {
|
|
47
|
+
if (
|
|
48
|
+
retries > 1 &&
|
|
49
|
+
(error?.message?.includes('UNIQUE') ||
|
|
50
|
+
error?.message?.includes('unique') ||
|
|
51
|
+
error?.code === 'SQLITE_CONSTRAINT')
|
|
52
|
+
) {
|
|
53
|
+
retries--
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
throw error
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return data
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createAssignClientOnCreate(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
65
|
+
return async ({ data, operation, req }) => {
|
|
66
|
+
if (operation === 'create' && req.user?.collection === slugs.supportClients && !data.client) {
|
|
67
|
+
data.client = req.user.id
|
|
68
|
+
}
|
|
69
|
+
return data
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const autoPaidAt: CollectionBeforeChangeHook = async ({ data, operation, originalDoc }) => {
|
|
74
|
+
if (operation !== 'update') return data
|
|
75
|
+
if (data.paymentStatus === 'paid' && originalDoc?.paymentStatus !== 'paid' && !data.paidAt) {
|
|
76
|
+
data.paidAt = new Date().toISOString()
|
|
77
|
+
}
|
|
78
|
+
return data
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createRestrictClientUpdates(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
82
|
+
return async ({ data, operation, req, originalDoc }) => {
|
|
83
|
+
if (operation !== 'update') return data
|
|
84
|
+
if (req.user?.collection !== slugs.supportClients) return data
|
|
85
|
+
const allowedStatuses = ['open', 'resolved']
|
|
86
|
+
const newData: Record<string, unknown> = {}
|
|
87
|
+
if (data.status && allowedStatuses.includes(data.status as string)) {
|
|
88
|
+
newData.status = data.status
|
|
89
|
+
}
|
|
90
|
+
return { ...originalDoc, ...newData }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createAutoAssignAdmin(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
95
|
+
return async ({ data, operation, req }) => {
|
|
96
|
+
if (operation === 'create' && !data.assignedTo) {
|
|
97
|
+
const { payload } = req
|
|
98
|
+
let roundRobinEnabled = false
|
|
99
|
+
try {
|
|
100
|
+
const prefs = await payload.find({
|
|
101
|
+
collection: 'payload-preferences',
|
|
102
|
+
where: { key: { equals: 'support-round-robin' } },
|
|
103
|
+
limit: 1,
|
|
104
|
+
depth: 0,
|
|
105
|
+
overrideAccess: true,
|
|
106
|
+
})
|
|
107
|
+
if (prefs.docs.length > 0) {
|
|
108
|
+
roundRobinEnabled = (prefs.docs[0].value as { enabled?: boolean })?.enabled === true
|
|
109
|
+
}
|
|
110
|
+
} catch (err) { console.warn('[support] Failed to read round-robin preferences:', err) }
|
|
111
|
+
|
|
112
|
+
const admins = await payload.find({
|
|
113
|
+
collection: slugs.users,
|
|
114
|
+
limit: 100,
|
|
115
|
+
depth: 0,
|
|
116
|
+
overrideAccess: true,
|
|
117
|
+
})
|
|
118
|
+
if (admins.docs.length === 0) return data
|
|
119
|
+
|
|
120
|
+
if (roundRobinEnabled && admins.docs.length > 1) {
|
|
121
|
+
const counts = await Promise.all(
|
|
122
|
+
admins.docs.map(async (admin) => {
|
|
123
|
+
const result = await payload.count({
|
|
124
|
+
collection: slugs.tickets,
|
|
125
|
+
where: { assignedTo: { equals: admin.id }, status: { in: ['open', 'waiting_client'] } },
|
|
126
|
+
overrideAccess: true,
|
|
127
|
+
})
|
|
128
|
+
return { id: admin.id, count: result.totalDocs }
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
counts.sort((a, b) => a.count - b.count)
|
|
132
|
+
data.assignedTo = counts[0].id
|
|
133
|
+
} else {
|
|
134
|
+
data.assignedTo = admins.docs[0].id
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return data
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createTrackSLA(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
142
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
143
|
+
if (operation !== 'update') return doc
|
|
144
|
+
const statusChanged = previousDoc?.status !== doc.status
|
|
145
|
+
if (statusChanged && doc.status === 'resolved' && !doc.resolvedAt) {
|
|
146
|
+
await req.payload.update({
|
|
147
|
+
collection: slugs.tickets,
|
|
148
|
+
id: doc.id,
|
|
149
|
+
data: { resolvedAt: new Date().toISOString() },
|
|
150
|
+
overrideAccess: true,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
return doc
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createLogTicketActivity(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
158
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
159
|
+
if (operation !== 'update' || !previousDoc) return doc
|
|
160
|
+
const changes: { field: string; oldValue: string; newValue: string }[] = []
|
|
161
|
+
const trackedFields = ['status', 'priority', 'category', 'assignedTo'] as const
|
|
162
|
+
const displayVal = (val: unknown): string => {
|
|
163
|
+
if (val === null || val === undefined || val === '') return '(vide)'
|
|
164
|
+
if (typeof val === 'object') {
|
|
165
|
+
const obj = val as Record<string, unknown>
|
|
166
|
+
return String(obj.email || obj.name || obj.title || obj.id || JSON.stringify(val))
|
|
167
|
+
}
|
|
168
|
+
return String(val)
|
|
169
|
+
}
|
|
170
|
+
for (const field of trackedFields) {
|
|
171
|
+
const oldVal = previousDoc[field]
|
|
172
|
+
const newVal = doc[field]
|
|
173
|
+
const oldId = typeof oldVal === 'object' && oldVal !== null ? (oldVal as Record<string, unknown>).id : oldVal
|
|
174
|
+
const newId = typeof newVal === 'object' && newVal !== null ? (newVal as Record<string, unknown>).id : newVal
|
|
175
|
+
if (oldId !== newId) {
|
|
176
|
+
changes.push({ field, oldValue: displayVal(oldVal), newValue: displayVal(newVal) })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (changes.length === 0) return doc
|
|
180
|
+
const actorType = req.user?.collection === slugs.users ? 'admin' : 'client'
|
|
181
|
+
for (const change of changes) {
|
|
182
|
+
try {
|
|
183
|
+
await req.payload.create({
|
|
184
|
+
collection: slugs.ticketActivityLog,
|
|
185
|
+
data: {
|
|
186
|
+
ticket: doc.id,
|
|
187
|
+
action: `${change.field}_changed`,
|
|
188
|
+
detail: `${change.field}: ${change.oldValue || '(vide)'} → ${change.newValue}`,
|
|
189
|
+
actorType,
|
|
190
|
+
actorEmail: req.user?.email || 'system',
|
|
191
|
+
},
|
|
192
|
+
overrideAccess: true,
|
|
193
|
+
})
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error('[support] Failed to log activity:', err)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return doc
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function createNotifyOnAssignment(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
203
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
204
|
+
if (operation !== 'update' || !previousDoc) return doc
|
|
205
|
+
const oldAssigned = typeof previousDoc.assignedTo === 'object' ? previousDoc.assignedTo?.id : previousDoc.assignedTo
|
|
206
|
+
const newAssigned = typeof doc.assignedTo === 'object' ? doc.assignedTo?.id : doc.assignedTo
|
|
207
|
+
if (!newAssigned || oldAssigned === newAssigned) return doc
|
|
208
|
+
try {
|
|
209
|
+
const assignee = typeof doc.assignedTo === 'object' && doc.assignedTo?.email
|
|
210
|
+
? doc.assignedTo
|
|
211
|
+
: await req.payload.findByID({ collection: slugs.users, id: newAssigned, depth: 0, overrideAccess: true })
|
|
212
|
+
if (!assignee?.email) return doc
|
|
213
|
+
const settings = await readSupportSettings(req.payload)
|
|
214
|
+
const ticketNumber = doc.ticketNumber || 'TK-????'
|
|
215
|
+
const subject = doc.subject || 'Support'
|
|
216
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || ''
|
|
217
|
+
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || ''
|
|
218
|
+
await req.payload.sendEmail({
|
|
219
|
+
to: assignee.email,
|
|
220
|
+
...(replyTo ? { replyTo } : {}),
|
|
221
|
+
subject: `Ticket assigne : [${ticketNumber}] ${subject}`,
|
|
222
|
+
html: `<p>Le ticket <strong>${escapeHtml(ticketNumber)}</strong> — <em>${escapeHtml(subject)}</em> — vous a ete assigne.</p><p><a href="${baseUrl}/admin/collections/${slugs.tickets}/${doc.id}">Ouvrir le ticket</a></p>`,
|
|
223
|
+
})
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.error('[support] Failed to notify on assignment:', err)
|
|
226
|
+
}
|
|
227
|
+
return doc
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function createNotifyClientOnResolve(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
232
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
233
|
+
if (operation !== 'update' || !previousDoc) return doc
|
|
234
|
+
if (previousDoc.status === doc.status || doc.status !== 'resolved') return doc
|
|
235
|
+
try {
|
|
236
|
+
const client = typeof doc.client === 'object' ? doc.client : null
|
|
237
|
+
const clientId = typeof doc.client === 'number' ? doc.client : client?.id
|
|
238
|
+
const clientData = client?.email ? client : (clientId ? await req.payload.findByID({
|
|
239
|
+
collection: slugs.supportClients,
|
|
240
|
+
id: clientId,
|
|
241
|
+
depth: 0,
|
|
242
|
+
overrideAccess: true,
|
|
243
|
+
}) : null)
|
|
244
|
+
if (!clientData?.email || clientData.notifyOnStatusChange === false) return doc
|
|
245
|
+
const settings = await readSupportSettings(req.payload)
|
|
246
|
+
const ticketNumber = doc.ticketNumber || 'TK-????'
|
|
247
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || ''
|
|
248
|
+
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || ''
|
|
249
|
+
await req.payload.sendEmail({
|
|
250
|
+
to: clientData.email,
|
|
251
|
+
...(replyTo ? { replyTo } : {}),
|
|
252
|
+
subject: `[${ticketNumber}] Ticket resolu`,
|
|
253
|
+
html: `<p>Bonjour ${escapeHtml(clientData.firstName || '')},</p><p>Votre ticket <strong>${escapeHtml(ticketNumber)}</strong> a ete resolu.</p><p><a href="${baseUrl}/support/tickets/${doc.id}">Consulter le ticket</a></p>`,
|
|
254
|
+
})
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.error('[support] Failed to notify client on resolve:', err)
|
|
257
|
+
}
|
|
258
|
+
return doc
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function createAutoCalculateSLA(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
263
|
+
return async ({ doc, operation, req }) => {
|
|
264
|
+
if (operation !== 'create') return doc
|
|
265
|
+
try {
|
|
266
|
+
const { payload } = req
|
|
267
|
+
const ticketPriority = doc.priority || 'normal'
|
|
268
|
+
|
|
269
|
+
// Try to find a SLA policy matching the ticket's priority first
|
|
270
|
+
let policy: any = null
|
|
271
|
+
const byPriority = await payload.find({
|
|
272
|
+
collection: slugs.slaPolicies as any,
|
|
273
|
+
where: { priority: { equals: ticketPriority } },
|
|
274
|
+
limit: 1,
|
|
275
|
+
depth: 0,
|
|
276
|
+
overrideAccess: true,
|
|
277
|
+
})
|
|
278
|
+
if (byPriority.docs.length > 0) {
|
|
279
|
+
policy = byPriority.docs[0]
|
|
280
|
+
} else {
|
|
281
|
+
// Fall back to the default policy
|
|
282
|
+
const defaults = await payload.find({
|
|
283
|
+
collection: slugs.slaPolicies as any,
|
|
284
|
+
where: { isDefault: { equals: true } },
|
|
285
|
+
limit: 1,
|
|
286
|
+
depth: 0,
|
|
287
|
+
overrideAccess: true,
|
|
288
|
+
})
|
|
289
|
+
if (defaults.docs.length > 0) {
|
|
290
|
+
policy = defaults.docs[0]
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!policy) return doc
|
|
295
|
+
|
|
296
|
+
// SLA fields are in minutes (firstResponseTime, resolutionTime)
|
|
297
|
+
const now = new Date()
|
|
298
|
+
const firstResponseMinutes = policy.firstResponseTime || 240 // default 4h
|
|
299
|
+
const resolutionMinutes = policy.resolutionTime || 1440 // default 24h
|
|
300
|
+
const firstResponseDue = new Date(now.getTime() + firstResponseMinutes * 60000)
|
|
301
|
+
const resolutionDue = new Date(now.getTime() + resolutionMinutes * 60000)
|
|
302
|
+
|
|
303
|
+
await payload.update({
|
|
304
|
+
collection: slugs.tickets as any,
|
|
305
|
+
id: doc.id,
|
|
306
|
+
data: {
|
|
307
|
+
slaPolicy: policy.id,
|
|
308
|
+
slaFirstResponseDue: firstResponseDue.toISOString(),
|
|
309
|
+
slaResolutionDue: resolutionDue.toISOString(),
|
|
310
|
+
},
|
|
311
|
+
overrideAccess: true,
|
|
312
|
+
})
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error('[support] Failed to auto-calculate SLA:', err)
|
|
315
|
+
}
|
|
316
|
+
return doc
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function createFireTicketWebhooks(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
321
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
322
|
+
const { payload } = req
|
|
323
|
+
|
|
324
|
+
if (operation === 'create') {
|
|
325
|
+
// ticket_created
|
|
326
|
+
fireWebhooks(payload, slugs, 'ticket_created', {
|
|
327
|
+
id: doc.id,
|
|
328
|
+
ticketNumber: doc.ticketNumber,
|
|
329
|
+
subject: doc.subject,
|
|
330
|
+
status: doc.status,
|
|
331
|
+
priority: doc.priority,
|
|
332
|
+
category: doc.category,
|
|
333
|
+
})
|
|
334
|
+
return doc
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (operation === 'update' && previousDoc) {
|
|
338
|
+
// ticket_resolved
|
|
339
|
+
if (previousDoc.status !== doc.status && doc.status === 'resolved') {
|
|
340
|
+
fireWebhooks(payload, slugs, 'ticket_resolved', {
|
|
341
|
+
id: doc.id,
|
|
342
|
+
ticketNumber: doc.ticketNumber,
|
|
343
|
+
subject: doc.subject,
|
|
344
|
+
previousStatus: previousDoc.status,
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ticket_assigned
|
|
349
|
+
const oldAssigned = typeof previousDoc.assignedTo === 'object' ? previousDoc.assignedTo?.id : previousDoc.assignedTo
|
|
350
|
+
const newAssigned = typeof doc.assignedTo === 'object' ? doc.assignedTo?.id : doc.assignedTo
|
|
351
|
+
if (newAssigned && oldAssigned !== newAssigned) {
|
|
352
|
+
fireWebhooks(payload, slugs, 'ticket_assigned', {
|
|
353
|
+
id: doc.id,
|
|
354
|
+
ticketNumber: doc.ticketNumber,
|
|
355
|
+
subject: doc.subject,
|
|
356
|
+
assignedTo: newAssigned,
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return doc
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function createNotifyAdminOnNewTicket(slugs: CollectionSlugs, notificationSlug: string): CollectionAfterChangeHook {
|
|
366
|
+
return async ({ doc, operation, req }) => {
|
|
367
|
+
if (operation !== 'create') return doc
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const ticketNumber = doc.ticketNumber || 'TK-????'
|
|
371
|
+
const subject = doc.subject || 'Support'
|
|
372
|
+
|
|
373
|
+
await createAdminNotification(req.payload, {
|
|
374
|
+
title: `Nouveau ticket : ${ticketNumber}`,
|
|
375
|
+
message: `${ticketNumber} — ${subject}`,
|
|
376
|
+
type: 'new_ticket',
|
|
377
|
+
link: `/admin/collections/${slugs.tickets}/${doc.id}`,
|
|
378
|
+
}, notificationSlug)
|
|
379
|
+
} catch (err) {
|
|
380
|
+
console.error('[support] Failed to create admin notification on new ticket:', err)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return doc
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function createDispatchWebhookOnTicket(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
388
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
389
|
+
if (operation === 'create') {
|
|
390
|
+
dispatchWebhook(
|
|
391
|
+
{ ticketId: doc.id, ticketNumber: doc.ticketNumber, subject: doc.subject },
|
|
392
|
+
'ticket_created',
|
|
393
|
+
req.payload,
|
|
394
|
+
slugs,
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (operation === 'update' && previousDoc?.status !== doc.status && doc.status === 'resolved') {
|
|
399
|
+
dispatchWebhook(
|
|
400
|
+
{ ticketId: doc.id, ticketNumber: doc.ticketNumber, subject: doc.subject },
|
|
401
|
+
'ticket_resolved',
|
|
402
|
+
req.payload,
|
|
403
|
+
slugs,
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return doc
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function createCascadeDelete(slugs: CollectionSlugs): CollectionBeforeDeleteHook {
|
|
412
|
+
return async ({ id, req }) => {
|
|
413
|
+
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys]
|
|
414
|
+
for (const slug of collections) {
|
|
415
|
+
await req.payload.delete({
|
|
416
|
+
collection: slug as any,
|
|
417
|
+
where: { ticket: { equals: id } },
|
|
418
|
+
overrideAccess: true,
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ─── Collection factory ──────────────────────────────────
|
|
425
|
+
|
|
426
|
+
export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
427
|
+
conversationComponent?: string
|
|
428
|
+
projectCollectionSlug?: string
|
|
429
|
+
documentsCollectionSlug?: string
|
|
430
|
+
notificationSlug?: string
|
|
431
|
+
}): CollectionConfig {
|
|
432
|
+
const notificationSlug = options?.notificationSlug || 'admin-notifications'
|
|
433
|
+
|
|
434
|
+
// Build dynamic fields
|
|
435
|
+
const dynamicFields: Field[] = []
|
|
436
|
+
|
|
437
|
+
// Conversation UI field at the top
|
|
438
|
+
dynamicFields.push({
|
|
439
|
+
name: 'conversation',
|
|
440
|
+
type: 'ui',
|
|
441
|
+
admin: {
|
|
442
|
+
components: {
|
|
443
|
+
Field: options?.conversationComponent || '@consilioweb/payload-support/src/components/TicketConversation',
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
// Project relationship (only if configured)
|
|
449
|
+
if (options?.projectCollectionSlug) {
|
|
450
|
+
dynamicFields.push({
|
|
451
|
+
name: 'project',
|
|
452
|
+
type: 'relationship',
|
|
453
|
+
relationTo: options.projectCollectionSlug,
|
|
454
|
+
label: 'Projet',
|
|
455
|
+
admin: { position: 'sidebar' },
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Billing collapsible fields
|
|
460
|
+
const billingFields: Field[] = [
|
|
461
|
+
{
|
|
462
|
+
type: 'row',
|
|
463
|
+
fields: [
|
|
464
|
+
...(options?.documentsCollectionSlug ? [
|
|
465
|
+
{
|
|
466
|
+
name: 'quote',
|
|
467
|
+
type: 'upload' as const,
|
|
468
|
+
relationTo: options.documentsCollectionSlug,
|
|
469
|
+
label: 'Devis',
|
|
470
|
+
admin: { width: '50%' },
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: 'invoice',
|
|
474
|
+
type: 'upload' as const,
|
|
475
|
+
relationTo: options.documentsCollectionSlug,
|
|
476
|
+
label: 'Facture',
|
|
477
|
+
admin: { width: '50%' },
|
|
478
|
+
},
|
|
479
|
+
] : []),
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
type: 'row',
|
|
484
|
+
fields: [
|
|
485
|
+
{
|
|
486
|
+
name: 'paymentStatus',
|
|
487
|
+
type: 'select',
|
|
488
|
+
label: 'Statut de paiement',
|
|
489
|
+
defaultValue: 'unpaid',
|
|
490
|
+
options: [
|
|
491
|
+
{ label: 'Non payé', value: 'unpaid' },
|
|
492
|
+
{ label: 'Paiement partiel', value: 'partial' },
|
|
493
|
+
{ label: 'Payé', value: 'paid' },
|
|
494
|
+
],
|
|
495
|
+
access: { update: ({ req }) => req.user?.collection === 'users' },
|
|
496
|
+
admin: { width: '50%', condition: (data) => !!data?.invoice },
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: 'paidAt',
|
|
500
|
+
type: 'date',
|
|
501
|
+
label: 'Paye le',
|
|
502
|
+
admin: { width: '33%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } },
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: 'billedAmount',
|
|
506
|
+
type: 'number',
|
|
507
|
+
label: 'Montant facture (EUR)',
|
|
508
|
+
admin: { width: '33%' },
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
]
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
slug: slugs.tickets,
|
|
516
|
+
labels: { singular: 'Ticket', plural: 'Tickets' },
|
|
517
|
+
admin: {
|
|
518
|
+
useAsTitle: 'subject',
|
|
519
|
+
group: 'Support',
|
|
520
|
+
defaultColumns: ['ticketNumber', 'subject', 'status', 'priority', 'category', 'client', 'updatedAt'],
|
|
521
|
+
listSearchableFields: ['ticketNumber', 'subject'],
|
|
522
|
+
},
|
|
523
|
+
fields: [
|
|
524
|
+
// Conversation UI field first
|
|
525
|
+
...dynamicFields,
|
|
526
|
+
{ name: 'subject', type: 'text', required: true, label: 'Sujet' },
|
|
527
|
+
{
|
|
528
|
+
type: 'row',
|
|
529
|
+
fields: [
|
|
530
|
+
{
|
|
531
|
+
name: 'status', type: 'select', defaultValue: 'open', label: 'Statut',
|
|
532
|
+
options: [
|
|
533
|
+
{ label: 'Ouvert', value: 'open' },
|
|
534
|
+
{ label: 'En attente client', value: 'waiting_client' },
|
|
535
|
+
{ label: 'Resolu', value: 'resolved' },
|
|
536
|
+
],
|
|
537
|
+
admin: { width: '50%' },
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: 'priority', type: 'select', defaultValue: 'normal', label: 'Priorite',
|
|
541
|
+
options: [
|
|
542
|
+
{ label: 'Basse', value: 'low' },
|
|
543
|
+
{ label: 'Normale', value: 'normal' },
|
|
544
|
+
{ label: 'Haute', value: 'high' },
|
|
545
|
+
{ label: 'Urgente', value: 'urgent' },
|
|
546
|
+
],
|
|
547
|
+
admin: { width: '50%' },
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
type: 'row',
|
|
553
|
+
fields: [
|
|
554
|
+
{ name: 'client', type: 'relationship', relationTo: slugs.supportClients, required: true, label: 'Client', admin: { width: '50%' } },
|
|
555
|
+
],
|
|
556
|
+
},
|
|
557
|
+
// Billing collapsible
|
|
558
|
+
{
|
|
559
|
+
type: 'collapsible', label: 'Facturation',
|
|
560
|
+
admin: { initCollapsed: true },
|
|
561
|
+
fields: billingFields,
|
|
562
|
+
},
|
|
563
|
+
// SLA & Delais
|
|
564
|
+
{
|
|
565
|
+
type: 'collapsible', label: 'SLA & Delais',
|
|
566
|
+
admin: { initCollapsed: true },
|
|
567
|
+
fields: [
|
|
568
|
+
{
|
|
569
|
+
type: 'row',
|
|
570
|
+
fields: [
|
|
571
|
+
{ name: 'firstResponseAt', type: 'date', label: 'Premiere reponse', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
572
|
+
{ name: 'resolvedAt', type: 'date', label: 'Date de resolution', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
573
|
+
],
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
type: 'row',
|
|
577
|
+
fields: [
|
|
578
|
+
{ name: 'slaFirstResponseDue', type: 'date', label: 'Echeance 1ere reponse (SLA)', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
579
|
+
{ name: 'slaResolutionDue', type: 'date', label: 'Echeance resolution (SLA)', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
580
|
+
],
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
type: 'row',
|
|
584
|
+
fields: [
|
|
585
|
+
{ name: 'slaFirstResponseBreached', type: 'checkbox', label: '1ere reponse depassee', admin: { readOnly: true, width: '50%' } },
|
|
586
|
+
{ name: 'slaResolutionBreached', type: 'checkbox', label: 'Resolution depassee', admin: { readOnly: true, width: '50%' } },
|
|
587
|
+
],
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
},
|
|
591
|
+
// Systeme
|
|
592
|
+
{
|
|
593
|
+
type: 'collapsible', label: 'Systeme',
|
|
594
|
+
admin: { initCollapsed: true },
|
|
595
|
+
fields: [
|
|
596
|
+
{ name: 'chatSession', type: 'text', label: 'Session Chat', admin: { readOnly: true } },
|
|
597
|
+
{
|
|
598
|
+
type: 'row',
|
|
599
|
+
fields: [
|
|
600
|
+
{ name: 'lastClientReadAt', type: 'date', label: 'Derniere lecture client', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
601
|
+
{ name: 'lastAdminReadAt', type: 'date', label: 'Derniere lecture admin', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
602
|
+
{ name: 'lastClientMessageAt', type: 'date', label: 'Dernier message client', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
603
|
+
],
|
|
604
|
+
},
|
|
605
|
+
{ name: 'mergedInto', type: 'relationship', relationTo: slugs.tickets, label: 'Fusionne dans', admin: { readOnly: true } },
|
|
606
|
+
{ name: 'autoCloseRemindedAt', type: 'date', label: 'Rappel auto-close envoye', admin: { readOnly: true, date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
607
|
+
],
|
|
608
|
+
},
|
|
609
|
+
// Sidebar
|
|
610
|
+
{ name: 'ticketNumber', type: 'text', unique: true, label: 'N° Ticket', admin: { readOnly: true, position: 'sidebar' } },
|
|
611
|
+
{ name: 'slaPolicy', type: 'relationship', relationTo: slugs.slaPolicies, label: 'Politique SLA', admin: { position: 'sidebar' } },
|
|
612
|
+
{
|
|
613
|
+
name: 'category', type: 'select', label: 'Categorie',
|
|
614
|
+
options: [
|
|
615
|
+
{ label: 'Bug / Dysfonctionnement', value: 'bug' },
|
|
616
|
+
{ label: 'Modification de contenu', value: 'content' },
|
|
617
|
+
{ label: 'Nouvelle fonctionnalite', value: 'feature' },
|
|
618
|
+
{ label: 'Question / Aide', value: 'question' },
|
|
619
|
+
{ label: 'Hebergement / Domaine', value: 'hosting' },
|
|
620
|
+
],
|
|
621
|
+
admin: { position: 'sidebar' },
|
|
622
|
+
},
|
|
623
|
+
{ name: 'assignedTo', type: 'relationship', relationTo: slugs.users, label: 'Assigne a', admin: { position: 'sidebar' } },
|
|
624
|
+
{
|
|
625
|
+
name: 'source', type: 'select', defaultValue: 'portal', label: 'Source',
|
|
626
|
+
options: [
|
|
627
|
+
{ label: 'Portail client', value: 'portal' },
|
|
628
|
+
{ label: 'Chat en direct', value: 'live-chat' },
|
|
629
|
+
{ label: 'Email', value: 'email' },
|
|
630
|
+
{ label: 'Admin', value: 'admin' },
|
|
631
|
+
],
|
|
632
|
+
admin: { position: 'sidebar' },
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: 'tags', type: 'select', hasMany: true, label: 'Tags',
|
|
636
|
+
options: [
|
|
637
|
+
{ label: 'Urgent client', value: 'urgent-client' },
|
|
638
|
+
{ label: 'Facturable', value: 'facturable' },
|
|
639
|
+
{ label: 'Bug critique', value: 'bug-critique' },
|
|
640
|
+
{ label: 'En attente info', value: 'attente-info' },
|
|
641
|
+
{ label: 'Evolution', value: 'evolution' },
|
|
642
|
+
{ label: 'Maintenance', value: 'maintenance' },
|
|
643
|
+
{ label: 'Design', value: 'design' },
|
|
644
|
+
{ label: 'SEO', value: 'seo' },
|
|
645
|
+
{ label: 'SMS', value: 'sms' },
|
|
646
|
+
{ label: 'WhatsApp', value: 'whatsapp' },
|
|
647
|
+
],
|
|
648
|
+
admin: { position: 'sidebar' },
|
|
649
|
+
},
|
|
650
|
+
{ name: 'relatedTickets', type: 'relationship', relationTo: slugs.tickets, hasMany: true, label: 'Tickets lies', admin: { position: 'sidebar' } },
|
|
651
|
+
{ name: 'snoozeUntil', type: 'date', label: 'Snooze jusqu\'au', admin: { position: 'sidebar', date: { pickerAppearance: 'dayAndTime', displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
652
|
+
// Billing sidebar
|
|
653
|
+
{ name: 'billable', type: 'checkbox', defaultValue: true, label: 'Facturable', admin: { position: 'sidebar' } },
|
|
654
|
+
{ name: 'showTimeToClient', type: 'checkbox', defaultValue: true, label: 'Afficher le temps au client', admin: { position: 'sidebar' } },
|
|
655
|
+
{ name: 'totalTimeMinutes', type: 'number', defaultValue: 0, label: 'Temps total (minutes)', admin: { readOnly: true, position: 'sidebar' } },
|
|
656
|
+
],
|
|
657
|
+
hooks: {
|
|
658
|
+
beforeChange: [
|
|
659
|
+
createAssignTicketNumber(slugs),
|
|
660
|
+
createAssignClientOnCreate(slugs),
|
|
661
|
+
createAutoAssignAdmin(slugs),
|
|
662
|
+
autoPaidAt,
|
|
663
|
+
createRestrictClientUpdates(slugs),
|
|
664
|
+
],
|
|
665
|
+
afterChange: [
|
|
666
|
+
createTrackSLA(slugs),
|
|
667
|
+
createAutoCalculateSLA(slugs),
|
|
668
|
+
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
669
|
+
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
670
|
+
createLogTicketActivity(slugs),
|
|
671
|
+
createNotifyOnAssignment(slugs),
|
|
672
|
+
createNotifyClientOnResolve(slugs),
|
|
673
|
+
createTicketStatusEmail(slugs),
|
|
674
|
+
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
675
|
+
createFireTicketWebhooks(slugs),
|
|
676
|
+
createDispatchWebhookOnTicket(slugs),
|
|
677
|
+
],
|
|
678
|
+
beforeDelete: [createCascadeDelete(slugs)],
|
|
679
|
+
},
|
|
680
|
+
access: {
|
|
681
|
+
create: ({ req }) => {
|
|
682
|
+
if (req.user?.collection === slugs.users) return true
|
|
683
|
+
if (req.user?.collection === slugs.supportClients) return true
|
|
684
|
+
return false
|
|
685
|
+
},
|
|
686
|
+
read: ({ req }) => {
|
|
687
|
+
if (req.user?.collection === slugs.users) return true
|
|
688
|
+
if (req.user?.collection === slugs.supportClients) {
|
|
689
|
+
return { client: { equals: req.user.id } }
|
|
690
|
+
}
|
|
691
|
+
return false
|
|
692
|
+
},
|
|
693
|
+
update: ({ req }) => {
|
|
694
|
+
if (req.user?.collection === slugs.users) return true
|
|
695
|
+
if (req.user?.collection === slugs.supportClients) {
|
|
696
|
+
return { client: { equals: req.user.id } }
|
|
697
|
+
}
|
|
698
|
+
return false
|
|
699
|
+
},
|
|
700
|
+
delete: ({ req }) => req.user?.collection === slugs.users,
|
|
701
|
+
},
|
|
702
|
+
timestamps: true,
|
|
703
|
+
}
|
|
704
|
+
}
|