@consilioweb/payload-support 0.16.0 → 1.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 +347 -435
- package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
- package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
- package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
- package/dist/components/TicketConversation/SkeletonText.js +20 -0
- package/dist/components/TicketConversation/constants.cjs +27 -0
- package/dist/components/TicketConversation/constants.js +26 -1
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
- package/dist/components/TicketConversation/index.cjs +10 -147
- package/dist/components/TicketConversation/index.js +4 -141
- package/dist/index.cjs +1604 -671
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1602 -671
- package/dist/views/BillingView/client.cjs +40 -54
- package/dist/views/BillingView/client.js +40 -54
- package/dist/views/ChatView/client.cjs +4 -3
- package/dist/views/ChatView/client.js +4 -3
- package/dist/views/CrmView/client.cjs +7 -7
- package/dist/views/CrmView/client.js +7 -7
- package/dist/views/EmailTrackingView/client.cjs +6 -5
- package/dist/views/EmailTrackingView/client.js +6 -5
- package/dist/views/ImportConversationView/client.cjs +4 -4
- package/dist/views/ImportConversationView/client.js +4 -4
- package/dist/views/LogsView/client.cjs +3 -2
- package/dist/views/LogsView/client.js +3 -2
- package/dist/views/NewTicketView/client.cjs +1 -1
- package/dist/views/NewTicketView/client.js +1 -1
- package/dist/views/PendingEmailsView/client.cjs +7 -6
- package/dist/views/PendingEmailsView/client.js +7 -6
- package/dist/views/SupportDashboardView/client.cjs +19 -22
- package/dist/views/SupportDashboardView/client.js +19 -22
- package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
- package/dist/views/TicketDetailView/NextActionItem.js +34 -0
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
- package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
- package/dist/views/TicketDetailView/client.cjs +59 -190
- package/dist/views/TicketDetailView/client.js +52 -183
- package/dist/views/TicketDetailView/constants.cjs +20 -0
- package/dist/views/TicketDetailView/constants.js +16 -0
- package/dist/views/TicketDetailView/helpers.cjs +23 -0
- package/dist/views/TicketDetailView/helpers.js +20 -0
- package/dist/views/TicketDetailView/types.cjs +2 -0
- package/dist/views/TicketDetailView/types.js +1 -0
- package/dist/views/TicketInboxView/client.cjs +14 -10
- package/dist/views/TicketInboxView/client.js +14 -10
- package/dist/views/TicketingSettingsView/client.cjs +101 -96
- package/dist/views/TicketingSettingsView/client.js +92 -88
- package/dist/views/TimeDashboardView/client.cjs +2 -2
- package/dist/views/TimeDashboardView/client.js +2 -2
- package/dist/views/shared/StatusPill.cjs +23 -12
- package/dist/views/shared/StatusPill.js +23 -12
- package/dist/views/shared/dateLocale.cjs +5 -0
- package/dist/views/shared/dateLocale.js +3 -0
- package/package.json +23 -15
- package/src/__tests__/generateTrackingToken.test.ts +53 -0
- package/src/__tests__/integration/ai-agent.test.ts +46 -0
- package/src/__tests__/integration/auth.test.ts +43 -0
- package/src/__tests__/integration/automation-rules.test.ts +68 -0
- package/src/__tests__/integration/buildTestPayload.ts +52 -0
- package/src/__tests__/integration/channels.test.ts +52 -0
- package/src/__tests__/integration/core-behaviors.test.ts +58 -0
- package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
- package/src/__tests__/integration/digest.test.ts +48 -0
- package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
- package/src/__tests__/integration/isolation.test.ts +81 -0
- package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
- package/src/__tests__/integration/nps.test.ts +37 -0
- package/src/__tests__/integration/sanitization.test.ts +35 -0
- package/src/__tests__/integration/sla-pause.test.ts +39 -0
- package/src/__tests__/integration/smoke.test.ts +26 -0
- package/src/__tests__/integration/snooze.test.ts +68 -0
- package/src/__tests__/integration/teams.test.ts +38 -0
- package/src/__tests__/rateLimiter.test.ts +89 -0
- package/src/__tests__/sanitizeHtml.test.ts +165 -0
- package/src/__tests__/sla.test.ts +258 -0
- package/src/collections/AutomationRules.ts +66 -0
- package/src/collections/NotificationQueue.ts +30 -0
- package/src/collections/SatisfactionSurveys.ts +11 -2
- package/src/collections/SupportClients.ts +58 -1
- package/src/collections/SupportTeam.ts +27 -0
- package/src/collections/TicketActivityLog.ts +9 -4
- package/src/collections/TicketMessages.ts +129 -31
- package/src/collections/Tickets.ts +31 -73
- package/src/collections/index.ts +3 -0
- package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
- package/src/components/TicketConversation/SkeletonText.tsx +23 -0
- package/src/components/TicketConversation/constants.ts +29 -0
- package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
- package/src/components/TicketConversation/index.tsx +4 -152
- package/src/endpoints/admin-chat-stream.ts +3 -4
- package/src/endpoints/admin-chat.ts +11 -20
- package/src/endpoints/admin-stats.ts +70 -32
- package/src/endpoints/ai-agent.ts +36 -0
- package/src/endpoints/apply-macro.ts +8 -14
- package/src/endpoints/auth-2fa.ts +12 -12
- package/src/endpoints/auto-close.ts +6 -10
- package/src/endpoints/billing.ts +17 -4
- package/src/endpoints/bulk-action.ts +7 -12
- package/src/endpoints/channels.ts +89 -0
- package/src/endpoints/chat-stream.ts +3 -4
- package/src/endpoints/chat.ts +7 -12
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/client-intelligence.ts +4 -4
- package/src/endpoints/delete-account.ts +9 -16
- package/src/endpoints/email-stats.ts +2 -2
- package/src/endpoints/escalate.ts +2 -2
- package/src/endpoints/export-csv.ts +2 -2
- package/src/endpoints/export-data.ts +5 -8
- package/src/endpoints/import-conversation.ts +6 -10
- package/src/endpoints/index.ts +18 -1
- package/src/endpoints/invite-collaborator.ts +30 -15
- package/src/endpoints/invoice.ts +127 -0
- package/src/endpoints/kb-search.ts +2 -2
- package/src/endpoints/login.ts +12 -4
- package/src/endpoints/merge-clients.ts +14 -19
- package/src/endpoints/merge-tickets.ts +11 -18
- package/src/endpoints/oauth-google.ts +86 -39
- package/src/endpoints/pending-emails-process.ts +15 -25
- package/src/endpoints/process-digests.ts +93 -0
- package/src/endpoints/process-scheduled.ts +6 -10
- package/src/endpoints/process-snooze.ts +78 -0
- package/src/endpoints/purge-logs.ts +2 -2
- package/src/endpoints/resend-notification.ts +3 -4
- package/src/endpoints/round-robin-config.ts +3 -4
- package/src/endpoints/satisfaction.ts +21 -14
- package/src/endpoints/search.ts +5 -8
- package/src/endpoints/seed-kb.ts +3 -4
- package/src/endpoints/send-reminder.ts +6 -9
- package/src/endpoints/settings.ts +5 -2
- package/src/endpoints/signature.ts +3 -4
- package/src/endpoints/sla-check.ts +2 -2
- package/src/endpoints/split-ticket.ts +7 -12
- package/src/endpoints/statuses.ts +2 -2
- package/src/endpoints/ticket-feedback.ts +4 -6
- package/src/endpoints/ticket-synthesis.ts +2 -2
- package/src/endpoints/track-open.ts +16 -28
- package/src/endpoints/transfer-ticket.ts +21 -9
- package/src/endpoints/user-prefs.ts +3 -4
- package/src/hooks/automationRules.ts +84 -0
- package/src/hooks/checkSLA.ts +48 -16
- package/src/plugin.ts +6 -0
- package/src/portal/LiveChat.tsx +2 -63
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
- package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
- package/src/portal/auth/tickets/detail/page.tsx +2 -2
- package/src/portal/helpers.ts +46 -0
- package/src/portal/login/page.tsx +3 -8
- package/src/portal/types.ts +15 -0
- package/src/types/lucide-react.d.ts +34 -0
- package/src/types/payload-config.d.ts +10 -0
- package/src/utils/adminNotification.ts +2 -2
- package/src/utils/aiAgent.ts +147 -0
- package/src/utils/auth.ts +5 -4
- package/src/utils/channels.ts +36 -0
- package/src/utils/db.ts +73 -0
- package/src/utils/emailTemplate.ts +8 -1
- package/src/utils/fireWebhooks.ts +4 -6
- package/src/utils/generateTicketSynthesis.ts +7 -12
- package/src/utils/invoicePdf.ts +88 -0
- package/src/utils/notificationQueue.ts +36 -0
- package/src/utils/readSettings.ts +22 -6
- package/src/utils/sanitizeHtml.ts +57 -0
- package/src/utils/slugs.ts +6 -0
- package/src/utils/teamAccess.ts +27 -0
- package/src/utils/ticketAccess.ts +53 -0
- package/src/utils/webhookDispatcher.ts +4 -6
- package/src/views/BillingView/client.tsx +49 -37
- package/src/views/ChatView/client.tsx +4 -3
- package/src/views/CrmView/client.tsx +7 -7
- package/src/views/EmailTrackingView/client.tsx +7 -6
- package/src/views/ImportConversationView/client.tsx +4 -4
- package/src/views/LogsView/client.tsx +3 -2
- package/src/views/NewTicketView/client.tsx +1 -1
- package/src/views/PendingEmailsView/client.tsx +7 -6
- package/src/views/SupportDashboardView/client.tsx +21 -21
- package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
- package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
- package/src/views/TicketDetailView/client.tsx +54 -179
- package/src/views/TicketDetailView/constants.ts +23 -0
- package/src/views/TicketDetailView/helpers.ts +17 -0
- package/src/views/TicketDetailView/types.ts +10 -0
- package/src/views/TicketInboxView/client.tsx +16 -10
- package/src/views/TicketingSettingsView/client.tsx +90 -87
- package/src/views/TimeDashboardView/client.tsx +2 -2
- package/src/views/shared/StatusPill.tsx +24 -12
- package/src/views/shared/dateLocale.ts +5 -0
- package/src/views/shared/locales/en.json +708 -0
- package/src/views/shared/locales/fr.json +708 -0
|
@@ -2,6 +2,7 @@ import type { Endpoint } from 'payload'
|
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { RateLimiter } from '../utils/rateLimiter'
|
|
4
4
|
import { readSupportSettings } from '../utils/readSettings'
|
|
5
|
+
import { dbFind, dbCreate } from '../utils/db'
|
|
5
6
|
import crypto from 'crypto'
|
|
6
7
|
|
|
7
8
|
interface ParsedConversation {
|
|
@@ -215,8 +216,7 @@ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
// Find or create support client
|
|
218
|
-
const clientResult = await payload.
|
|
219
|
-
collection: slugs.supportClients as any,
|
|
219
|
+
const clientResult = await dbFind(payload, slugs.supportClients, {
|
|
220
220
|
where: { email: { equals: conversation.client.email } },
|
|
221
221
|
limit: 1,
|
|
222
222
|
depth: 0,
|
|
@@ -230,8 +230,7 @@ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
230
230
|
const nameParts = conversation.client.name.split(' ')
|
|
231
231
|
const randomPassword = crypto.randomBytes(48).toString('base64url')
|
|
232
232
|
|
|
233
|
-
client = await payload.
|
|
234
|
-
collection: slugs.supportClients as any,
|
|
233
|
+
client = await dbCreate(payload, slugs.supportClients, {
|
|
235
234
|
data: {
|
|
236
235
|
email: conversation.client.email,
|
|
237
236
|
password: randomPassword,
|
|
@@ -245,8 +244,7 @@ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
245
244
|
}
|
|
246
245
|
|
|
247
246
|
// Find admin user for authorAdmin
|
|
248
|
-
const adminUsers = await payload.
|
|
249
|
-
collection: slugs.users as any,
|
|
247
|
+
const adminUsers = await dbFind(payload, slugs.users, {
|
|
250
248
|
limit: 1,
|
|
251
249
|
depth: 0,
|
|
252
250
|
overrideAccess: true,
|
|
@@ -254,8 +252,7 @@ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
254
252
|
const adminUserId = adminUsers.docs[0]?.id
|
|
255
253
|
|
|
256
254
|
// Create ticket
|
|
257
|
-
const ticket = await payload.
|
|
258
|
-
collection: slugs.tickets as any,
|
|
255
|
+
const ticket = await dbCreate(payload, slugs.tickets, {
|
|
259
256
|
data: {
|
|
260
257
|
subject: conversation.subject,
|
|
261
258
|
client: client.id,
|
|
@@ -271,8 +268,7 @@ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
271
268
|
for (const msg of conversation.messages) {
|
|
272
269
|
const isAdmin = msg.from === 'admin'
|
|
273
270
|
|
|
274
|
-
await payload.
|
|
275
|
-
collection: slugs.ticketMessages as any,
|
|
271
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
276
272
|
data: {
|
|
277
273
|
ticket: ticket.id,
|
|
278
274
|
body: msg.content,
|
package/src/endpoints/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { CollectionSlugs } from '../utils/slugs'
|
|
|
3
3
|
import type { SupportFeatures } from '../types'
|
|
4
4
|
|
|
5
5
|
import { createAiEndpoint } from './ai'
|
|
6
|
+
import { createAiAgentEndpoint } from './ai-agent'
|
|
6
7
|
import { createClientIntelligenceEndpoint } from './client-intelligence'
|
|
7
8
|
import { createSearchEndpoint } from './search'
|
|
8
9
|
import { createKbSearchEndpoint } from './kb-search'
|
|
@@ -27,6 +28,7 @@ import { createAdminChatGetEndpoint, createAdminChatPostEndpoint } from './admin
|
|
|
27
28
|
import { createAdminChatStreamEndpoint } from './admin-chat-stream'
|
|
28
29
|
import { createAdminStatsEndpoint } from './admin-stats'
|
|
29
30
|
import { createBillingEndpoint } from './billing'
|
|
31
|
+
import { createInvoiceEndpoint } from './invoice'
|
|
30
32
|
import { createTicketSynthesisEndpoint } from './ticket-synthesis'
|
|
31
33
|
import { createEmailStatsEndpoint } from './email-stats'
|
|
32
34
|
import { createSatisfactionEndpoint } from './satisfaction'
|
|
@@ -43,6 +45,9 @@ import { createDeleteAccountEndpoint } from './delete-account'
|
|
|
43
45
|
import { createMergeClientsEndpoint } from './merge-clients'
|
|
44
46
|
import { createImportConversationEndpoint } from './import-conversation'
|
|
45
47
|
import { createProcessScheduledEndpoint } from './process-scheduled'
|
|
48
|
+
import { createProcessSnoozeEndpoint } from './process-snooze'
|
|
49
|
+
import { createProcessDigestsEndpoint } from './process-digests'
|
|
50
|
+
import { createChannelsWebhookEndpoint } from './channels'
|
|
46
51
|
import { createUserPrefsGetEndpoint, createUserPrefsPostEndpoint } from './user-prefs'
|
|
47
52
|
import { createEscalateEndpoint } from './escalate'
|
|
48
53
|
import { createTicketFeedbackEndpoint } from './ticket-feedback'
|
|
@@ -51,6 +56,7 @@ import { createInviteCollaboratorEndpoint } from './invite-collaborator'
|
|
|
51
56
|
|
|
52
57
|
// Re-export all individual endpoint creators
|
|
53
58
|
export { createAiEndpoint } from './ai'
|
|
59
|
+
export { createAiAgentEndpoint } from './ai-agent'
|
|
54
60
|
export { createSearchEndpoint } from './search'
|
|
55
61
|
export { createKbSearchEndpoint } from './kb-search'
|
|
56
62
|
export { createBulkActionEndpoint } from './bulk-action'
|
|
@@ -74,6 +80,7 @@ export { createAdminChatGetEndpoint, createAdminChatPostEndpoint } from './admin
|
|
|
74
80
|
export { createAdminChatStreamEndpoint } from './admin-chat-stream'
|
|
75
81
|
export { createAdminStatsEndpoint } from './admin-stats'
|
|
76
82
|
export { createBillingEndpoint } from './billing'
|
|
83
|
+
export { createInvoiceEndpoint } from './invoice'
|
|
77
84
|
export { createTicketSynthesisEndpoint } from './ticket-synthesis'
|
|
78
85
|
export { createEmailStatsEndpoint } from './email-stats'
|
|
79
86
|
export { createSatisfactionEndpoint } from './satisfaction'
|
|
@@ -90,6 +97,9 @@ export { createDeleteAccountEndpoint } from './delete-account'
|
|
|
90
97
|
export { createMergeClientsEndpoint } from './merge-clients'
|
|
91
98
|
export { createImportConversationEndpoint } from './import-conversation'
|
|
92
99
|
export { createProcessScheduledEndpoint } from './process-scheduled'
|
|
100
|
+
export { createProcessSnoozeEndpoint } from './process-snooze'
|
|
101
|
+
export { createProcessDigestsEndpoint } from './process-digests'
|
|
102
|
+
export { createChannelsWebhookEndpoint } from './channels'
|
|
93
103
|
export { createUserPrefsGetEndpoint, createUserPrefsPostEndpoint } from './user-prefs'
|
|
94
104
|
export type { UserPrefs } from './user-prefs'
|
|
95
105
|
export { createEscalateEndpoint } from './escalate'
|
|
@@ -139,6 +149,7 @@ export function createSupportEndpoints(slugs: CollectionSlugs, options?: Support
|
|
|
139
149
|
// Conditional endpoints based on feature flags
|
|
140
150
|
if (!f || f.ai !== false) {
|
|
141
151
|
endpoints.push(createAiEndpoint(slugs))
|
|
152
|
+
endpoints.push(createAiAgentEndpoint(slugs))
|
|
142
153
|
endpoints.push(...createClientIntelligenceEndpoint(slugs))
|
|
143
154
|
endpoints.push(createTicketSynthesisEndpoint(slugs))
|
|
144
155
|
}
|
|
@@ -169,13 +180,19 @@ export function createSupportEndpoints(slugs: CollectionSlugs, options?: Support
|
|
|
169
180
|
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs))
|
|
170
181
|
endpoints.push(createAdminChatStreamEndpoint(slugs))
|
|
171
182
|
}
|
|
172
|
-
if (!f || f.timeTracking !== false)
|
|
183
|
+
if (!f || f.timeTracking !== false) {
|
|
184
|
+
endpoints.push(createBillingEndpoint(slugs))
|
|
185
|
+
endpoints.push(createInvoiceEndpoint(slugs))
|
|
186
|
+
}
|
|
173
187
|
if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs))
|
|
174
188
|
if (!f || f.emailTracking !== false) {
|
|
175
189
|
endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs))
|
|
176
190
|
}
|
|
177
191
|
if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs))
|
|
178
192
|
if (!f || f.scheduledReplies !== false) endpoints.push(createProcessScheduledEndpoint(slugs))
|
|
193
|
+
if (!f || f.snooze !== false) endpoints.push(createProcessSnoozeEndpoint(slugs))
|
|
194
|
+
endpoints.push(createProcessDigestsEndpoint(slugs))
|
|
195
|
+
endpoints.push(createChannelsWebhookEndpoint(slugs))
|
|
179
196
|
|
|
180
197
|
return endpoints
|
|
181
198
|
}
|
|
@@ -4,9 +4,16 @@ import { handleAuthError, AuthError } from '../utils/auth'
|
|
|
4
4
|
import { escapeHtml, emailWrapper, emailButton, emailParagraph } from '../utils/emailTemplate'
|
|
5
5
|
import { readSupportSettings } from '../utils/readSettings'
|
|
6
6
|
import { randomBytes } from 'crypto'
|
|
7
|
+
import { RateLimiter } from '../utils/rateLimiter'
|
|
8
|
+
import { dbFindByID, dbFind, dbCreate, dbUpdate, dbCount } from '../utils/db'
|
|
7
9
|
|
|
8
10
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
9
11
|
|
|
12
|
+
// Anti-abuse limits: an invite can create a placeholder account AND send an
|
|
13
|
+
// email, so cap both the volume per user and the collaborators per ticket.
|
|
14
|
+
const MAX_COLLABORATORS_PER_TICKET = 20
|
|
15
|
+
const inviteLimiter = new RateLimiter(60 * 60 * 1000, 10)
|
|
16
|
+
|
|
10
17
|
/**
|
|
11
18
|
* POST /api/support/tickets/:id/invite
|
|
12
19
|
*
|
|
@@ -52,12 +59,11 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
52
59
|
|
|
53
60
|
const payload = req.payload
|
|
54
61
|
|
|
55
|
-
const ticket = await payload.
|
|
56
|
-
collection: slugs.tickets as any,
|
|
62
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
57
63
|
id: ticketId,
|
|
58
64
|
depth: 1,
|
|
59
65
|
overrideAccess: true,
|
|
60
|
-
})
|
|
66
|
+
})
|
|
61
67
|
if (!ticket) return Response.json({ error: 'Ticket introuvable' }, { status: 404 })
|
|
62
68
|
|
|
63
69
|
const isAdmin = req.user.collection === slugs.users
|
|
@@ -68,6 +74,21 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
68
74
|
return Response.json({ error: 'Forbidden' }, { status: 403 })
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
// Anti-abuse: cap invite volume per user (in-memory, fail-closed).
|
|
78
|
+
if (inviteLimiter.check(String(req.user.id))) {
|
|
79
|
+
return Response.json({ error: 'Trop d\'invitations. Réessayez plus tard.' }, { status: 429 })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Cap collaborators per ticket to prevent mass placeholder-account creation.
|
|
83
|
+
const collabCount = await dbCount(payload, 'ticket-collaborators', { where: { ticket: { equals: ticketId } }, overrideAccess: true })
|
|
84
|
+
.catch(() => ({ totalDocs: 0 }))
|
|
85
|
+
if (collabCount.totalDocs >= MAX_COLLABORATORS_PER_TICKET) {
|
|
86
|
+
return Response.json(
|
|
87
|
+
{ error: `Limite de ${MAX_COLLABORATORS_PER_TICKET} collaborateurs atteinte sur ce ticket` },
|
|
88
|
+
{ status: 429 },
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
// Prevent inviting the primary owner (no-op)
|
|
72
93
|
const ownerEmail =
|
|
73
94
|
typeof ticket.client === 'object' ? (ticket.client?.email as string | undefined) : undefined
|
|
@@ -77,8 +98,7 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
77
98
|
|
|
78
99
|
// Find-or-create the invited support-client
|
|
79
100
|
let inviteeId: number | string | null = null
|
|
80
|
-
const existing = await payload.
|
|
81
|
-
collection: slugs.supportClients as any,
|
|
101
|
+
const existing = await dbFind(payload, slugs.supportClients, {
|
|
82
102
|
where: { email: { equals: email } },
|
|
83
103
|
limit: 1,
|
|
84
104
|
depth: 0,
|
|
@@ -90,8 +110,7 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
90
110
|
// Create a placeholder client; password is random — the invitee will reset
|
|
91
111
|
// via the forgotPassword flow triggered by the SupportClients afterChange hook.
|
|
92
112
|
const tempPassword = randomBytes(16).toString('hex')
|
|
93
|
-
const created = await payload.
|
|
94
|
-
collection: slugs.supportClients as any,
|
|
113
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
95
114
|
data: {
|
|
96
115
|
email,
|
|
97
116
|
firstName: 'Invité',
|
|
@@ -111,8 +130,7 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
111
130
|
}
|
|
112
131
|
|
|
113
132
|
// Upsert ticket-collaborator row (one per (ticket, client))
|
|
114
|
-
const dupe = await payload
|
|
115
|
-
collection: 'ticket-collaborators' as any,
|
|
133
|
+
const dupe = await dbFind(payload, 'ticket-collaborators', {
|
|
116
134
|
where: {
|
|
117
135
|
and: [
|
|
118
136
|
{ ticket: { equals: ticketId } },
|
|
@@ -127,15 +145,13 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
127
145
|
const invitationToken = randomBytes(24).toString('hex')
|
|
128
146
|
|
|
129
147
|
if (dupe.docs.length > 0) {
|
|
130
|
-
await payload
|
|
131
|
-
collection: 'ticket-collaborators' as any,
|
|
148
|
+
await dbUpdate(payload, 'ticket-collaborators', {
|
|
132
149
|
id: (dupe.docs[0] as any).id,
|
|
133
150
|
data: { role, invitationToken },
|
|
134
151
|
overrideAccess: true,
|
|
135
152
|
})
|
|
136
153
|
} else {
|
|
137
|
-
await payload
|
|
138
|
-
collection: 'ticket-collaborators' as any,
|
|
154
|
+
await dbCreate(payload, 'ticket-collaborators', {
|
|
139
155
|
data: {
|
|
140
156
|
ticket: ticketId,
|
|
141
157
|
client: inviteeId,
|
|
@@ -187,8 +203,7 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
|
|
|
187
203
|
|
|
188
204
|
// Best-effort email log
|
|
189
205
|
try {
|
|
190
|
-
await payload.
|
|
191
|
-
collection: slugs.emailLogs as any,
|
|
206
|
+
await dbCreate(payload, slugs.emailLogs, {
|
|
192
207
|
data: {
|
|
193
208
|
status: 'success',
|
|
194
209
|
action: 'invite',
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Endpoint, Where } from 'payload'
|
|
2
|
+
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFind } from '../utils/db'
|
|
5
|
+
import { escapeHtml } from '../utils/emailTemplate'
|
|
6
|
+
import { generateInvoicePdf, type InvoiceLine } from '../utils/invoicePdf'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /api/support/billing/invoice?from=YYYY-MM-DD&to=YYYY-MM-DD&projectId=...&format=html|pdf
|
|
10
|
+
*
|
|
11
|
+
* Admin-only. Renders an invoice for the billable tickets active in the period.
|
|
12
|
+
* `format=pdf` returns a real binary PDF (pdfkit); otherwise a self-contained,
|
|
13
|
+
* print-ready HTML document. Defaults to HTML.
|
|
14
|
+
*/
|
|
15
|
+
export function createInvoiceEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
16
|
+
return {
|
|
17
|
+
path: '/support/billing/invoice',
|
|
18
|
+
method: 'get',
|
|
19
|
+
handler: async (req) => {
|
|
20
|
+
try {
|
|
21
|
+
requireAdmin(req, slugs)
|
|
22
|
+
const payload = req.payload
|
|
23
|
+
const url = new URL(req.url!)
|
|
24
|
+
const from = url.searchParams.get('from')
|
|
25
|
+
const to = url.searchParams.get('to')
|
|
26
|
+
const projectId = url.searchParams.get('projectId')
|
|
27
|
+
const format = url.searchParams.get('format') === 'pdf' ? 'pdf' : 'html'
|
|
28
|
+
if (!from || !to) {
|
|
29
|
+
return Response.json({ error: 'Paramètres from et to requis.' }, { status: 400 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const toExclusive = new Date(to)
|
|
33
|
+
toExclusive.setDate(toExclusive.getDate() + 1)
|
|
34
|
+
const toIso = toExclusive.toISOString()
|
|
35
|
+
|
|
36
|
+
const where: Where = {
|
|
37
|
+
and: [
|
|
38
|
+
{ billable: { equals: true } },
|
|
39
|
+
...(projectId ? [{ project: { equals: Number(projectId) } } as Where] : []),
|
|
40
|
+
{
|
|
41
|
+
or: [
|
|
42
|
+
{ and: [{ updatedAt: { greater_than_equal: from } }, { updatedAt: { less_than: toIso } }] },
|
|
43
|
+
{ and: [{ createdAt: { greater_than_equal: from } }, { createdAt: { less_than: toIso } }] },
|
|
44
|
+
{ and: [{ resolvedAt: { greater_than_equal: from } }, { resolvedAt: { less_than: toIso } }] },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const tickets: Array<Record<string, unknown>> = []
|
|
51
|
+
let page = 1
|
|
52
|
+
let hasMore = true
|
|
53
|
+
while (hasMore && page <= 50) {
|
|
54
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
55
|
+
where, limit: 500, page, depth: 2, overrideAccess: true,
|
|
56
|
+
select: { ticketNumber: true, subject: true, billedAmount: true, totalTimeMinutes: true, project: true, client: true },
|
|
57
|
+
})
|
|
58
|
+
tickets.push(...(batch.docs as Array<Record<string, unknown>>))
|
|
59
|
+
hasMore = batch.hasNextPage ?? false
|
|
60
|
+
page++
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Shared structured lines (used by both HTML and PDF renderers).
|
|
64
|
+
let totalAmount = 0
|
|
65
|
+
let totalMinutes = 0
|
|
66
|
+
const lines: InvoiceLine[] = tickets.map((t) => {
|
|
67
|
+
const amount = typeof t.billedAmount === 'number' ? t.billedAmount : 0
|
|
68
|
+
const minutes = typeof t.totalTimeMinutes === 'number' ? t.totalTimeMinutes : 0
|
|
69
|
+
totalAmount += amount
|
|
70
|
+
totalMinutes += minutes
|
|
71
|
+
const client = typeof t.client === 'object' && t.client ? (t.client as { company?: string }).company || '' : ''
|
|
72
|
+
return { ticketNumber: String(t.ticketNumber || ''), subject: String(t.subject || ''), client, minutes, amount }
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
if (format === 'pdf') {
|
|
76
|
+
const pdf = await generateInvoicePdf({ from, to, projectId, lines, totalMinutes, totalAmount })
|
|
77
|
+
return new Response(new Uint8Array(pdf), {
|
|
78
|
+
status: 200,
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/pdf',
|
|
81
|
+
'Content-Disposition': `inline; filename="invoice-${from}_${to}.pdf"`,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const fmtAmount = (n: number) => `${n.toFixed(2)} €`
|
|
87
|
+
const fmtTime = (min: number) => `${Math.floor(min / 60)}h${String(min % 60).padStart(2, '0')}`
|
|
88
|
+
const rows = lines.map((l) => `<tr>
|
|
89
|
+
<td>${escapeHtml(l.ticketNumber)}</td>
|
|
90
|
+
<td>${escapeHtml(l.subject)}</td>
|
|
91
|
+
<td>${escapeHtml(l.client)}</td>
|
|
92
|
+
<td style="text-align:right;">${fmtTime(l.minutes)}</td>
|
|
93
|
+
<td style="text-align:right;">${fmtAmount(l.amount)}</td>
|
|
94
|
+
</tr>`).join('')
|
|
95
|
+
|
|
96
|
+
const html = `<!doctype html>
|
|
97
|
+
<html lang="fr"><head><meta charset="utf-8"><title>Facture ${escapeHtml(from)} → ${escapeHtml(to)}</title>
|
|
98
|
+
<style>
|
|
99
|
+
body { font-family: system-ui, -apple-system, sans-serif; color: #1e293b; max-width: 820px; margin: 32px auto; padding: 0 24px; }
|
|
100
|
+
h1 { font-size: 22px; margin: 0 0 4px; }
|
|
101
|
+
.period { color: #64748b; margin-bottom: 24px; }
|
|
102
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
103
|
+
th, td { padding: 8px 10px; border-bottom: 1px solid #e2e8f0; text-align: left; }
|
|
104
|
+
th { background: #f8fafc; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: #475569; }
|
|
105
|
+
tfoot td { font-weight: 700; border-top: 2px solid #0f172a; border-bottom: none; }
|
|
106
|
+
@media print { body { margin: 0; } }
|
|
107
|
+
</style></head>
|
|
108
|
+
<body>
|
|
109
|
+
<h1>Facture / Pré-facture</h1>
|
|
110
|
+
<div class="period">Période : ${escapeHtml(from)} → ${escapeHtml(to)}${projectId ? ` · Projet #${escapeHtml(projectId)}` : ''}</div>
|
|
111
|
+
<table>
|
|
112
|
+
<thead><tr><th>Ticket</th><th>Sujet</th><th>Client</th><th style="text-align:right;">Temps</th><th style="text-align:right;">Montant</th></tr></thead>
|
|
113
|
+
<tbody>${rows || '<tr><td colspan="5" style="color:#94a3b8;">Aucun ticket facturable sur la période.</td></tr>'}</tbody>
|
|
114
|
+
<tfoot><tr><td colspan="3">Total (${lines.length} ticket${lines.length > 1 ? 's' : ''})</td><td style="text-align:right;">${fmtTime(totalMinutes)}</td><td style="text-align:right;">${fmtAmount(totalAmount)}</td></tr></tfoot>
|
|
115
|
+
</table>
|
|
116
|
+
</body></html>`
|
|
117
|
+
|
|
118
|
+
return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } })
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const authResponse = handleAuthError(error)
|
|
121
|
+
if (authResponse) return authResponse
|
|
122
|
+
console.error('[invoice] Error:', error)
|
|
123
|
+
return Response.json({ error: 'Erreur interne' }, { status: 500 })
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
import { dbFind } from '../utils/db'
|
|
3
4
|
|
|
4
5
|
// Combining diacritics range used to strip accents after NFD normalization.
|
|
5
6
|
const DIACRITICS = /[̀-ͯ]/g
|
|
@@ -107,8 +108,7 @@ export function createKbSearchEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
107
108
|
// Always include the raw query for short single-word matches.
|
|
108
109
|
orClauses.push({ title: { contains: q } })
|
|
109
110
|
|
|
110
|
-
const res = await payload.
|
|
111
|
-
collection: slugs.knowledgeBase as any,
|
|
111
|
+
const res = await dbFind(payload, slugs.knowledgeBase, {
|
|
112
112
|
where: {
|
|
113
113
|
and: [
|
|
114
114
|
{ published: { equals: true } },
|
package/src/endpoints/login.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { RateLimiter } from '../utils/rateLimiter'
|
|
4
|
+
import { dbCreate } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
const loginLimiter = new RateLimiter(15 * 60_000, 10) // 10 per 15 min
|
|
6
7
|
|
|
@@ -13,6 +14,9 @@ export function createLoginEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
13
14
|
path: '/support/login',
|
|
14
15
|
method: 'post',
|
|
15
16
|
handler: async (req) => {
|
|
17
|
+
// NOTE: x-forwarded-for is spoofable unless this app sits strictly behind a
|
|
18
|
+
// trusted proxy that rewrites it. This IP rate-limit is a SECONDARY defense;
|
|
19
|
+
// the primary brute-force control is Payload's account lock (maxLoginAttempts).
|
|
16
20
|
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || req.headers.get('x-real-ip') || 'unknown'
|
|
17
21
|
|
|
18
22
|
if (loginLimiter.check(ip)) {
|
|
@@ -43,8 +47,7 @@ export function createLoginEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
43
47
|
})
|
|
44
48
|
|
|
45
49
|
// Log successful login (fire-and-forget)
|
|
46
|
-
payload.
|
|
47
|
-
collection: slugs.authLogs as any,
|
|
50
|
+
dbCreate(payload, slugs.authLogs, {
|
|
48
51
|
data: { email, success: true, action: 'login', ipAddress: ip, userAgent },
|
|
49
52
|
overrideAccess: true,
|
|
50
53
|
}).catch(() => {})
|
|
@@ -71,13 +74,18 @@ export function createLoginEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
71
74
|
} catch (err: unknown) {
|
|
72
75
|
const errorMessage = err instanceof Error ? err.message : 'Erreur inconnue'
|
|
73
76
|
|
|
77
|
+
// 2FA gate: password was correct but a fresh 2FA verification is required.
|
|
78
|
+
// No session/cookie is issued (Payload rolled it back on the throw).
|
|
79
|
+
if (errorMessage.includes('2FA_REQUIRED')) {
|
|
80
|
+
return Response.json({ requires2FA: true }, { status: 200 })
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
let errorReason = 'Identifiants incorrects'
|
|
75
84
|
if (errorMessage.includes('locked') || errorMessage.includes('verrouillé') || errorMessage.includes('Too many')) {
|
|
76
85
|
errorReason = 'Compte verrouillé (trop de tentatives)'
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
payload.
|
|
80
|
-
collection: slugs.authLogs as any,
|
|
88
|
+
dbCreate(payload, slugs.authLogs, {
|
|
81
89
|
data: { email, success: false, action: 'login', errorReason, ipAddress: ip, userAgent },
|
|
82
90
|
overrideAccess: true,
|
|
83
91
|
}).catch(() => {})
|
|
@@ -1,6 +1,7 @@
|
|
|
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 { dbFind, dbFindByID, dbUpdate, dbDelete } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* POST /api/support/merge-clients
|
|
@@ -23,8 +24,8 @@ export function createMergeClientsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const [source, target] = await Promise.all([
|
|
26
|
-
payload
|
|
27
|
-
payload
|
|
27
|
+
dbFindByID(payload, slugs.supportClients, { id: sourceId, depth: 0, overrideAccess: true }),
|
|
28
|
+
dbFindByID(payload, slugs.supportClients, { id: targetId, depth: 0, overrideAccess: true }),
|
|
28
29
|
]) as [any, any]
|
|
29
30
|
|
|
30
31
|
if (!source || !target) {
|
|
@@ -40,73 +41,67 @@ export function createMergeClientsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// 1. Transfer tickets
|
|
43
|
-
const tickets = await payload.
|
|
44
|
-
collection: slugs.tickets as any,
|
|
44
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
45
45
|
where: { client: { equals: sourceId } },
|
|
46
46
|
limit: 500,
|
|
47
47
|
depth: 0,
|
|
48
48
|
overrideAccess: true,
|
|
49
49
|
})
|
|
50
50
|
for (const ticket of tickets.docs) {
|
|
51
|
-
await payload
|
|
51
|
+
await dbUpdate(payload, slugs.tickets, { id: ticket.id, data: { client: targetId }, overrideAccess: true })
|
|
52
52
|
results.tickets++
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// 2. Transfer ticket messages (authorClient)
|
|
56
|
-
const messages = await payload.
|
|
57
|
-
collection: slugs.ticketMessages as any,
|
|
56
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
58
57
|
where: { authorClient: { equals: sourceId } },
|
|
59
58
|
limit: 1000,
|
|
60
59
|
depth: 0,
|
|
61
60
|
overrideAccess: true,
|
|
62
61
|
})
|
|
63
62
|
for (const msg of messages.docs) {
|
|
64
|
-
await payload
|
|
63
|
+
await dbUpdate(payload, slugs.ticketMessages, { id: msg.id, data: { authorClient: targetId }, overrideAccess: true })
|
|
65
64
|
results.ticketMessages++
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
// 3. Transfer chat messages
|
|
69
|
-
const chats = await payload.
|
|
70
|
-
collection: slugs.chatMessages as any,
|
|
68
|
+
const chats = await dbFind(payload, slugs.chatMessages, {
|
|
71
69
|
where: { client: { equals: sourceId } },
|
|
72
70
|
limit: 1000,
|
|
73
71
|
depth: 0,
|
|
74
72
|
overrideAccess: true,
|
|
75
73
|
})
|
|
76
74
|
for (const chat of chats.docs) {
|
|
77
|
-
await payload
|
|
75
|
+
await dbUpdate(payload, slugs.chatMessages, { id: chat.id, data: { client: targetId }, overrideAccess: true })
|
|
78
76
|
results.chatMessages++
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
// 4. Transfer pending emails
|
|
82
|
-
const pendingEmails = await payload.
|
|
83
|
-
collection: slugs.pendingEmails as any,
|
|
80
|
+
const pendingEmails = await dbFind(payload, slugs.pendingEmails, {
|
|
84
81
|
where: { client: { equals: sourceId } },
|
|
85
82
|
limit: 500,
|
|
86
83
|
depth: 0,
|
|
87
84
|
overrideAccess: true,
|
|
88
85
|
})
|
|
89
86
|
for (const pe of pendingEmails.docs) {
|
|
90
|
-
await payload
|
|
87
|
+
await dbUpdate(payload, slugs.pendingEmails, { id: pe.id, data: { client: targetId }, overrideAccess: true })
|
|
91
88
|
results.pendingEmails++
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
// 5. Transfer satisfaction surveys
|
|
95
|
-
const surveys = await payload.
|
|
96
|
-
collection: slugs.satisfactionSurveys as any,
|
|
92
|
+
const surveys = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
97
93
|
where: { client: { equals: sourceId } },
|
|
98
94
|
limit: 500,
|
|
99
95
|
depth: 0,
|
|
100
96
|
overrideAccess: true,
|
|
101
97
|
})
|
|
102
98
|
for (const survey of surveys.docs) {
|
|
103
|
-
await payload
|
|
99
|
+
await dbUpdate(payload, slugs.satisfactionSurveys, { id: survey.id, data: { client: targetId }, overrideAccess: true })
|
|
104
100
|
results.satisfactionSurveys++
|
|
105
101
|
}
|
|
106
102
|
|
|
107
103
|
// 6. Delete source client
|
|
108
|
-
await payload.
|
|
109
|
-
collection: slugs.supportClients as any,
|
|
104
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
110
105
|
id: sourceId,
|
|
111
106
|
overrideAccess: true,
|
|
112
107
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
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 { dbFind, dbFindByID, dbCreate, dbUpdate, dbDelete } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* POST /api/support/merge-tickets
|
|
@@ -27,8 +28,8 @@ export function createMergeTicketsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const [source, target] = await Promise.all([
|
|
30
|
-
payload
|
|
31
|
-
payload
|
|
31
|
+
dbFindByID(payload, slugs.tickets, { id: sourceTicketId, depth: 0, overrideAccess: true }),
|
|
32
|
+
dbFindByID(payload, slugs.tickets, { id: targetTicketId, depth: 0, overrideAccess: true }),
|
|
32
33
|
])
|
|
33
34
|
|
|
34
35
|
if (!source) return Response.json({ error: 'Ticket source introuvable' }, { status: 404 })
|
|
@@ -42,8 +43,7 @@ export function createMergeTicketsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
// Move all messages from source to target
|
|
45
|
-
const messages = await payload.
|
|
46
|
-
collection: slugs.ticketMessages as any,
|
|
46
|
+
const messages = await dbFind(payload, slugs.ticketMessages, {
|
|
47
47
|
where: { ticket: { equals: sourceTicketId } },
|
|
48
48
|
limit: 500,
|
|
49
49
|
depth: 0,
|
|
@@ -51,8 +51,7 @@ export function createMergeTicketsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
for (const msg of messages.docs) {
|
|
54
|
-
await payload.
|
|
55
|
-
collection: slugs.ticketMessages as any,
|
|
54
|
+
await dbUpdate(payload, slugs.ticketMessages, {
|
|
56
55
|
id: msg.id,
|
|
57
56
|
data: { ticket: targetTicketId },
|
|
58
57
|
overrideAccess: true,
|
|
@@ -60,8 +59,7 @@ export function createMergeTicketsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
// Move time entries
|
|
63
|
-
const timeEntries = await payload.
|
|
64
|
-
collection: slugs.timeEntries as any,
|
|
62
|
+
const timeEntries = await dbFind(payload, slugs.timeEntries, {
|
|
65
63
|
where: { ticket: { equals: sourceTicketId } },
|
|
66
64
|
limit: 500,
|
|
67
65
|
depth: 0,
|
|
@@ -69,8 +67,7 @@ export function createMergeTicketsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
69
67
|
})
|
|
70
68
|
|
|
71
69
|
for (const entry of timeEntries.docs) {
|
|
72
|
-
await payload.
|
|
73
|
-
collection: slugs.timeEntries as any,
|
|
70
|
+
await dbUpdate(payload, slugs.timeEntries, {
|
|
74
71
|
id: entry.id,
|
|
75
72
|
data: { ticket: targetTicketId },
|
|
76
73
|
overrideAccess: true,
|
|
@@ -79,8 +76,7 @@ export function createMergeTicketsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
79
76
|
|
|
80
77
|
// Add internal note to target
|
|
81
78
|
const sourceNumber = (source as any).ticketNumber || `#${sourceTicketId}`
|
|
82
|
-
await payload.
|
|
83
|
-
collection: slugs.ticketMessages as any,
|
|
79
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
84
80
|
data: {
|
|
85
81
|
ticket: targetTicketId,
|
|
86
82
|
body: `Messages fusionnés depuis ${sourceNumber} (${messages.totalDocs} messages, ${timeEntries.totalDocs} entrées de temps)`,
|
|
@@ -92,15 +88,13 @@ export function createMergeTicketsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
92
88
|
})
|
|
93
89
|
|
|
94
90
|
// Delete source ticket
|
|
95
|
-
await payload.
|
|
96
|
-
collection: slugs.tickets as any,
|
|
91
|
+
await dbDelete(payload, slugs.tickets, {
|
|
97
92
|
id: sourceTicketId,
|
|
98
93
|
overrideAccess: true,
|
|
99
94
|
})
|
|
100
95
|
|
|
101
96
|
// Recalculate totalTimeMinutes on target
|
|
102
|
-
const allTimeEntries = await payload.
|
|
103
|
-
collection: slugs.timeEntries as any,
|
|
97
|
+
const allTimeEntries = await dbFind(payload, slugs.timeEntries, {
|
|
104
98
|
where: { ticket: { equals: targetTicketId } },
|
|
105
99
|
limit: 500,
|
|
106
100
|
depth: 0,
|
|
@@ -112,8 +106,7 @@ export function createMergeTicketsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
112
106
|
0,
|
|
113
107
|
)
|
|
114
108
|
|
|
115
|
-
await payload.
|
|
116
|
-
collection: slugs.tickets as any,
|
|
109
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
117
110
|
id: targetTicketId,
|
|
118
111
|
data: { totalTimeMinutes: totalMinutes },
|
|
119
112
|
overrideAccess: true,
|