@consilioweb/payload-support 0.13.1 → 0.14.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/dist/index.cjs +303 -85
- package/dist/index.js +303 -85
- package/package.json +1 -1
- package/src/collections/KnowledgeBase.ts +29 -0
- package/src/collections/SupportClients.ts +115 -0
- package/src/collections/TicketMessages.ts +21 -3
- package/src/collections/Tickets.ts +22 -3
- package/src/collections/TimeEntries.ts +12 -0
- package/src/hooks/ticketStatusEmail.ts +38 -27
- package/src/utils/emailTemplate.ts +153 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consilioweb/payload-support",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -47,6 +47,14 @@ export function createKnowledgeBaseCollection(slugs: CollectionSlugs): Collectio
|
|
|
47
47
|
{ label: 'Général', value: 'general' },
|
|
48
48
|
],
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: 'excerpt',
|
|
52
|
+
type: 'textarea',
|
|
53
|
+
label: 'Résumé',
|
|
54
|
+
admin: {
|
|
55
|
+
description: 'Court résumé affiché dans les listes',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
50
58
|
{
|
|
51
59
|
name: 'body',
|
|
52
60
|
type: 'richText',
|
|
@@ -60,6 +68,13 @@ export function createKnowledgeBaseCollection(slugs: CollectionSlugs): Collectio
|
|
|
60
68
|
label: 'Publié',
|
|
61
69
|
admin: { position: 'sidebar' },
|
|
62
70
|
},
|
|
71
|
+
{
|
|
72
|
+
name: 'popular',
|
|
73
|
+
type: 'checkbox',
|
|
74
|
+
defaultValue: false,
|
|
75
|
+
label: 'Populaire',
|
|
76
|
+
admin: { position: 'sidebar' },
|
|
77
|
+
},
|
|
63
78
|
{
|
|
64
79
|
name: 'sortOrder',
|
|
65
80
|
type: 'number',
|
|
@@ -67,6 +82,20 @@ export function createKnowledgeBaseCollection(slugs: CollectionSlugs): Collectio
|
|
|
67
82
|
label: 'Ordre',
|
|
68
83
|
admin: { position: 'sidebar' },
|
|
69
84
|
},
|
|
85
|
+
{
|
|
86
|
+
name: 'helpfulYes',
|
|
87
|
+
type: 'number',
|
|
88
|
+
defaultValue: 0,
|
|
89
|
+
label: 'Votes utiles',
|
|
90
|
+
admin: { position: 'sidebar', readOnly: true },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'helpfulNo',
|
|
94
|
+
type: 'number',
|
|
95
|
+
defaultValue: 0,
|
|
96
|
+
label: 'Votes non utiles',
|
|
97
|
+
admin: { position: 'sidebar', readOnly: true },
|
|
98
|
+
},
|
|
70
99
|
],
|
|
71
100
|
hooks: {
|
|
72
101
|
beforeChange: [
|
|
@@ -134,6 +134,79 @@ export function createSupportClientsCollection(slugs: CollectionSlugs): Collecti
|
|
|
134
134
|
},
|
|
135
135
|
],
|
|
136
136
|
},
|
|
137
|
+
{
|
|
138
|
+
type: 'row',
|
|
139
|
+
fields: [
|
|
140
|
+
{
|
|
141
|
+
name: 'city',
|
|
142
|
+
type: 'text',
|
|
143
|
+
label: 'Ville',
|
|
144
|
+
admin: { width: '50%' },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'country',
|
|
148
|
+
type: 'text',
|
|
149
|
+
label: 'Pays',
|
|
150
|
+
admin: { width: '50%' },
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'additionalContacts',
|
|
156
|
+
type: 'array',
|
|
157
|
+
label: 'Contacts additionnels',
|
|
158
|
+
labels: {
|
|
159
|
+
singular: 'Contact',
|
|
160
|
+
plural: 'Contacts',
|
|
161
|
+
},
|
|
162
|
+
fields: [
|
|
163
|
+
{
|
|
164
|
+
type: 'row',
|
|
165
|
+
fields: [
|
|
166
|
+
{
|
|
167
|
+
name: 'email',
|
|
168
|
+
type: 'email',
|
|
169
|
+
required: true,
|
|
170
|
+
label: 'Email',
|
|
171
|
+
admin: { width: '50%' },
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'name',
|
|
175
|
+
type: 'text',
|
|
176
|
+
label: 'Nom',
|
|
177
|
+
admin: { width: '50%' },
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'opportunities',
|
|
185
|
+
type: 'textarea',
|
|
186
|
+
label: 'Opportunites commerciales',
|
|
187
|
+
admin: {
|
|
188
|
+
description: 'Notes commerciales (visible uniquement par les admins)',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'tier',
|
|
193
|
+
type: 'select',
|
|
194
|
+
label: 'Plan',
|
|
195
|
+
options: [
|
|
196
|
+
{ label: 'Free', value: 'free' },
|
|
197
|
+
{ label: 'Basic', value: 'basic' },
|
|
198
|
+
{ label: 'Pro', value: 'pro' },
|
|
199
|
+
{ label: 'Enterprise', value: 'enterprise' },
|
|
200
|
+
],
|
|
201
|
+
admin: { position: 'sidebar' },
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'accountManager',
|
|
205
|
+
type: 'relationship',
|
|
206
|
+
relationTo: slugs.users,
|
|
207
|
+
label: 'Agent referent',
|
|
208
|
+
admin: { position: 'sidebar' },
|
|
209
|
+
},
|
|
137
210
|
{
|
|
138
211
|
name: 'twoFactorEnabled',
|
|
139
212
|
type: 'checkbox',
|
|
@@ -174,6 +247,48 @@ export function createSupportClientsCollection(slugs: CollectionSlugs): Collecti
|
|
|
174
247
|
position: 'sidebar',
|
|
175
248
|
},
|
|
176
249
|
},
|
|
250
|
+
{
|
|
251
|
+
name: 'notifyReminders',
|
|
252
|
+
type: 'checkbox',
|
|
253
|
+
defaultValue: true,
|
|
254
|
+
label: 'Notifications rappels',
|
|
255
|
+
admin: {
|
|
256
|
+
description: 'Recevoir un email pour les rappels',
|
|
257
|
+
position: 'sidebar',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'notifyResolution',
|
|
262
|
+
type: 'checkbox',
|
|
263
|
+
defaultValue: true,
|
|
264
|
+
label: 'Notifications resolution',
|
|
265
|
+
admin: {
|
|
266
|
+
description: 'Recevoir un email a la resolution d\'un ticket',
|
|
267
|
+
position: 'sidebar',
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'notifyNews',
|
|
272
|
+
type: 'checkbox',
|
|
273
|
+
defaultValue: false,
|
|
274
|
+
label: 'Notifications actualites',
|
|
275
|
+
admin: {
|
|
276
|
+
description: 'Recevoir les actualites et nouveautes',
|
|
277
|
+
position: 'sidebar',
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'notificationFrequency',
|
|
282
|
+
type: 'select',
|
|
283
|
+
defaultValue: 'immediate',
|
|
284
|
+
label: 'Frequence des notifications',
|
|
285
|
+
options: [
|
|
286
|
+
{ label: 'Immediate', value: 'immediate' },
|
|
287
|
+
{ label: 'Quotidienne', value: 'daily' },
|
|
288
|
+
{ label: 'Hebdomadaire', value: 'weekly' },
|
|
289
|
+
],
|
|
290
|
+
admin: { position: 'sidebar' },
|
|
291
|
+
},
|
|
177
292
|
{
|
|
178
293
|
name: 'notes',
|
|
179
294
|
type: 'textarea',
|
|
@@ -67,14 +67,25 @@ function createNotifyClient(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
|
67
67
|
const supportEmail = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || ''
|
|
68
68
|
const portalUrl = `${baseUrl}/support/tickets/${ticketId}`
|
|
69
69
|
|
|
70
|
+
// Internal archive copy — this email goes TO the client, so any internal
|
|
71
|
+
// address MUST be bcc (never cc): the client must not see internal
|
|
72
|
+
// addresses, and an exposed internal cc/bcc reply chain leaks routing.
|
|
73
|
+
// Use a dedicated archive mailbox via env, NOT support@/noreply@ — those
|
|
74
|
+
// are filtered by the inbound-email anti-loop guard and would re-create a
|
|
75
|
+
// ticket from the copy.
|
|
76
|
+
const internalBcc = process.env.SUPPORT_CLIENT_BCC || undefined
|
|
77
|
+
|
|
70
78
|
// Use rich HTML content if available, otherwise plain text preview
|
|
71
79
|
const rawContent = doc.bodyHtml
|
|
72
80
|
? emailRichContent(doc.bodyHtml)
|
|
73
81
|
: emailQuote(doc.body?.length > 500 ? doc.body.slice(0, 500) + '...' : doc.body)
|
|
74
82
|
|
|
83
|
+
const replyPreview = (doc.body || '').replace(/\s+/g, ' ').trim().slice(0, 90)
|
|
84
|
+
|
|
75
85
|
await req.payload.sendEmail({
|
|
76
86
|
to: client.email,
|
|
77
87
|
...(supportEmail ? { replyTo: supportEmail } : {}),
|
|
88
|
+
...(internalBcc ? { bcc: internalBcc } : {}),
|
|
78
89
|
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
79
90
|
html: emailWrapper(`Nouvelle reponse — ${ticketNumber}`, [
|
|
80
91
|
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || '')}</strong>,`),
|
|
@@ -83,7 +94,10 @@ function createNotifyClient(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
|
83
94
|
emailButton('Consulter le ticket', portalUrl),
|
|
84
95
|
emailParagraph('<span style="font-size: 13px; color: #6b7280;">Vous pouvez egalement repondre directement a cet email. Votre message sera automatiquement ajoute au ticket.</span>'),
|
|
85
96
|
emailTrackingPixel(ticketId, doc.id),
|
|
86
|
-
].join('')
|
|
97
|
+
].join(''), {
|
|
98
|
+
kind: 'reply',
|
|
99
|
+
preheader: replyPreview ? `${replyPreview}…` : `Nouvelle reponse a votre ticket ${ticketNumber}`,
|
|
100
|
+
}),
|
|
87
101
|
})
|
|
88
102
|
|
|
89
103
|
await req.payload.update({
|
|
@@ -216,6 +230,10 @@ function createNotifyAdminOnClientMessage(slugs: CollectionSlugs, notificationSl
|
|
|
216
230
|
|
|
217
231
|
if (primaryEmail) {
|
|
218
232
|
await payload.sendEmail({
|
|
233
|
+
// INTERNAL email (to the support team) — recipients are the contact
|
|
234
|
+
// inbox + assigned agent. The cc here is internal→internal and stays
|
|
235
|
+
// a cc on purpose. Do NOT confuse with the client reply email
|
|
236
|
+
// (createNotifyClient), where internal copies must be bcc.
|
|
219
237
|
to: primaryEmail,
|
|
220
238
|
...(ccEmail ? { cc: ccEmail } : {}),
|
|
221
239
|
...(clientEmail !== 'inconnu' ? { replyTo: clientEmail } : (supportEmail ? { replyTo: supportEmail } : {})),
|
|
@@ -223,9 +241,9 @@ function createNotifyAdminOnClientMessage(slugs: CollectionSlugs, notificationSl
|
|
|
223
241
|
html: emailWrapper(headerTitle, [
|
|
224
242
|
emailParagraph(`<strong>${escapeHtml(clientName)}</strong> (${escapeHtml(clientEmail)}) a ${isNewTicket ? 'ouvert un nouveau ticket' : 'repondu au ticket'} <strong>${escapeHtml(ticketNumber)}</strong> :`),
|
|
225
243
|
`<p style="margin: 0 0 4px 0; font-size: 14px; font-weight: 600; color: #374151;">Sujet : ${escapeHtml(subject)}</p>`,
|
|
226
|
-
emailQuote(preview, isNewTicket ? '#
|
|
244
|
+
emailQuote(preview, isNewTicket ? '#1d2b4d' : '#17807c'),
|
|
227
245
|
emailButton('Ouvrir dans l\'admin', adminUrl, 'dark'),
|
|
228
|
-
].join(''), {
|
|
246
|
+
].join(''), { kind: 'system' }),
|
|
229
247
|
})
|
|
230
248
|
}
|
|
231
249
|
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
Where,
|
|
8
8
|
} from 'payload'
|
|
9
9
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
10
|
-
import { escapeHtml } from '../utils/emailTemplate'
|
|
10
|
+
import { escapeHtml, emailWrapper, emailParagraph, emailButton } from '../utils/emailTemplate'
|
|
11
11
|
import { fireWebhooks } from '../utils/fireWebhooks'
|
|
12
12
|
import { createAdminNotification } from '../utils/adminNotification'
|
|
13
13
|
import { dispatchWebhook } from '../utils/webhookDispatcher'
|
|
@@ -277,16 +277,35 @@ function createNotifyClientOnResolve(slugs: CollectionSlugs): CollectionAfterCha
|
|
|
277
277
|
depth: 0,
|
|
278
278
|
overrideAccess: true,
|
|
279
279
|
}) : null)
|
|
280
|
-
|
|
280
|
+
// Respect client notification preferences: the dedicated `notifyResolution`
|
|
281
|
+
// toggle, and the legacy global `notifyOnStatusChange`. Strict `=== false`
|
|
282
|
+
// so clients not yet migrated (undefined) keep the default-on behaviour.
|
|
283
|
+
if (!clientData?.email) return doc
|
|
284
|
+
if (clientData.notifyResolution === false || clientData.notifyOnStatusChange === false) return doc
|
|
285
|
+
|
|
286
|
+
// TODO digest/batching: if clientData.notificationFrequency !== 'immediate',
|
|
287
|
+
// this resolution should be queued and folded into a daily/weekly digest
|
|
288
|
+
// instead of sending now. Out of scope here — sent immediately for all.
|
|
289
|
+
|
|
281
290
|
const settings = await readSupportSettings(req.payload)
|
|
282
291
|
const ticketNumber = doc.ticketNumber || 'TK-????'
|
|
292
|
+
const subject = doc.subject || 'Support'
|
|
283
293
|
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || ''
|
|
284
294
|
const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || ''
|
|
295
|
+
const ticketUrl = `${baseUrl}/support/tickets/${doc.id}`
|
|
285
296
|
await req.payload.sendEmail({
|
|
286
297
|
to: clientData.email,
|
|
287
298
|
...(replyTo ? { replyTo } : {}),
|
|
288
299
|
subject: `[${ticketNumber}] Ticket resolu`,
|
|
289
|
-
html:
|
|
300
|
+
html: emailWrapper(`Votre ticket a ete resolu`, [
|
|
301
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(clientData.firstName || '')}</strong>,`),
|
|
302
|
+
emailParagraph(`Votre ticket <strong>${escapeHtml(ticketNumber)}</strong> — <em>${escapeHtml(String(subject))}</em> — a ete resolu.`),
|
|
303
|
+
emailParagraph('Si le probleme persiste ou si vous avez une question, repondez simplement a cet email pour rouvrir le ticket.'),
|
|
304
|
+
emailButton('Consulter le ticket', ticketUrl),
|
|
305
|
+
].join(''), {
|
|
306
|
+
kind: 'system',
|
|
307
|
+
preheader: `Votre ticket ${ticketNumber} a ete resolu.`,
|
|
308
|
+
}),
|
|
290
309
|
})
|
|
291
310
|
} catch (err) {
|
|
292
311
|
console.error('[support] Failed to notify client on resolve:', err)
|
|
@@ -90,6 +90,18 @@ export function createTimeEntriesCollection(slugs: CollectionSlugs): CollectionC
|
|
|
90
90
|
type: 'textarea',
|
|
91
91
|
label: 'Description du travail',
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: 'billable',
|
|
95
|
+
type: 'checkbox',
|
|
96
|
+
defaultValue: true,
|
|
97
|
+
label: 'Facturable',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'agent',
|
|
101
|
+
type: 'relationship',
|
|
102
|
+
relationTo: slugs.users,
|
|
103
|
+
label: 'Agent',
|
|
104
|
+
},
|
|
93
105
|
],
|
|
94
106
|
hooks: {
|
|
95
107
|
afterChange: [createRecalculateTicketTime(slugs)],
|
|
@@ -9,9 +9,22 @@ import { readSupportSettings } from '../utils/readSettings'
|
|
|
9
9
|
const STATUS_LABELS: Record<string, string> = {
|
|
10
10
|
open: 'Ouvert',
|
|
11
11
|
waiting_client: 'En attente client',
|
|
12
|
+
waiting_support: 'En attente support',
|
|
12
13
|
resolved: 'Resolu',
|
|
14
|
+
closed: 'Ferme',
|
|
15
|
+
escalated: 'Escalade',
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Status transitions for which we DON'T email the client.
|
|
20
|
+
* - waiting_client / waiting_support: these are the automatic ping-pong
|
|
21
|
+
* toggles. When the agent replies the ticket flips to waiting_client — but
|
|
22
|
+
* the reply email already covers it. When the client replies it flips back
|
|
23
|
+
* to waiting_support — emailing the client about their own action is noise.
|
|
24
|
+
* - resolved: handled separately by notifyClientOnResolve (Tickets.ts).
|
|
25
|
+
*/
|
|
26
|
+
const SILENT_STATUSES = new Set(['waiting_client', 'waiting_support', 'resolved'])
|
|
27
|
+
|
|
15
28
|
/**
|
|
16
29
|
* Resolve the client's email and first name from the ticket's `client` relationship.
|
|
17
30
|
* Returns null if the client cannot be resolved or has no email.
|
|
@@ -90,21 +103,24 @@ export function createTicketStatusEmail(slugs: CollectionSlugs): CollectionAfter
|
|
|
90
103
|
emailParagraph(`Nous avons bien recu votre demande de support. Voici les details :`),
|
|
91
104
|
`<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 20px;">
|
|
92
105
|
<tr>
|
|
93
|
-
<td style="padding: 8px 0; font-size:
|
|
94
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
106
|
+
<td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">N° Ticket</td>
|
|
107
|
+
<td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;"><strong>${escapeHtml(String(ticketNumber))}</strong></td>
|
|
95
108
|
</tr>
|
|
96
109
|
<tr>
|
|
97
|
-
<td style="padding: 8px 0; font-size:
|
|
98
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
110
|
+
<td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">Sujet</td>
|
|
111
|
+
<td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(String(subject))}</td>
|
|
99
112
|
</tr>
|
|
100
113
|
<tr>
|
|
101
|
-
<td style="padding: 8px 0; font-size:
|
|
102
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
114
|
+
<td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">Statut</td>
|
|
115
|
+
<td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(STATUS_LABELS[doc.status] || String(doc.status))}</td>
|
|
103
116
|
</tr>
|
|
104
117
|
</table>`,
|
|
105
118
|
emailParagraph('Notre equipe vous repondra dans les meilleurs delais.'),
|
|
106
119
|
emailButton('Consulter le ticket', ticketUrl),
|
|
107
|
-
].join('')
|
|
120
|
+
].join(''), {
|
|
121
|
+
kind: 'system',
|
|
122
|
+
preheader: `Votre demande ${ticketNumber} est bien enregistree. Nous revenons vers vous rapidement.`,
|
|
123
|
+
}),
|
|
108
124
|
})
|
|
109
125
|
|
|
110
126
|
payload.logger.info(`[tickets] Creation notification sent to ${client.email} for ${ticketNumber}`)
|
|
@@ -117,8 +133,9 @@ export function createTicketStatusEmail(slugs: CollectionSlugs): CollectionAfter
|
|
|
117
133
|
|
|
118
134
|
// --- Status change on update ---
|
|
119
135
|
if (operation === 'update' && previousDoc?.status !== doc.status) {
|
|
120
|
-
// Skip
|
|
121
|
-
|
|
136
|
+
// Skip the noisy/automatic transitions (ping-pong toggles + resolved).
|
|
137
|
+
// Only meaningful transitions (e.g. open, closed, escalated) reach the client.
|
|
138
|
+
if (SILENT_STATUSES.has(doc.status)) return doc
|
|
122
139
|
|
|
123
140
|
try {
|
|
124
141
|
const client = await resolveClient(doc, payload, slugs.supportClients)
|
|
@@ -133,14 +150,6 @@ export function createTicketStatusEmail(slugs: CollectionSlugs): CollectionAfter
|
|
|
133
150
|
const previousStatusLabel = STATUS_LABELS[previousDoc?.status] || previousDoc?.status || 'N/A'
|
|
134
151
|
const ticketUrl = `${baseUrl}/support/tickets/${doc.id}`
|
|
135
152
|
|
|
136
|
-
// Contextual message based on new status
|
|
137
|
-
let contextMessage = ''
|
|
138
|
-
if (doc.status === 'waiting_client') {
|
|
139
|
-
contextMessage = emailParagraph(
|
|
140
|
-
'Nous avons besoin d\'informations supplementaires de votre part pour continuer le traitement de votre demande. Merci de consulter le ticket et de nous repondre.',
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
153
|
await payload.sendEmail({
|
|
145
154
|
to: client.email,
|
|
146
155
|
...(supportEmail ? { replyTo: supportEmail } : {}),
|
|
@@ -150,25 +159,27 @@ export function createTicketStatusEmail(slugs: CollectionSlugs): CollectionAfter
|
|
|
150
159
|
emailParagraph(`Le statut de votre ticket a ete mis a jour :`),
|
|
151
160
|
`<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 20px;">
|
|
152
161
|
<tr>
|
|
153
|
-
<td style="padding: 8px 0; font-size:
|
|
154
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
162
|
+
<td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">N° Ticket</td>
|
|
163
|
+
<td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;"><strong>${escapeHtml(String(ticketNumber))}</strong></td>
|
|
155
164
|
</tr>
|
|
156
165
|
<tr>
|
|
157
|
-
<td style="padding: 8px 0; font-size:
|
|
158
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
166
|
+
<td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">Sujet</td>
|
|
167
|
+
<td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(String(subject))}</td>
|
|
159
168
|
</tr>
|
|
160
169
|
<tr>
|
|
161
|
-
<td style="padding: 8px 0; font-size:
|
|
162
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
170
|
+
<td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">Ancien statut</td>
|
|
171
|
+
<td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(String(previousStatusLabel))}</td>
|
|
163
172
|
</tr>
|
|
164
173
|
<tr>
|
|
165
|
-
<td style="padding: 8px 0; font-size:
|
|
166
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
174
|
+
<td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">Nouveau statut</td>
|
|
175
|
+
<td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;"><strong>${escapeHtml(String(statusLabel))}</strong></td>
|
|
167
176
|
</tr>
|
|
168
177
|
</table>`,
|
|
169
|
-
contextMessage,
|
|
170
178
|
emailButton('Consulter le ticket', ticketUrl),
|
|
171
|
-
].join('')
|
|
179
|
+
].join(''), {
|
|
180
|
+
kind: 'system',
|
|
181
|
+
preheader: `Ticket ${ticketNumber} — nouveau statut : ${statusLabel}`,
|
|
182
|
+
}),
|
|
172
183
|
})
|
|
173
184
|
|
|
174
185
|
payload.logger.info(`[tickets] Status change notification sent to ${client.email} for ${ticketNumber} (${previousDoc?.status} -> ${doc.status})`)
|