@consilioweb/payload-support 0.10.0 → 0.11.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/components/TicketConversation/locales/en.json +25 -1
- package/dist/components/TicketConversation/locales/fr.json +25 -1
- package/dist/index.cjs +799 -4
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +800 -6
- package/dist/styles/TicketDetail.module.scss +294 -0
- package/dist/views/TicketDetailView/client.cjs +455 -249
- package/dist/views/TicketDetailView/client.js +456 -250
- package/dist/views/TicketInboxView/client.cjs +10 -9
- package/dist/views/TicketInboxView/client.js +2 -1
- package/package.json +1 -1
- package/src/collections/ClientSummaries.ts +16 -0
- package/src/collections/TicketCollaborators.ts +93 -0
- package/src/collections/TicketFeedback.ts +116 -0
- package/src/collections/TicketMessages.ts +9 -0
- package/src/collections/Tickets.ts +31 -1
- package/src/collections/index.ts +2 -0
- package/src/components/TicketConversation/locales/en.json +25 -1
- package/src/components/TicketConversation/locales/fr.json +25 -1
- package/src/endpoints/escalate.ts +44 -0
- package/src/endpoints/index.ts +15 -0
- package/src/endpoints/invite-collaborator.ts +215 -0
- package/src/endpoints/kb-search.ts +156 -0
- package/src/endpoints/ticket-feedback.ts +104 -0
- package/src/endpoints/transfer-ticket.ts +248 -0
- package/src/index.ts +1 -0
- package/src/plugin.ts +4 -0
- package/src/portal/auth/ChatWidget.tsx +11 -0
- package/src/portal/auth/dashboard/DashboardClient.tsx +85 -99
- package/src/portal/auth/dashboard/page.tsx +11 -2
- package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +402 -0
- package/src/portal/auth/tickets/detail/page.tsx +122 -23
- package/src/portal/auth/tickets/new/KbDeflection.tsx +128 -0
- package/src/portal/auth/tickets/new/page.tsx +203 -100
- package/src/portal/locales/en.json +5 -0
- package/src/portal/locales/fr.json +5 -0
- package/src/styles/TicketDetail.module.scss +294 -0
- package/src/types.ts +19 -0
- package/src/utils/slugs.ts +2 -0
- package/src/views/TicketDetailView/client.tsx +346 -89
- package/src/views/TicketInboxView/client.tsx +2 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import crypto3, { createHmac } from 'crypto';
|
|
1
|
+
import crypto3, { randomBytes, createHmac } from 'crypto';
|
|
2
2
|
|
|
3
3
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
4
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -57,6 +57,7 @@ var DEFAULT_SLUGS = {
|
|
|
57
57
|
slaPolicies: "sla-policies",
|
|
58
58
|
macros: "macros",
|
|
59
59
|
ticketStatuses: "ticket-statuses",
|
|
60
|
+
ticketFeedback: "ticket-feedback",
|
|
60
61
|
users: "users",
|
|
61
62
|
media: "media"
|
|
62
63
|
};
|
|
@@ -612,6 +613,104 @@ function createSearchEndpoint(slugs) {
|
|
|
612
613
|
};
|
|
613
614
|
}
|
|
614
615
|
|
|
616
|
+
// src/endpoints/kb-search.ts
|
|
617
|
+
var DIACRITICS = /[̀-ͯ]/g;
|
|
618
|
+
function lexicalToPlainText(richText) {
|
|
619
|
+
if (!richText) return "";
|
|
620
|
+
const parts = [];
|
|
621
|
+
const visit = (node) => {
|
|
622
|
+
if (!node || typeof node !== "object") return;
|
|
623
|
+
if (typeof node.text === "string") {
|
|
624
|
+
parts.push(node.text);
|
|
625
|
+
}
|
|
626
|
+
if (Array.isArray(node.children)) {
|
|
627
|
+
for (const child of node.children) visit(child);
|
|
628
|
+
}
|
|
629
|
+
if (node.root) visit(node.root);
|
|
630
|
+
};
|
|
631
|
+
visit(richText);
|
|
632
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
633
|
+
}
|
|
634
|
+
function buildExcerpt(plain, maxChars = 160) {
|
|
635
|
+
if (!plain) return "";
|
|
636
|
+
if (plain.length <= maxChars) return plain;
|
|
637
|
+
const slice = plain.slice(0, maxChars + 1);
|
|
638
|
+
const lastSpace = slice.lastIndexOf(" ");
|
|
639
|
+
const cut = lastSpace > 60 ? slice.slice(0, lastSpace) : slice.slice(0, maxChars);
|
|
640
|
+
return `${cut.trim()}\u2026`;
|
|
641
|
+
}
|
|
642
|
+
function tokenize(q) {
|
|
643
|
+
return q.toLowerCase().normalize("NFD").replace(DIACRITICS, "").split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
|
|
644
|
+
}
|
|
645
|
+
function normalize(s) {
|
|
646
|
+
return s.toLowerCase().normalize("NFD").replace(DIACRITICS, "");
|
|
647
|
+
}
|
|
648
|
+
function createKbSearchEndpoint(slugs) {
|
|
649
|
+
return {
|
|
650
|
+
path: "/support/kb/search",
|
|
651
|
+
method: "get",
|
|
652
|
+
handler: async (req) => {
|
|
653
|
+
try {
|
|
654
|
+
const payload = req.payload;
|
|
655
|
+
const url = new URL(req.url);
|
|
656
|
+
const q = (url.searchParams.get("q") || "").trim();
|
|
657
|
+
const limitRaw = parseInt(url.searchParams.get("limit") || "5", 10);
|
|
658
|
+
const limit = Math.min(Math.max(Number.isNaN(limitRaw) ? 5 : limitRaw, 1), 10);
|
|
659
|
+
if (!q || q.length < 3) {
|
|
660
|
+
return Response.json({ results: [], total: 0 });
|
|
661
|
+
}
|
|
662
|
+
const tokens = tokenize(q);
|
|
663
|
+
if (tokens.length === 0) {
|
|
664
|
+
return Response.json({ results: [], total: 0 });
|
|
665
|
+
}
|
|
666
|
+
const orClauses = [];
|
|
667
|
+
for (const t of tokens) {
|
|
668
|
+
orClauses.push({ title: { contains: t } });
|
|
669
|
+
orClauses.push({ category: { contains: t } });
|
|
670
|
+
}
|
|
671
|
+
orClauses.push({ title: { contains: q } });
|
|
672
|
+
const res = await payload.find({
|
|
673
|
+
collection: slugs.knowledgeBase,
|
|
674
|
+
where: {
|
|
675
|
+
and: [
|
|
676
|
+
{ published: { equals: true } },
|
|
677
|
+
{ or: orClauses }
|
|
678
|
+
]
|
|
679
|
+
},
|
|
680
|
+
// Over-fetch so we can re-rank and trim ourselves.
|
|
681
|
+
limit: Math.max(limit * 3, 10),
|
|
682
|
+
depth: 0,
|
|
683
|
+
overrideAccess: true
|
|
684
|
+
});
|
|
685
|
+
const results = res.docs.map((a) => {
|
|
686
|
+
const title = a.title || "";
|
|
687
|
+
const category = a.category || "";
|
|
688
|
+
const titleN = normalize(title);
|
|
689
|
+
const categoryN = normalize(category);
|
|
690
|
+
let score = 0;
|
|
691
|
+
for (const t of tokens) {
|
|
692
|
+
if (titleN.includes(t)) score += 2;
|
|
693
|
+
if (categoryN.includes(t)) score += 1;
|
|
694
|
+
}
|
|
695
|
+
const plain = lexicalToPlainText(a.body);
|
|
696
|
+
return {
|
|
697
|
+
id: a.id,
|
|
698
|
+
title,
|
|
699
|
+
slug: a.slug,
|
|
700
|
+
category,
|
|
701
|
+
excerpt: buildExcerpt(plain, 160),
|
|
702
|
+
score
|
|
703
|
+
};
|
|
704
|
+
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
705
|
+
return Response.json({ results, total: results.length });
|
|
706
|
+
} catch (error) {
|
|
707
|
+
console.error("[kb-search] Error:", error);
|
|
708
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
615
714
|
// src/endpoints/bulk-action.ts
|
|
616
715
|
function createBulkActionEndpoint(slugs) {
|
|
617
716
|
return {
|
|
@@ -5204,11 +5303,467 @@ function createUserPrefsPostEndpoint(slugs) {
|
|
|
5204
5303
|
};
|
|
5205
5304
|
}
|
|
5206
5305
|
|
|
5306
|
+
// src/endpoints/escalate.ts
|
|
5307
|
+
function createEscalateEndpoint(slugs) {
|
|
5308
|
+
return {
|
|
5309
|
+
path: "/support/tickets/:id/escalate",
|
|
5310
|
+
method: "post",
|
|
5311
|
+
handler: async (req) => {
|
|
5312
|
+
try {
|
|
5313
|
+
requireAdmin(req, slugs);
|
|
5314
|
+
const idRaw = req.routeParams?.id;
|
|
5315
|
+
if (!idRaw) {
|
|
5316
|
+
return Response.json({ error: "missing-id" }, { status: 400 });
|
|
5317
|
+
}
|
|
5318
|
+
const ticketId = Number(idRaw);
|
|
5319
|
+
if (Number.isNaN(ticketId)) {
|
|
5320
|
+
return Response.json({ error: "invalid-id" }, { status: 400 });
|
|
5321
|
+
}
|
|
5322
|
+
const ticket = await req.payload.update({
|
|
5323
|
+
collection: slugs.tickets,
|
|
5324
|
+
id: ticketId,
|
|
5325
|
+
data: { status: "escalated" },
|
|
5326
|
+
overrideAccess: true
|
|
5327
|
+
});
|
|
5328
|
+
return Response.json({ ok: true, ticket });
|
|
5329
|
+
} catch (err) {
|
|
5330
|
+
const authResponse = handleAuthError(err);
|
|
5331
|
+
if (authResponse) return authResponse;
|
|
5332
|
+
console.error("[support/escalate] Error:", err);
|
|
5333
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
};
|
|
5337
|
+
}
|
|
5338
|
+
|
|
5339
|
+
// src/endpoints/ticket-feedback.ts
|
|
5340
|
+
function createTicketFeedbackEndpoint(slugs) {
|
|
5341
|
+
return {
|
|
5342
|
+
path: "/support/tickets/:id/feedback",
|
|
5343
|
+
method: "post",
|
|
5344
|
+
handler: async (req) => {
|
|
5345
|
+
try {
|
|
5346
|
+
requireClient(req, slugs);
|
|
5347
|
+
const payload = req.payload;
|
|
5348
|
+
const idRaw = req.routeParams?.id;
|
|
5349
|
+
if (!idRaw) {
|
|
5350
|
+
return Response.json({ error: "missing-id" }, { status: 400 });
|
|
5351
|
+
}
|
|
5352
|
+
const ticketId = Number(idRaw);
|
|
5353
|
+
if (Number.isNaN(ticketId)) {
|
|
5354
|
+
return Response.json({ error: "invalid-id" }, { status: 400 });
|
|
5355
|
+
}
|
|
5356
|
+
const body = await req.json();
|
|
5357
|
+
const ratingNum = Number(body?.rating);
|
|
5358
|
+
if (!Number.isInteger(ratingNum) || ratingNum < 1 || ratingNum > 5) {
|
|
5359
|
+
return Response.json(
|
|
5360
|
+
{ error: "rating (entier 1-5) requis." },
|
|
5361
|
+
{ status: 400 }
|
|
5362
|
+
);
|
|
5363
|
+
}
|
|
5364
|
+
const comment = typeof body?.comment === "string" ? body.comment.trim() : void 0;
|
|
5365
|
+
if (comment && comment.length > 5e3) {
|
|
5366
|
+
return Response.json(
|
|
5367
|
+
{ error: "Le commentaire ne peut pas depasser 5000 caracteres." },
|
|
5368
|
+
{ status: 400 }
|
|
5369
|
+
);
|
|
5370
|
+
}
|
|
5371
|
+
const ticket = await payload.findByID({
|
|
5372
|
+
collection: slugs.tickets,
|
|
5373
|
+
id: ticketId,
|
|
5374
|
+
depth: 0,
|
|
5375
|
+
overrideAccess: true
|
|
5376
|
+
});
|
|
5377
|
+
if (!ticket) {
|
|
5378
|
+
return Response.json({ error: "Ticket introuvable." }, { status: 404 });
|
|
5379
|
+
}
|
|
5380
|
+
const ticketClientId = typeof ticket.client === "object" && ticket.client !== null ? ticket.client.id : ticket.client;
|
|
5381
|
+
if (ticketClientId !== req.user.id) {
|
|
5382
|
+
return Response.json({ error: "forbidden" }, { status: 403 });
|
|
5383
|
+
}
|
|
5384
|
+
const existing = await payload.find({
|
|
5385
|
+
collection: "ticket-feedback",
|
|
5386
|
+
where: { ticket: { equals: ticketId } },
|
|
5387
|
+
limit: 1,
|
|
5388
|
+
depth: 0,
|
|
5389
|
+
overrideAccess: true
|
|
5390
|
+
});
|
|
5391
|
+
if (existing.docs.length > 0) {
|
|
5392
|
+
return Response.json(
|
|
5393
|
+
{ error: "Feedback deja soumis pour ce ticket." },
|
|
5394
|
+
{ status: 409 }
|
|
5395
|
+
);
|
|
5396
|
+
}
|
|
5397
|
+
const feedback = await payload.create({
|
|
5398
|
+
collection: "ticket-feedback",
|
|
5399
|
+
data: {
|
|
5400
|
+
ticket: ticketId,
|
|
5401
|
+
client: req.user.id,
|
|
5402
|
+
rating: ratingNum,
|
|
5403
|
+
...comment ? { comment } : {},
|
|
5404
|
+
submittedFrom: "portal"
|
|
5405
|
+
},
|
|
5406
|
+
overrideAccess: true
|
|
5407
|
+
});
|
|
5408
|
+
return Response.json({ success: true, feedback });
|
|
5409
|
+
} catch (err) {
|
|
5410
|
+
const authResponse = handleAuthError(err);
|
|
5411
|
+
if (authResponse) return authResponse;
|
|
5412
|
+
console.error("[support/ticket-feedback] Error:", err);
|
|
5413
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
};
|
|
5417
|
+
}
|
|
5418
|
+
|
|
5419
|
+
// src/endpoints/transfer-ticket.ts
|
|
5420
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5421
|
+
var MAX_TRANSFERS_PER_DAY = 5;
|
|
5422
|
+
function createTransferTicketEndpoint(slugs) {
|
|
5423
|
+
return {
|
|
5424
|
+
path: "/support/tickets/:id/transfer",
|
|
5425
|
+
method: "post",
|
|
5426
|
+
handler: async (req) => {
|
|
5427
|
+
try {
|
|
5428
|
+
if (!req.user) throw new AuthError("Authentication required", 401);
|
|
5429
|
+
const idRaw = req.routeParams?.id;
|
|
5430
|
+
if (!idRaw) return Response.json({ error: "missing-id" }, { status: 400 });
|
|
5431
|
+
const ticketId = Number(idRaw);
|
|
5432
|
+
if (Number.isNaN(ticketId)) return Response.json({ error: "invalid-id" }, { status: 400 });
|
|
5433
|
+
let body;
|
|
5434
|
+
try {
|
|
5435
|
+
body = await req.json();
|
|
5436
|
+
} catch {
|
|
5437
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5438
|
+
}
|
|
5439
|
+
const email = (body.email || "").trim().toLowerCase();
|
|
5440
|
+
if (!email || !EMAIL_RE.test(email) || email.length > 254) {
|
|
5441
|
+
return Response.json({ error: "Email invalide" }, { status: 400 });
|
|
5442
|
+
}
|
|
5443
|
+
const includeAttachments = body.includeAttachments !== false;
|
|
5444
|
+
const customMessage = typeof body.message === "string" ? body.message.slice(0, 1e3) : "";
|
|
5445
|
+
const payload = req.payload;
|
|
5446
|
+
const ticket = await payload.findByID({
|
|
5447
|
+
collection: slugs.tickets,
|
|
5448
|
+
id: ticketId,
|
|
5449
|
+
depth: 1,
|
|
5450
|
+
overrideAccess: true
|
|
5451
|
+
});
|
|
5452
|
+
if (!ticket) return Response.json({ error: "Ticket introuvable" }, { status: 404 });
|
|
5453
|
+
const isAdmin = req.user.collection === slugs.users;
|
|
5454
|
+
const isOwner = req.user.collection === slugs.supportClients && (typeof ticket.client === "object" ? ticket.client?.id === req.user.id : ticket.client === req.user.id);
|
|
5455
|
+
if (!isAdmin && !isOwner) {
|
|
5456
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5457
|
+
}
|
|
5458
|
+
try {
|
|
5459
|
+
const since = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
5460
|
+
const existing = await payload.count({
|
|
5461
|
+
collection: slugs.emailLogs,
|
|
5462
|
+
where: {
|
|
5463
|
+
and: [
|
|
5464
|
+
{ action: { equals: "transfer" } },
|
|
5465
|
+
{ ticketNumber: { equals: ticket.ticketNumber || `#${ticketId}` } },
|
|
5466
|
+
{ createdAt: { greater_than: since } }
|
|
5467
|
+
]
|
|
5468
|
+
},
|
|
5469
|
+
overrideAccess: true
|
|
5470
|
+
});
|
|
5471
|
+
if (existing.totalDocs >= MAX_TRANSFERS_PER_DAY) {
|
|
5472
|
+
return Response.json(
|
|
5473
|
+
{ error: `Limite atteinte (${MAX_TRANSFERS_PER_DAY} transferts par 24h sur ce ticket)` },
|
|
5474
|
+
{ status: 429 }
|
|
5475
|
+
);
|
|
5476
|
+
}
|
|
5477
|
+
} catch {
|
|
5478
|
+
}
|
|
5479
|
+
const messages = await payload.find({
|
|
5480
|
+
collection: slugs.ticketMessages,
|
|
5481
|
+
where: {
|
|
5482
|
+
and: [
|
|
5483
|
+
{ ticket: { equals: ticketId } },
|
|
5484
|
+
{ isInternal: { equals: false } }
|
|
5485
|
+
]
|
|
5486
|
+
},
|
|
5487
|
+
sort: "createdAt",
|
|
5488
|
+
limit: 200,
|
|
5489
|
+
depth: 1,
|
|
5490
|
+
overrideAccess: true
|
|
5491
|
+
});
|
|
5492
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
5493
|
+
const ticketNumber = ticket.ticketNumber || `#${ticketId}`;
|
|
5494
|
+
const subject = ticket.subject || "Ticket";
|
|
5495
|
+
const statusLabel = statusToLabel(ticket.status);
|
|
5496
|
+
const timelineHtml = messages.docs.map((m) => {
|
|
5497
|
+
const date = m.createdAt ? new Date(m.createdAt).toLocaleString("fr-FR", { timeZone: "Europe/Paris" }) : "";
|
|
5498
|
+
const authorLabel = m.authorType === "admin" ? "\xC9quipe support" : m.authorClient && typeof m.authorClient === "object" ? `${m.authorClient.firstName || ""} ${m.authorClient.lastName || ""}`.trim() || "Client" : "Client";
|
|
5499
|
+
const bodyText = m.body || "";
|
|
5500
|
+
const bodyHtml = escapeHtml(bodyText).replace(/\n/g, "<br/>");
|
|
5501
|
+
let attachmentsHtml = "";
|
|
5502
|
+
if (includeAttachments && Array.isArray(m.attachments) && m.attachments.length > 0) {
|
|
5503
|
+
const items = m.attachments.map((a) => {
|
|
5504
|
+
const f = a?.file;
|
|
5505
|
+
if (!f || typeof f !== "object") return "";
|
|
5506
|
+
const url = f.url ? f.url.startsWith("http") ? f.url : `${baseUrl}${f.url}` : "";
|
|
5507
|
+
const name = f.filename || f.title || "pi\xE8ce jointe";
|
|
5508
|
+
if (!url) return `<li>${escapeHtml(name)} (lien indisponible)</li>`;
|
|
5509
|
+
return `<li><a href="${escapeHtml(url)}">${escapeHtml(name)}</a></li>`;
|
|
5510
|
+
}).filter(Boolean).join("");
|
|
5511
|
+
if (items) {
|
|
5512
|
+
attachmentsHtml = `<ul style="margin: 8px 0 0 0; padding-left: 20px; font-size: 13px; color: #475569;">${items}</ul>`;
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
return `
|
|
5516
|
+
<div style="margin: 0 0 16px 0; padding: 14px 16px; border-left: 3px solid #e2e8f0; background: #f8fafc; border-radius: 0 8px 8px 0;">
|
|
5517
|
+
<div style="margin-bottom: 8px; font-size: 12px; color: #64748b;">
|
|
5518
|
+
<strong style="color: #0f172a;">${escapeHtml(authorLabel)}</strong>
|
|
5519
|
+
${date ? ` · ${escapeHtml(date)}` : ""}
|
|
5520
|
+
</div>
|
|
5521
|
+
<div style="font-size: 14px; line-height: 1.6; color: #1e293b; white-space: pre-wrap;">${bodyHtml || "<em>(message vide)</em>"}</div>
|
|
5522
|
+
${attachmentsHtml}
|
|
5523
|
+
</div>
|
|
5524
|
+
`;
|
|
5525
|
+
}).join("");
|
|
5526
|
+
const customMessageHtml = customMessage ? `<div style="margin: 0 0 24px 0; padding: 16px; background: #fef9c3; border: 1px solid #fde047; border-radius: 8px;">
|
|
5527
|
+
<div style="font-size: 12px; font-weight: 700; text-transform: uppercase; color: #854d0e; margin-bottom: 6px;">Message du transmetteur</div>
|
|
5528
|
+
<div style="font-size: 14px; color: #422006; white-space: pre-wrap;">${escapeHtml(customMessage)}</div>
|
|
5529
|
+
</div>` : "";
|
|
5530
|
+
const settings = await readSupportSettings(payload).catch(() => null);
|
|
5531
|
+
const replyTo = settings?.email?.replyToAddress || process.env.SUPPORT_REPLY_TO || "";
|
|
5532
|
+
const html = emailWrapper(
|
|
5533
|
+
`R\xE9capitulatif ticket ${ticketNumber}`,
|
|
5534
|
+
[
|
|
5535
|
+
emailParagraph(`Bonjour,`),
|
|
5536
|
+
emailParagraph(
|
|
5537
|
+
`Voici le r\xE9capitulatif du ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(subject)}</em>.`
|
|
5538
|
+
),
|
|
5539
|
+
customMessageHtml,
|
|
5540
|
+
`<table cellpadding="0" cellspacing="0" border="0" style="margin: 0 0 24px 0;">
|
|
5541
|
+
<tr><td style="padding: 4px 12px 4px 0; font-size: 12px; text-transform: uppercase; color: #64748b; font-weight: 700;">Sujet</td><td style="padding: 4px 0; font-size: 14px; color: #0f172a;">${escapeHtml(subject)}</td></tr>
|
|
5542
|
+
<tr><td style="padding: 4px 12px 4px 0; font-size: 12px; text-transform: uppercase; color: #64748b; font-weight: 700;">Statut</td><td style="padding: 4px 0; font-size: 14px; color: #0f172a;">${escapeHtml(statusLabel)}</td></tr>
|
|
5543
|
+
<tr><td style="padding: 4px 12px 4px 0; font-size: 12px; text-transform: uppercase; color: #64748b; font-weight: 700;">Num\xE9ro</td><td style="padding: 4px 0; font-size: 14px; color: #0f172a;">${escapeHtml(ticketNumber)}</td></tr>
|
|
5544
|
+
</table>`,
|
|
5545
|
+
`<h2 style="margin: 24px 0 12px 0; font-size: 16px; color: #0f172a;">Conversation</h2>`,
|
|
5546
|
+
timelineHtml || emailParagraph("<em>Aucun \xE9change \xE0 afficher.</em>"),
|
|
5547
|
+
baseUrl ? emailButton("Voir le ticket", `${baseUrl}/support/tickets/${ticketId}`) : "",
|
|
5548
|
+
emailParagraph(
|
|
5549
|
+
`<span style="font-size: 12px; color: #94a3b8;">Ce r\xE9capitulatif a \xE9t\xE9 transmis depuis l'espace support. Ne r\xE9pondez pas \xE0 cet email \u2014 utilisez le lien ci-dessus pour \xE9changer.</span>`
|
|
5550
|
+
)
|
|
5551
|
+
].join("")
|
|
5552
|
+
);
|
|
5553
|
+
await payload.sendEmail({
|
|
5554
|
+
to: email,
|
|
5555
|
+
...replyTo ? { replyTo } : {},
|
|
5556
|
+
subject: `R\xE9capitulatif support \u2014 ${ticketNumber} ${subject}`,
|
|
5557
|
+
html
|
|
5558
|
+
});
|
|
5559
|
+
try {
|
|
5560
|
+
await payload.create({
|
|
5561
|
+
collection: slugs.emailLogs,
|
|
5562
|
+
data: {
|
|
5563
|
+
status: "success",
|
|
5564
|
+
action: "transfer",
|
|
5565
|
+
senderEmail: req.user.collection === slugs.supportClients ? req.user.email : req.user.email,
|
|
5566
|
+
recipientEmail: email,
|
|
5567
|
+
subject: `Transfert ${ticketNumber}`,
|
|
5568
|
+
ticketNumber
|
|
5569
|
+
},
|
|
5570
|
+
overrideAccess: true
|
|
5571
|
+
});
|
|
5572
|
+
} catch {
|
|
5573
|
+
}
|
|
5574
|
+
return Response.json({ ok: true, sentTo: email });
|
|
5575
|
+
} catch (err) {
|
|
5576
|
+
const authResponse = handleAuthError(err);
|
|
5577
|
+
if (authResponse) return authResponse;
|
|
5578
|
+
console.error("[support/transfer-ticket] Error:", err);
|
|
5579
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
};
|
|
5583
|
+
}
|
|
5584
|
+
function statusToLabel(status) {
|
|
5585
|
+
switch (status) {
|
|
5586
|
+
case "open":
|
|
5587
|
+
return "Ouvert";
|
|
5588
|
+
case "waiting_client":
|
|
5589
|
+
return "En attente du client";
|
|
5590
|
+
case "resolved":
|
|
5591
|
+
return "R\xE9solu";
|
|
5592
|
+
case "closed":
|
|
5593
|
+
return "Cl\xF4tur\xE9";
|
|
5594
|
+
case "escalated":
|
|
5595
|
+
return "Escalad\xE9";
|
|
5596
|
+
default:
|
|
5597
|
+
return String(status || "\u2014");
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5601
|
+
function createInviteCollaboratorEndpoint(slugs) {
|
|
5602
|
+
return {
|
|
5603
|
+
path: "/support/tickets/:id/invite",
|
|
5604
|
+
method: "post",
|
|
5605
|
+
handler: async (req) => {
|
|
5606
|
+
try {
|
|
5607
|
+
if (!req.user) throw new AuthError("Authentication required", 401);
|
|
5608
|
+
const idRaw = req.routeParams?.id;
|
|
5609
|
+
if (!idRaw) return Response.json({ error: "missing-id" }, { status: 400 });
|
|
5610
|
+
const ticketId = Number(idRaw);
|
|
5611
|
+
if (Number.isNaN(ticketId)) return Response.json({ error: "invalid-id" }, { status: 400 });
|
|
5612
|
+
let body;
|
|
5613
|
+
try {
|
|
5614
|
+
body = await req.json();
|
|
5615
|
+
} catch {
|
|
5616
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5617
|
+
}
|
|
5618
|
+
const email = (body.email || "").trim().toLowerCase();
|
|
5619
|
+
if (!email || !EMAIL_RE2.test(email) || email.length > 254) {
|
|
5620
|
+
return Response.json({ error: "Email invalide" }, { status: 400 });
|
|
5621
|
+
}
|
|
5622
|
+
const role = body.role === "collaborator" ? "collaborator" : "viewer";
|
|
5623
|
+
const payload = req.payload;
|
|
5624
|
+
const ticket = await payload.findByID({
|
|
5625
|
+
collection: slugs.tickets,
|
|
5626
|
+
id: ticketId,
|
|
5627
|
+
depth: 1,
|
|
5628
|
+
overrideAccess: true
|
|
5629
|
+
});
|
|
5630
|
+
if (!ticket) return Response.json({ error: "Ticket introuvable" }, { status: 404 });
|
|
5631
|
+
const isAdmin = req.user.collection === slugs.users;
|
|
5632
|
+
const ownerId = typeof ticket.client === "object" ? ticket.client?.id : ticket.client;
|
|
5633
|
+
const isOwner = req.user.collection === slugs.supportClients && ownerId === req.user.id;
|
|
5634
|
+
if (!isAdmin && !isOwner) {
|
|
5635
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
5636
|
+
}
|
|
5637
|
+
const ownerEmail = typeof ticket.client === "object" ? ticket.client?.email : void 0;
|
|
5638
|
+
if (ownerEmail && ownerEmail.toLowerCase() === email) {
|
|
5639
|
+
return Response.json({ error: "Ce client est d\xE9j\xE0 propri\xE9taire du ticket" }, { status: 400 });
|
|
5640
|
+
}
|
|
5641
|
+
let inviteeId = null;
|
|
5642
|
+
const existing = await payload.find({
|
|
5643
|
+
collection: slugs.supportClients,
|
|
5644
|
+
where: { email: { equals: email } },
|
|
5645
|
+
limit: 1,
|
|
5646
|
+
depth: 0,
|
|
5647
|
+
overrideAccess: true
|
|
5648
|
+
});
|
|
5649
|
+
if (existing.docs.length > 0) {
|
|
5650
|
+
inviteeId = existing.docs[0].id;
|
|
5651
|
+
} else {
|
|
5652
|
+
const tempPassword = randomBytes(16).toString("hex");
|
|
5653
|
+
const created = await payload.create({
|
|
5654
|
+
collection: slugs.supportClients,
|
|
5655
|
+
data: {
|
|
5656
|
+
email,
|
|
5657
|
+
firstName: "Invit\xE9",
|
|
5658
|
+
lastName: email.split("@")[0]?.slice(0, 40) || "",
|
|
5659
|
+
company: "\u2014",
|
|
5660
|
+
password: tempPassword
|
|
5661
|
+
},
|
|
5662
|
+
overrideAccess: true,
|
|
5663
|
+
// Skip the "welcome" hook firing — let invitation email do the welcome instead
|
|
5664
|
+
context: { skipInviteEmail: true }
|
|
5665
|
+
});
|
|
5666
|
+
inviteeId = created.id;
|
|
5667
|
+
}
|
|
5668
|
+
if (!inviteeId) {
|
|
5669
|
+
return Response.json({ error: "Impossible de cr\xE9er le client invit\xE9" }, { status: 500 });
|
|
5670
|
+
}
|
|
5671
|
+
const dupe = await payload.find({
|
|
5672
|
+
collection: "ticket-collaborators",
|
|
5673
|
+
where: {
|
|
5674
|
+
and: [
|
|
5675
|
+
{ ticket: { equals: ticketId } },
|
|
5676
|
+
{ client: { equals: inviteeId } }
|
|
5677
|
+
]
|
|
5678
|
+
},
|
|
5679
|
+
limit: 1,
|
|
5680
|
+
depth: 0,
|
|
5681
|
+
overrideAccess: true
|
|
5682
|
+
});
|
|
5683
|
+
const invitationToken = randomBytes(24).toString("hex");
|
|
5684
|
+
if (dupe.docs.length > 0) {
|
|
5685
|
+
await payload.update({
|
|
5686
|
+
collection: "ticket-collaborators",
|
|
5687
|
+
id: dupe.docs[0].id,
|
|
5688
|
+
data: { role, invitationToken },
|
|
5689
|
+
overrideAccess: true
|
|
5690
|
+
});
|
|
5691
|
+
} else {
|
|
5692
|
+
await payload.create({
|
|
5693
|
+
collection: "ticket-collaborators",
|
|
5694
|
+
data: {
|
|
5695
|
+
ticket: ticketId,
|
|
5696
|
+
client: inviteeId,
|
|
5697
|
+
email,
|
|
5698
|
+
role,
|
|
5699
|
+
invitedBy: {
|
|
5700
|
+
relationTo: req.user.collection,
|
|
5701
|
+
value: req.user.id
|
|
5702
|
+
},
|
|
5703
|
+
invitationToken
|
|
5704
|
+
},
|
|
5705
|
+
overrideAccess: true
|
|
5706
|
+
});
|
|
5707
|
+
}
|
|
5708
|
+
const settings = await readSupportSettings(payload).catch(() => null);
|
|
5709
|
+
const replyTo = settings?.email?.replyToAddress || process.env.SUPPORT_REPLY_TO || "";
|
|
5710
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
|
|
5711
|
+
const ticketNumber = ticket.ticketNumber || `#${ticketId}`;
|
|
5712
|
+
const subject = ticket.subject || "Ticket";
|
|
5713
|
+
const inviteUrl = `${baseUrl}/support/tickets/${ticketId}?inviteToken=${invitationToken}`;
|
|
5714
|
+
const inviterLabel = req.user.collection === slugs.users ? "L'\xE9quipe support" : `${req.user.firstName || ""} ${req.user.lastName || ""}`.trim() || "Un coll\xE8gue";
|
|
5715
|
+
const roleLabel = role === "collaborator" ? "collaborateur (peut r\xE9pondre)" : "lecteur (consultation)";
|
|
5716
|
+
await payload.sendEmail({
|
|
5717
|
+
to: email,
|
|
5718
|
+
...replyTo ? { replyTo } : {},
|
|
5719
|
+
subject: `Invitation au ticket ${ticketNumber}`,
|
|
5720
|
+
html: emailWrapper(
|
|
5721
|
+
`Invitation \xE0 un ticket support`,
|
|
5722
|
+
[
|
|
5723
|
+
emailParagraph(`Bonjour,`),
|
|
5724
|
+
emailParagraph(
|
|
5725
|
+
`<strong>${escapeHtml(inviterLabel)}</strong> vous invite \xE0 consulter le ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(subject)}</em>.`
|
|
5726
|
+
),
|
|
5727
|
+
emailParagraph(`R\xF4le attribu\xE9 : <strong>${escapeHtml(roleLabel)}</strong>.`),
|
|
5728
|
+
baseUrl ? emailButton("Acc\xE9der au ticket", inviteUrl) : "",
|
|
5729
|
+
emailParagraph(
|
|
5730
|
+
`<span style="font-size: 12px; color: #94a3b8;">Si vous n'avez pas encore de compte support, vous serez invit\xE9 \xE0 d\xE9finir un mot de passe pour activer votre acc\xE8s.</span>`
|
|
5731
|
+
)
|
|
5732
|
+
].join("")
|
|
5733
|
+
)
|
|
5734
|
+
});
|
|
5735
|
+
try {
|
|
5736
|
+
await payload.create({
|
|
5737
|
+
collection: slugs.emailLogs,
|
|
5738
|
+
data: {
|
|
5739
|
+
status: "success",
|
|
5740
|
+
action: "invite",
|
|
5741
|
+
senderEmail: req.user.email,
|
|
5742
|
+
recipientEmail: email,
|
|
5743
|
+
subject: `Invitation ${ticketNumber}`,
|
|
5744
|
+
ticketNumber
|
|
5745
|
+
},
|
|
5746
|
+
overrideAccess: true
|
|
5747
|
+
});
|
|
5748
|
+
} catch {
|
|
5749
|
+
}
|
|
5750
|
+
return Response.json({ ok: true, invitedTo: email, role });
|
|
5751
|
+
} catch (err) {
|
|
5752
|
+
const authResponse = handleAuthError(err);
|
|
5753
|
+
if (authResponse) return authResponse;
|
|
5754
|
+
console.error("[support/invite-collaborator] Error:", err);
|
|
5755
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
5756
|
+
}
|
|
5757
|
+
}
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
5760
|
+
|
|
5207
5761
|
// src/endpoints/index.ts
|
|
5208
5762
|
function createSupportEndpoints(slugs, options) {
|
|
5209
5763
|
const f = options?.features;
|
|
5210
5764
|
const endpoints = [
|
|
5211
5765
|
createSearchEndpoint(slugs),
|
|
5766
|
+
createKbSearchEndpoint(slugs),
|
|
5212
5767
|
createSettingsGetEndpoint(slugs),
|
|
5213
5768
|
createSettingsPostEndpoint(slugs),
|
|
5214
5769
|
createAdminStatsEndpoint(slugs),
|
|
@@ -5224,7 +5779,11 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5224
5779
|
createPurgeLogsEndpoint(slugs),
|
|
5225
5780
|
createResendNotificationEndpoint(slugs),
|
|
5226
5781
|
createUserPrefsGetEndpoint(slugs),
|
|
5227
|
-
createUserPrefsPostEndpoint(slugs)
|
|
5782
|
+
createUserPrefsPostEndpoint(slugs),
|
|
5783
|
+
createEscalateEndpoint(slugs),
|
|
5784
|
+
createTicketFeedbackEndpoint(slugs),
|
|
5785
|
+
createTransferTicketEndpoint(slugs),
|
|
5786
|
+
createInviteCollaboratorEndpoint(slugs)
|
|
5228
5787
|
];
|
|
5229
5788
|
if (!f || f.ai !== false) {
|
|
5230
5789
|
endpoints.push(createAiEndpoint(slugs));
|
|
@@ -6284,7 +6843,9 @@ function createTicketsCollection(slugs, options) {
|
|
|
6284
6843
|
options: [
|
|
6285
6844
|
{ label: "Ouvert", value: "open" },
|
|
6286
6845
|
{ label: "En attente client", value: "waiting_client" },
|
|
6287
|
-
{ label: "Resolu", value: "resolved" }
|
|
6846
|
+
{ label: "Resolu", value: "resolved" },
|
|
6847
|
+
{ label: "Ferme", value: "closed" },
|
|
6848
|
+
{ label: "Escalade", value: "escalated" }
|
|
6288
6849
|
],
|
|
6289
6850
|
admin: { width: "50%" }
|
|
6290
6851
|
},
|
|
@@ -6503,9 +7064,30 @@ function createTicketsCollection(slugs, options) {
|
|
|
6503
7064
|
if (req.user?.collection === slugs.supportClients) return true;
|
|
6504
7065
|
return false;
|
|
6505
7066
|
},
|
|
6506
|
-
read: ({ req }) => {
|
|
7067
|
+
read: async ({ req }) => {
|
|
6507
7068
|
if (req.user?.collection === slugs.users) return true;
|
|
6508
7069
|
if (req.user?.collection === slugs.supportClients) {
|
|
7070
|
+
let collabTicketIds = [];
|
|
7071
|
+
try {
|
|
7072
|
+
const rows = await req.payload.find({
|
|
7073
|
+
collection: "ticket-collaborators",
|
|
7074
|
+
where: { client: { equals: req.user.id } },
|
|
7075
|
+
limit: 1e3,
|
|
7076
|
+
depth: 0,
|
|
7077
|
+
overrideAccess: true
|
|
7078
|
+
});
|
|
7079
|
+
collabTicketIds = rows.docs.map((r) => typeof r.ticket === "object" ? r.ticket?.id : r.ticket).filter((v) => v !== void 0 && v !== null);
|
|
7080
|
+
} catch {
|
|
7081
|
+
}
|
|
7082
|
+
if (collabTicketIds.length > 0) {
|
|
7083
|
+
const where = {
|
|
7084
|
+
or: [
|
|
7085
|
+
{ client: { equals: req.user.id } },
|
|
7086
|
+
{ id: { in: collabTicketIds } }
|
|
7087
|
+
]
|
|
7088
|
+
};
|
|
7089
|
+
return where;
|
|
7090
|
+
}
|
|
6509
7091
|
return { client: { equals: req.user.id } };
|
|
6510
7092
|
}
|
|
6511
7093
|
return false;
|
|
@@ -6789,6 +7371,15 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
6789
7371
|
label: "Pieces jointes",
|
|
6790
7372
|
fields: [{ name: "file", type: "upload", relationTo: slugs.media, required: true, label: "Fichier" }]
|
|
6791
7373
|
},
|
|
7374
|
+
{
|
|
7375
|
+
name: "fromAlias",
|
|
7376
|
+
type: "text",
|
|
7377
|
+
label: "Alias d'exp\xE9diteur",
|
|
7378
|
+
admin: {
|
|
7379
|
+
description: "Si d\xE9fini, le message est envoy\xE9 au nom de cet alias plut\xF4t que de l'agent authentifi\xE9",
|
|
7380
|
+
position: "sidebar"
|
|
7381
|
+
}
|
|
7382
|
+
},
|
|
6792
7383
|
{ name: "isInternal", type: "checkbox", defaultValue: false, label: "Note interne", admin: { position: "sidebar" } },
|
|
6793
7384
|
{ name: "isSolution", type: "checkbox", defaultValue: false, label: "Reponse solution", admin: { position: "sidebar" } },
|
|
6794
7385
|
{ name: "skipNotification", type: "checkbox", defaultValue: false, label: "Sans notification", admin: { position: "sidebar", condition: (data) => data?.skipNotification === true } },
|
|
@@ -8292,6 +8883,22 @@ function createClientSummariesCollection(slugs) {
|
|
|
8292
8883
|
admin: { readOnly: true }
|
|
8293
8884
|
// Array of strings: "Hébergé chez OVH", "Site WordPress", etc.
|
|
8294
8885
|
},
|
|
8886
|
+
{
|
|
8887
|
+
name: "nextActions",
|
|
8888
|
+
type: "array",
|
|
8889
|
+
label: "Prochaines actions sugg\xE9r\xE9es",
|
|
8890
|
+
admin: { description: "Liste des actions sugg\xE9r\xE9es par l'IA" },
|
|
8891
|
+
fields: [
|
|
8892
|
+
{ name: "label", type: "text", required: true },
|
|
8893
|
+
{
|
|
8894
|
+
name: "priority",
|
|
8895
|
+
type: "select",
|
|
8896
|
+
options: ["now", "today", "this-week"],
|
|
8897
|
+
defaultValue: "today"
|
|
8898
|
+
},
|
|
8899
|
+
{ name: "done", type: "checkbox", defaultValue: false }
|
|
8900
|
+
]
|
|
8901
|
+
},
|
|
8295
8902
|
// ── Stats ──
|
|
8296
8903
|
{
|
|
8297
8904
|
name: "ticketCount",
|
|
@@ -8349,6 +8956,191 @@ function createClientSummariesCollection(slugs) {
|
|
|
8349
8956
|
};
|
|
8350
8957
|
}
|
|
8351
8958
|
|
|
8959
|
+
// src/collections/TicketFeedback.ts
|
|
8960
|
+
function createAlertOnLowRating(slugs, notificationSlug) {
|
|
8961
|
+
return async ({ doc, operation, req }) => {
|
|
8962
|
+
if (operation !== "create") return doc;
|
|
8963
|
+
const rating = typeof doc.rating === "number" ? doc.rating : Number(doc.rating);
|
|
8964
|
+
if (!Number.isFinite(rating) || rating > 2) return doc;
|
|
8965
|
+
try {
|
|
8966
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket?.id : doc.ticket;
|
|
8967
|
+
const ticketNumber = typeof doc.ticket === "object" ? doc.ticket?.ticketNumber : void 0;
|
|
8968
|
+
const ticketLabel = ticketNumber || (ticketId ? `#${ticketId}` : "?");
|
|
8969
|
+
await createAdminNotification(req.payload, {
|
|
8970
|
+
title: `Client mecontent : ${ticketLabel}`,
|
|
8971
|
+
message: `Note ${rating}/5${doc.comment ? ` \u2014 ${String(doc.comment).slice(0, 200)}` : ""}`,
|
|
8972
|
+
type: "satisfaction",
|
|
8973
|
+
link: ticketId ? `/admin/collections/${slugs.tickets}/${ticketId}` : void 0
|
|
8974
|
+
}, notificationSlug);
|
|
8975
|
+
} catch (err) {
|
|
8976
|
+
console.error("[support] Failed to alert on low rating:", err);
|
|
8977
|
+
}
|
|
8978
|
+
return doc;
|
|
8979
|
+
};
|
|
8980
|
+
}
|
|
8981
|
+
function createTicketFeedbackCollection(slugs, options) {
|
|
8982
|
+
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
8983
|
+
return {
|
|
8984
|
+
slug: "ticket-feedback",
|
|
8985
|
+
labels: {
|
|
8986
|
+
singular: "Feedback ticket",
|
|
8987
|
+
plural: "Feedbacks tickets"
|
|
8988
|
+
},
|
|
8989
|
+
admin: {
|
|
8990
|
+
useAsTitle: "rating",
|
|
8991
|
+
group: "Support",
|
|
8992
|
+
defaultColumns: ["ticket", "rating", "client", "submittedFrom", "createdAt"]
|
|
8993
|
+
},
|
|
8994
|
+
access: {
|
|
8995
|
+
// Creation is gated by the dedicated endpoint (signed token / portal session).
|
|
8996
|
+
// We accept admins and authenticated support clients here; anonymous creation
|
|
8997
|
+
// must go through the endpoint which uses overrideAccess.
|
|
8998
|
+
create: ({ req }) => {
|
|
8999
|
+
if (req.user?.collection === slugs.users) return true;
|
|
9000
|
+
if (req.user?.collection === slugs.supportClients) return true;
|
|
9001
|
+
return false;
|
|
9002
|
+
},
|
|
9003
|
+
read: ({ req }) => {
|
|
9004
|
+
return req.user?.collection === slugs.users;
|
|
9005
|
+
},
|
|
9006
|
+
update: () => false,
|
|
9007
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
9008
|
+
},
|
|
9009
|
+
fields: [
|
|
9010
|
+
{
|
|
9011
|
+
name: "ticket",
|
|
9012
|
+
type: "relationship",
|
|
9013
|
+
relationTo: slugs.tickets,
|
|
9014
|
+
required: true,
|
|
9015
|
+
hasMany: false,
|
|
9016
|
+
unique: true,
|
|
9017
|
+
label: "Ticket",
|
|
9018
|
+
admin: { readOnly: true }
|
|
9019
|
+
},
|
|
9020
|
+
{
|
|
9021
|
+
name: "rating",
|
|
9022
|
+
type: "number",
|
|
9023
|
+
required: true,
|
|
9024
|
+
min: 1,
|
|
9025
|
+
max: 5,
|
|
9026
|
+
label: "Note (1-5)",
|
|
9027
|
+
admin: { description: "1 = tres insatisfait, 5 = tres satisfait" }
|
|
9028
|
+
},
|
|
9029
|
+
{
|
|
9030
|
+
name: "comment",
|
|
9031
|
+
type: "textarea",
|
|
9032
|
+
label: "Commentaire",
|
|
9033
|
+
admin: { description: "Commentaire optionnel du client" }
|
|
9034
|
+
},
|
|
9035
|
+
{
|
|
9036
|
+
name: "client",
|
|
9037
|
+
type: "relationship",
|
|
9038
|
+
relationTo: slugs.supportClients,
|
|
9039
|
+
label: "Client",
|
|
9040
|
+
admin: { readOnly: true }
|
|
9041
|
+
},
|
|
9042
|
+
{
|
|
9043
|
+
name: "submittedFrom",
|
|
9044
|
+
type: "select",
|
|
9045
|
+
defaultValue: "portal",
|
|
9046
|
+
label: "Source",
|
|
9047
|
+
options: [
|
|
9048
|
+
{ label: "Portail", value: "portal" },
|
|
9049
|
+
{ label: "Email", value: "email" },
|
|
9050
|
+
{ label: "Admin", value: "admin" }
|
|
9051
|
+
],
|
|
9052
|
+
admin: { readOnly: true }
|
|
9053
|
+
}
|
|
9054
|
+
],
|
|
9055
|
+
hooks: {
|
|
9056
|
+
afterChange: [createAlertOnLowRating(slugs, notificationSlug)]
|
|
9057
|
+
},
|
|
9058
|
+
timestamps: true
|
|
9059
|
+
};
|
|
9060
|
+
}
|
|
9061
|
+
|
|
9062
|
+
// src/collections/TicketCollaborators.ts
|
|
9063
|
+
function createTicketCollaboratorsCollection(slugs) {
|
|
9064
|
+
return {
|
|
9065
|
+
slug: "ticket-collaborators",
|
|
9066
|
+
labels: {
|
|
9067
|
+
singular: "Collaborateur ticket",
|
|
9068
|
+
plural: "Collaborateurs tickets"
|
|
9069
|
+
},
|
|
9070
|
+
admin: {
|
|
9071
|
+
useAsTitle: "email",
|
|
9072
|
+
group: "Support",
|
|
9073
|
+
defaultColumns: ["email", "ticket", "client", "role", "invitedBy", "createdAt"]
|
|
9074
|
+
},
|
|
9075
|
+
access: {
|
|
9076
|
+
// Admins always; clients only see rows that reference them.
|
|
9077
|
+
read: ({ req }) => {
|
|
9078
|
+
if (req.user?.collection === slugs.users) return true;
|
|
9079
|
+
if (req.user?.collection === slugs.supportClients) {
|
|
9080
|
+
return { client: { equals: req.user.id } };
|
|
9081
|
+
}
|
|
9082
|
+
return false;
|
|
9083
|
+
},
|
|
9084
|
+
create: ({ req }) => !!req.user,
|
|
9085
|
+
update: () => false,
|
|
9086
|
+
delete: ({ req }) => req.user?.collection === slugs.users
|
|
9087
|
+
},
|
|
9088
|
+
fields: [
|
|
9089
|
+
{
|
|
9090
|
+
name: "ticket",
|
|
9091
|
+
type: "relationship",
|
|
9092
|
+
relationTo: slugs.tickets,
|
|
9093
|
+
required: true,
|
|
9094
|
+
hasMany: false,
|
|
9095
|
+
label: "Ticket"
|
|
9096
|
+
},
|
|
9097
|
+
{
|
|
9098
|
+
name: "client",
|
|
9099
|
+
type: "relationship",
|
|
9100
|
+
relationTo: slugs.supportClients,
|
|
9101
|
+
required: true,
|
|
9102
|
+
label: "Client invit\xE9"
|
|
9103
|
+
},
|
|
9104
|
+
{
|
|
9105
|
+
name: "email",
|
|
9106
|
+
type: "email",
|
|
9107
|
+
index: true,
|
|
9108
|
+
label: "Email invit\xE9",
|
|
9109
|
+
admin: { description: "Cache de l'email du client invit\xE9 au moment de l'invitation" }
|
|
9110
|
+
},
|
|
9111
|
+
{
|
|
9112
|
+
name: "role",
|
|
9113
|
+
type: "select",
|
|
9114
|
+
defaultValue: "viewer",
|
|
9115
|
+
options: [
|
|
9116
|
+
{ label: "Lecteur", value: "viewer" },
|
|
9117
|
+
{ label: "Collaborateur", value: "collaborator" }
|
|
9118
|
+
],
|
|
9119
|
+
label: "R\xF4le"
|
|
9120
|
+
},
|
|
9121
|
+
{
|
|
9122
|
+
name: "invitedBy",
|
|
9123
|
+
type: "relationship",
|
|
9124
|
+
relationTo: [slugs.users, slugs.supportClients],
|
|
9125
|
+
required: true,
|
|
9126
|
+
label: "Invit\xE9 par"
|
|
9127
|
+
},
|
|
9128
|
+
{
|
|
9129
|
+
name: "acceptedAt",
|
|
9130
|
+
type: "date",
|
|
9131
|
+
label: "Accept\xE9 le"
|
|
9132
|
+
},
|
|
9133
|
+
{
|
|
9134
|
+
name: "invitationToken",
|
|
9135
|
+
type: "text",
|
|
9136
|
+
index: true,
|
|
9137
|
+
admin: { hidden: true }
|
|
9138
|
+
}
|
|
9139
|
+
],
|
|
9140
|
+
timestamps: true
|
|
9141
|
+
};
|
|
9142
|
+
}
|
|
9143
|
+
|
|
8352
9144
|
// src/plugin.ts
|
|
8353
9145
|
function viewConfig(component, path) {
|
|
8354
9146
|
return { Component: component, path };
|
|
@@ -8382,7 +9174,9 @@ function supportPlugin(config) {
|
|
|
8382
9174
|
createCannedResponsesCollection(slugs),
|
|
8383
9175
|
createTicketActivityLogCollection(slugs),
|
|
8384
9176
|
createSatisfactionSurveysCollection(slugs),
|
|
8385
|
-
createKnowledgeBaseCollection(slugs)
|
|
9177
|
+
createKnowledgeBaseCollection(slugs),
|
|
9178
|
+
createTicketFeedbackCollection(slugs, { notificationSlug: config?.notificationSlug }),
|
|
9179
|
+
createTicketCollaboratorsCollection(slugs)
|
|
8386
9180
|
];
|
|
8387
9181
|
if (features.authLogs !== false) supportCollections.push(createAuthLogsCollection(slugs));
|
|
8388
9182
|
if (features.timeTracking !== false) supportCollections.push(createTimeEntriesCollection(slugs));
|
|
@@ -8438,4 +9232,4 @@ function supportPlugin(config) {
|
|
|
8438
9232
|
};
|
|
8439
9233
|
}
|
|
8440
9234
|
|
|
8441
|
-
export { DEFAULT_FEATURES, DEFAULT_SETTINGS, DEFAULT_SLUGS, DEFAULT_USER_PREFS, calculateBusinessHoursDeadline, createAdminNotification, createAssignSlaDeadlines, createAuthLogsCollection, createCannedResponsesCollection, createChatMessagesCollection, createCheckSlaOnReply, createCheckSlaOnResolve, createEmailLogsCollection, createKnowledgeBaseCollection, createMacrosCollection, createPendingEmailsCollection, createSatisfactionSurveysCollection, createSlaPoliciesCollection, createSupportClientsCollection, createTicketActivityLogCollection, createTicketMessagesCollection, createTicketStatusEmail, createTicketStatusesCollection, createTicketsCollection, createTimeEntriesCollection, createWebhookEndpointsCollection, dispatchWebhook, generateTicketSynthesis, readSupportSettings, readUserPrefs, resolveSlugs, supportPlugin };
|
|
9235
|
+
export { DEFAULT_FEATURES, DEFAULT_SETTINGS, DEFAULT_SLUGS, DEFAULT_USER_PREFS, calculateBusinessHoursDeadline, createAdminNotification, createAssignSlaDeadlines, createAuthLogsCollection, createCannedResponsesCollection, createChatMessagesCollection, createCheckSlaOnReply, createCheckSlaOnResolve, createEmailLogsCollection, createKnowledgeBaseCollection, createMacrosCollection, createPendingEmailsCollection, createSatisfactionSurveysCollection, createSlaPoliciesCollection, createSupportClientsCollection, createTicketActivityLogCollection, createTicketCollaboratorsCollection, createTicketMessagesCollection, createTicketStatusEmail, createTicketStatusesCollection, createTicketsCollection, createTimeEntriesCollection, createWebhookEndpointsCollection, dispatchWebhook, generateTicketSynthesis, readSupportSettings, readUserPrefs, resolveSlugs, supportPlugin };
|