@consilioweb/payload-support 0.6.5 → 0.8.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 +1 -1
- package/dist/components/TicketConversation/hooks/useAI.cjs +2 -2
- package/dist/components/TicketConversation/hooks/useAI.js +2 -2
- package/dist/components/TicketConversation/index.cjs +97 -4
- package/dist/components/TicketConversation/index.js +97 -4
- package/dist/index.cjs +387 -11
- package/dist/index.js +387 -11
- package/package.json +1 -1
- package/src/collections/ClientSummaries.ts +118 -0
- package/src/collections/Tickets.ts +54 -8
- package/src/collections/index.ts +1 -0
- package/src/components/TicketConversation/hooks/useAI.ts +2 -2
- package/src/components/TicketConversation/index.tsx +96 -6
- package/src/endpoints/ai.ts +12 -2
- package/src/endpoints/client-intelligence.ts +257 -0
- package/src/endpoints/index.ts +5 -1
- package/src/plugin.ts +2 -0
package/dist/index.js
CHANGED
|
@@ -274,9 +274,18 @@ R\xE9dige une r\xE9ponse appropri\xE9e au dernier message du client. Sois concis
|
|
|
274
274
|
if (!aiSettings.enableRewrite) {
|
|
275
275
|
return Response.json({ rewritten: "", disabled: true });
|
|
276
276
|
}
|
|
277
|
-
const { text } = body;
|
|
277
|
+
const { text, style } = body;
|
|
278
278
|
if (!text?.trim()) return Response.json({ error: "text required" }, { status: 400 });
|
|
279
|
-
const
|
|
279
|
+
const styleInstructions = {
|
|
280
|
+
auto: "Garde le m\xEAme ton (tutoiement/vouvoiement).",
|
|
281
|
+
tutoyer: "Utilise le tutoiement. Si le texte vouvoie, convertis en tutoiement.",
|
|
282
|
+
vouvoyer: "Utilise le vouvoiement. Si le texte tutoie, convertis en vouvoiement.",
|
|
283
|
+
formel: "Adopte un ton formel et professionnel avec vouvoiement.",
|
|
284
|
+
court: "Raccourcis le texte au maximum tout en gardant le sens. Sois concis et direct.",
|
|
285
|
+
amical: "Adopte un ton chaleureux et amical avec tutoiement."
|
|
286
|
+
};
|
|
287
|
+
const styleGuide = styleInstructions[style || "auto"] || styleInstructions.auto;
|
|
288
|
+
const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de mani\xE8re plus professionnelle et corrige les fautes d'orthographe/grammaire. ${styleGuide} Ne change pas le fond du message, am\xE9liore uniquement la forme. R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
|
|
280
289
|
|
|
281
290
|
Texte original :
|
|
282
291
|
${text}`;
|
|
@@ -299,6 +308,210 @@ ${text}`;
|
|
|
299
308
|
};
|
|
300
309
|
}
|
|
301
310
|
|
|
311
|
+
// src/endpoints/client-intelligence.ts
|
|
312
|
+
function getClient2(aiSettings) {
|
|
313
|
+
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
314
|
+
if (aiSettings.provider === "ollama") {
|
|
315
|
+
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
316
|
+
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
317
|
+
}
|
|
318
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
319
|
+
}
|
|
320
|
+
function getModel2(aiSettings) {
|
|
321
|
+
return aiSettings.model || "claude-haiku-4-5-20251001";
|
|
322
|
+
}
|
|
323
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
324
|
+
function createClientIntelligenceEndpoint(slugs) {
|
|
325
|
+
const getHandler = async (req) => {
|
|
326
|
+
try {
|
|
327
|
+
requireAdmin(req, slugs);
|
|
328
|
+
const payload = req.payload;
|
|
329
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
330
|
+
const clientId = url.searchParams.get("clientId");
|
|
331
|
+
if (!clientId) return Response.json({ error: "clientId required" }, { status: 400 });
|
|
332
|
+
const existing = await payload.find({
|
|
333
|
+
collection: "client-summaries",
|
|
334
|
+
where: { client: { equals: Number(clientId) } },
|
|
335
|
+
limit: 1,
|
|
336
|
+
depth: 0,
|
|
337
|
+
overrideAccess: true
|
|
338
|
+
});
|
|
339
|
+
if (existing.docs.length > 0) {
|
|
340
|
+
const cached = existing.docs[0];
|
|
341
|
+
const age = Date.now() - new Date(cached.generatedAt || 0).getTime();
|
|
342
|
+
if (age < CACHE_TTL_MS) {
|
|
343
|
+
return Response.json({ ...cached, fromCache: true });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return await generateSummary(payload, clientId, slugs, existing.docs[0]?.id);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
const authResponse = handleAuthError(error);
|
|
349
|
+
if (authResponse) return authResponse;
|
|
350
|
+
console.error("[client-intelligence] Error:", error);
|
|
351
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const postHandler = async (req) => {
|
|
355
|
+
try {
|
|
356
|
+
requireAdmin(req, slugs);
|
|
357
|
+
const payload = req.payload;
|
|
358
|
+
const body = await req.json?.() || {};
|
|
359
|
+
const clientId = body.clientId;
|
|
360
|
+
if (!clientId) return Response.json({ error: "clientId required" }, { status: 400 });
|
|
361
|
+
const existing = await payload.find({
|
|
362
|
+
collection: "client-summaries",
|
|
363
|
+
where: { client: { equals: Number(clientId) } },
|
|
364
|
+
limit: 1,
|
|
365
|
+
depth: 0,
|
|
366
|
+
overrideAccess: true
|
|
367
|
+
});
|
|
368
|
+
return await generateSummary(payload, clientId, slugs, existing.docs[0]?.id);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
const authResponse = handleAuthError(error);
|
|
371
|
+
if (authResponse) return authResponse;
|
|
372
|
+
console.error("[client-intelligence] Refresh error:", error);
|
|
373
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
return [
|
|
377
|
+
{ path: "/support/client-intelligence", method: "get", handler: getHandler },
|
|
378
|
+
{ path: "/support/client-intelligence", method: "post", handler: postHandler }
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
async function generateSummary(payload, clientId, slugs, existingId) {
|
|
382
|
+
const aiSettings = (await readSupportSettings(payload)).ai;
|
|
383
|
+
if (!aiSettings.enableSynthesis) {
|
|
384
|
+
return Response.json({ error: "AI synthesis disabled in settings" }, { status: 400 });
|
|
385
|
+
}
|
|
386
|
+
const client = await payload.findByID({
|
|
387
|
+
collection: slugs.supportClients,
|
|
388
|
+
id: Number(clientId),
|
|
389
|
+
depth: 0,
|
|
390
|
+
overrideAccess: true
|
|
391
|
+
});
|
|
392
|
+
if (!client) return Response.json({ error: "Client not found" }, { status: 404 });
|
|
393
|
+
const clientName = [client.firstName, client.lastName].filter(Boolean).join(" ") || client.company || client.email;
|
|
394
|
+
const tickets = await payload.find({
|
|
395
|
+
collection: slugs.tickets,
|
|
396
|
+
where: { client: { equals: Number(clientId) } },
|
|
397
|
+
sort: "-createdAt",
|
|
398
|
+
limit: 50,
|
|
399
|
+
depth: 0,
|
|
400
|
+
overrideAccess: true
|
|
401
|
+
});
|
|
402
|
+
if (tickets.totalDocs === 0) {
|
|
403
|
+
return Response.json({
|
|
404
|
+
summary: "Aucun ticket pour ce client.",
|
|
405
|
+
recurringTopics: [],
|
|
406
|
+
patterns: [],
|
|
407
|
+
keyFacts: [],
|
|
408
|
+
ticketCount: 0,
|
|
409
|
+
messageCount: 0
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const ticketIds = tickets.docs.slice(0, 20).map((t) => t.id);
|
|
413
|
+
const messages = await payload.find({
|
|
414
|
+
collection: slugs.ticketMessages,
|
|
415
|
+
where: { ticket: { in: ticketIds.join(",") } },
|
|
416
|
+
sort: "createdAt",
|
|
417
|
+
limit: 200,
|
|
418
|
+
depth: 0,
|
|
419
|
+
overrideAccess: true
|
|
420
|
+
});
|
|
421
|
+
let avgSatisfaction = null;
|
|
422
|
+
try {
|
|
423
|
+
const surveys = await payload.find({
|
|
424
|
+
collection: slugs.satisfactionSurveys || "satisfaction-surveys",
|
|
425
|
+
where: { client: { equals: Number(clientId) } },
|
|
426
|
+
limit: 50,
|
|
427
|
+
depth: 0,
|
|
428
|
+
overrideAccess: true
|
|
429
|
+
});
|
|
430
|
+
if (surveys.totalDocs > 0) {
|
|
431
|
+
const ratings = surveys.docs.filter((s) => s.rating).map((s) => s.rating);
|
|
432
|
+
if (ratings.length > 0) avgSatisfaction = Math.round(ratings.reduce((a, b) => a + b, 0) / ratings.length * 10) / 10;
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
const ticketSummaries = tickets.docs.map((t) => {
|
|
437
|
+
const msgs = messages.docs.filter((m) => {
|
|
438
|
+
const mTicket = typeof m.ticket === "object" ? m.ticket.id : m.ticket;
|
|
439
|
+
return mTicket === t.id;
|
|
440
|
+
});
|
|
441
|
+
const clientMsgs = msgs.filter((m) => m.authorType === "client" || m.authorType === "email");
|
|
442
|
+
const adminMsgs = msgs.filter((m) => m.authorType === "admin");
|
|
443
|
+
return `Ticket ${t.ticketNumber} (${t.status}) \u2014 "${t.subject}"
|
|
444
|
+
Client: ${clientMsgs.map((m) => m.body?.slice(0, 200)).join(" | ")}
|
|
445
|
+
Admin: ${adminMsgs.map((m) => m.body?.slice(0, 200)).join(" | ")}`;
|
|
446
|
+
}).join("\n\n");
|
|
447
|
+
const prompt = `Tu es un assistant d'analyse CRM pour un support technique. Analyse l'historique complet de ce client et g\xE9n\xE8re un rapport structur\xE9.
|
|
448
|
+
|
|
449
|
+
CLIENT : ${clientName} (${client.company || "pas de soci\xE9t\xE9"})
|
|
450
|
+
Email : ${client.email}
|
|
451
|
+
Nombre de tickets : ${tickets.totalDocs}
|
|
452
|
+
Satisfaction moyenne : ${avgSatisfaction ?? "non \xE9valu\xE9e"}
|
|
453
|
+
|
|
454
|
+
HISTORIQUE DES TICKETS :
|
|
455
|
+
${ticketSummaries.slice(0, 4e3)}
|
|
456
|
+
|
|
457
|
+
R\xE9ponds en JSON strict (pas de markdown, pas de commentaires) avec cette structure :
|
|
458
|
+
{
|
|
459
|
+
"summary": "R\xE9sum\xE9 global du client en 2-3 phrases (qui il est, ce qu'il demande habituellement, son niveau de satisfaction)",
|
|
460
|
+
"recurringTopics": [{"topic": "nom du sujet", "count": N, "lastSeen": "YYYY-MM-DD"}],
|
|
461
|
+
"patterns": ["pattern 1 observ\xE9", "pattern 2 observ\xE9"],
|
|
462
|
+
"keyFacts": ["fait cl\xE9 1 sur le client", "fait cl\xE9 2"]
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
Sois factuel. Ne d\xE9passe pas 5 items par tableau. R\xE9ponds UNIQUEMENT avec le JSON.`;
|
|
466
|
+
const anthropic = getClient2(aiSettings);
|
|
467
|
+
const model = getModel2(aiSettings);
|
|
468
|
+
const res = await anthropic.messages.create({
|
|
469
|
+
model,
|
|
470
|
+
max_tokens: 1e3,
|
|
471
|
+
messages: [{ role: "user", content: prompt }]
|
|
472
|
+
});
|
|
473
|
+
const rawText = res.content[0].type === "text" ? res.content[0].text : "{}";
|
|
474
|
+
let parsed = {};
|
|
475
|
+
try {
|
|
476
|
+
const jsonMatch = rawText.match(/\{[\s\S]*\}/);
|
|
477
|
+
if (jsonMatch) parsed = JSON.parse(jsonMatch[0]);
|
|
478
|
+
} catch {
|
|
479
|
+
parsed = { summary: rawText, recurringTopics: [], patterns: [], keyFacts: [] };
|
|
480
|
+
}
|
|
481
|
+
const data = {
|
|
482
|
+
client: Number(clientId),
|
|
483
|
+
clientName,
|
|
484
|
+
summary: parsed.summary || "R\xE9sum\xE9 non disponible",
|
|
485
|
+
recurringTopics: parsed.recurringTopics || [],
|
|
486
|
+
patterns: parsed.patterns || [],
|
|
487
|
+
keyFacts: parsed.keyFacts || [],
|
|
488
|
+
ticketCount: tickets.totalDocs,
|
|
489
|
+
messageCount: messages.totalDocs,
|
|
490
|
+
averageSatisfaction: avgSatisfaction,
|
|
491
|
+
firstTicketAt: tickets.docs[tickets.docs.length - 1]?.createdAt || null,
|
|
492
|
+
lastTicketAt: tickets.docs[0]?.createdAt || null,
|
|
493
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
494
|
+
aiModel: model
|
|
495
|
+
};
|
|
496
|
+
let saved;
|
|
497
|
+
if (existingId) {
|
|
498
|
+
saved = await payload.update({
|
|
499
|
+
collection: "client-summaries",
|
|
500
|
+
id: existingId,
|
|
501
|
+
data,
|
|
502
|
+
overrideAccess: true
|
|
503
|
+
});
|
|
504
|
+
} else {
|
|
505
|
+
saved = await payload.create({
|
|
506
|
+
collection: "client-summaries",
|
|
507
|
+
data,
|
|
508
|
+
overrideAccess: true
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
console.log(`[client-intelligence] Generated summary for ${clientName} (${tickets.totalDocs} tickets, ${messages.totalDocs} messages)`);
|
|
512
|
+
return Response.json({ ...saved, fromCache: false });
|
|
513
|
+
}
|
|
514
|
+
|
|
302
515
|
// src/endpoints/search.ts
|
|
303
516
|
function createSearchEndpoint(slugs) {
|
|
304
517
|
return {
|
|
@@ -4750,7 +4963,10 @@ function createSupportEndpoints(slugs, options) {
|
|
|
4750
4963
|
createUserPrefsGetEndpoint(slugs),
|
|
4751
4964
|
createUserPrefsPostEndpoint(slugs)
|
|
4752
4965
|
];
|
|
4753
|
-
if (!f || f.ai !== false)
|
|
4966
|
+
if (!f || f.ai !== false) {
|
|
4967
|
+
endpoints.push(createAiEndpoint(slugs));
|
|
4968
|
+
endpoints.push(...createClientIntelligenceEndpoint(slugs));
|
|
4969
|
+
}
|
|
4754
4970
|
if (!f || f.bulkActions !== false) endpoints.push(createBulkActionEndpoint(slugs));
|
|
4755
4971
|
if (!f || f.merge !== false) endpoints.push(createMergeTicketsEndpoint(slugs));
|
|
4756
4972
|
if (!f || f.splitTicket !== false) endpoints.push(createSplitTicketEndpoint(slugs));
|
|
@@ -5578,6 +5794,17 @@ function createFireTicketWebhooks(slugs) {
|
|
|
5578
5794
|
subject: doc.subject,
|
|
5579
5795
|
previousStatus: previousDoc.status
|
|
5580
5796
|
});
|
|
5797
|
+
const clientId = typeof doc.client === "object" ? doc.client?.id : doc.client;
|
|
5798
|
+
if (clientId) {
|
|
5799
|
+
payload.update({
|
|
5800
|
+
collection: "client-summaries",
|
|
5801
|
+
where: { client: { equals: clientId } },
|
|
5802
|
+
data: { generatedAt: (/* @__PURE__ */ new Date(0)).toISOString() },
|
|
5803
|
+
// Force cache expiry
|
|
5804
|
+
overrideAccess: true
|
|
5805
|
+
}).catch(() => {
|
|
5806
|
+
});
|
|
5807
|
+
}
|
|
5581
5808
|
}
|
|
5582
5809
|
const oldAssigned = typeof previousDoc.assignedTo === "object" ? previousDoc.assignedTo?.id : previousDoc.assignedTo;
|
|
5583
5810
|
const newAssigned = typeof doc.assignedTo === "object" ? doc.assignedTo?.id : doc.assignedTo;
|
|
@@ -5666,6 +5893,37 @@ function createTicketsCollection(slugs, options) {
|
|
|
5666
5893
|
});
|
|
5667
5894
|
}
|
|
5668
5895
|
const billingFields = [
|
|
5896
|
+
{
|
|
5897
|
+
type: "row",
|
|
5898
|
+
fields: [
|
|
5899
|
+
{
|
|
5900
|
+
name: "billingType",
|
|
5901
|
+
type: "select",
|
|
5902
|
+
label: "Type de facturation",
|
|
5903
|
+
defaultValue: "hourly",
|
|
5904
|
+
options: [
|
|
5905
|
+
{ label: "Au temps pass\xE9", value: "hourly" },
|
|
5906
|
+
{ label: "Forfait", value: "flat" }
|
|
5907
|
+
],
|
|
5908
|
+
admin: { width: "33%" }
|
|
5909
|
+
},
|
|
5910
|
+
{
|
|
5911
|
+
name: "flatRateAmount",
|
|
5912
|
+
type: "number",
|
|
5913
|
+
label: "Montant forfait (EUR)",
|
|
5914
|
+
admin: {
|
|
5915
|
+
width: "33%",
|
|
5916
|
+
condition: (data) => data?.billingType === "flat"
|
|
5917
|
+
}
|
|
5918
|
+
},
|
|
5919
|
+
{
|
|
5920
|
+
name: "billedAmount",
|
|
5921
|
+
type: "number",
|
|
5922
|
+
label: "Montant factur\xE9 (EUR)",
|
|
5923
|
+
admin: { width: "33%" }
|
|
5924
|
+
}
|
|
5925
|
+
]
|
|
5926
|
+
},
|
|
5669
5927
|
{
|
|
5670
5928
|
type: "row",
|
|
5671
5929
|
fields: [
|
|
@@ -5706,14 +5964,8 @@ function createTicketsCollection(slugs, options) {
|
|
|
5706
5964
|
{
|
|
5707
5965
|
name: "paidAt",
|
|
5708
5966
|
type: "date",
|
|
5709
|
-
label: "
|
|
5967
|
+
label: "Pay\xE9 le",
|
|
5710
5968
|
admin: { width: "33%", date: { displayFormat: "dd/MM/yyyy HH:mm" } }
|
|
5711
|
-
},
|
|
5712
|
-
{
|
|
5713
|
-
name: "billedAmount",
|
|
5714
|
-
type: "number",
|
|
5715
|
-
label: "Montant facture (EUR)",
|
|
5716
|
-
admin: { width: "33%" }
|
|
5717
5969
|
}
|
|
5718
5970
|
]
|
|
5719
5971
|
}
|
|
@@ -5875,7 +6127,17 @@ function createTicketsCollection(slugs, options) {
|
|
|
5875
6127
|
{ name: "snoozeUntil", type: "date", label: "Snooze jusqu'au", admin: { position: "sidebar", date: { pickerAppearance: "dayAndTime", displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
5876
6128
|
// Billing sidebar
|
|
5877
6129
|
{ name: "billable", type: "checkbox", defaultValue: true, label: "Facturable", admin: { position: "sidebar" } },
|
|
5878
|
-
{
|
|
6130
|
+
{
|
|
6131
|
+
name: "showTimeToClient",
|
|
6132
|
+
type: "checkbox",
|
|
6133
|
+
defaultValue: true,
|
|
6134
|
+
label: "Afficher le temps au client",
|
|
6135
|
+
admin: {
|
|
6136
|
+
position: "sidebar",
|
|
6137
|
+
description: "Auto-d\xE9sactiv\xE9 en mode forfait",
|
|
6138
|
+
condition: (data) => data?.billingType !== "flat"
|
|
6139
|
+
}
|
|
6140
|
+
},
|
|
5879
6141
|
{ name: "totalTimeMinutes", type: "number", defaultValue: 0, label: "Temps total (minutes)", admin: { readOnly: true, position: "sidebar" } }
|
|
5880
6142
|
],
|
|
5881
6143
|
hooks: {
|
|
@@ -7641,6 +7903,119 @@ function createTicketStatusesCollection(slugs) {
|
|
|
7641
7903
|
};
|
|
7642
7904
|
}
|
|
7643
7905
|
|
|
7906
|
+
// src/collections/ClientSummaries.ts
|
|
7907
|
+
function createClientSummariesCollection(slugs) {
|
|
7908
|
+
return {
|
|
7909
|
+
slug: "client-summaries",
|
|
7910
|
+
labels: { singular: "R\xE9sum\xE9 client", plural: "R\xE9sum\xE9s clients" },
|
|
7911
|
+
admin: {
|
|
7912
|
+
group: "Support",
|
|
7913
|
+
hidden: true,
|
|
7914
|
+
// Not directly editable — managed via API
|
|
7915
|
+
defaultColumns: ["client", "generatedAt", "ticketCount"],
|
|
7916
|
+
useAsTitle: "clientName"
|
|
7917
|
+
},
|
|
7918
|
+
fields: [
|
|
7919
|
+
{
|
|
7920
|
+
name: "client",
|
|
7921
|
+
type: "relationship",
|
|
7922
|
+
relationTo: slugs.supportClients,
|
|
7923
|
+
required: true,
|
|
7924
|
+
unique: true,
|
|
7925
|
+
index: true,
|
|
7926
|
+
label: "Client"
|
|
7927
|
+
},
|
|
7928
|
+
{
|
|
7929
|
+
name: "clientName",
|
|
7930
|
+
type: "text",
|
|
7931
|
+
label: "Nom client",
|
|
7932
|
+
admin: { readOnly: true }
|
|
7933
|
+
},
|
|
7934
|
+
// ── AI-generated content ──
|
|
7935
|
+
{
|
|
7936
|
+
name: "summary",
|
|
7937
|
+
type: "textarea",
|
|
7938
|
+
label: "R\xE9sum\xE9 global",
|
|
7939
|
+
admin: { readOnly: true }
|
|
7940
|
+
},
|
|
7941
|
+
{
|
|
7942
|
+
name: "recurringTopics",
|
|
7943
|
+
type: "json",
|
|
7944
|
+
label: "Sujets r\xE9currents",
|
|
7945
|
+
admin: { readOnly: true }
|
|
7946
|
+
// Array of { topic: string, count: number, lastSeen: string }
|
|
7947
|
+
},
|
|
7948
|
+
{
|
|
7949
|
+
name: "patterns",
|
|
7950
|
+
type: "json",
|
|
7951
|
+
label: "Patterns d\xE9tect\xE9s",
|
|
7952
|
+
admin: { readOnly: true }
|
|
7953
|
+
// Array of strings: "Revient souvent pour X", "Préfère le tutoiement", etc.
|
|
7954
|
+
},
|
|
7955
|
+
{
|
|
7956
|
+
name: "keyFacts",
|
|
7957
|
+
type: "json",
|
|
7958
|
+
label: "Faits cl\xE9s",
|
|
7959
|
+
admin: { readOnly: true }
|
|
7960
|
+
// Array of strings: "Hébergé chez OVH", "Site WordPress", etc.
|
|
7961
|
+
},
|
|
7962
|
+
// ── Stats ──
|
|
7963
|
+
{
|
|
7964
|
+
name: "ticketCount",
|
|
7965
|
+
type: "number",
|
|
7966
|
+
label: "Nombre de tickets analys\xE9s",
|
|
7967
|
+
defaultValue: 0,
|
|
7968
|
+
admin: { readOnly: true }
|
|
7969
|
+
},
|
|
7970
|
+
{
|
|
7971
|
+
name: "messageCount",
|
|
7972
|
+
type: "number",
|
|
7973
|
+
label: "Nombre de messages analys\xE9s",
|
|
7974
|
+
defaultValue: 0,
|
|
7975
|
+
admin: { readOnly: true }
|
|
7976
|
+
},
|
|
7977
|
+
{
|
|
7978
|
+
name: "averageSatisfaction",
|
|
7979
|
+
type: "number",
|
|
7980
|
+
label: "Satisfaction moyenne",
|
|
7981
|
+
admin: { readOnly: true }
|
|
7982
|
+
},
|
|
7983
|
+
{
|
|
7984
|
+
name: "firstTicketAt",
|
|
7985
|
+
type: "date",
|
|
7986
|
+
label: "Premier ticket",
|
|
7987
|
+
admin: { readOnly: true }
|
|
7988
|
+
},
|
|
7989
|
+
{
|
|
7990
|
+
name: "lastTicketAt",
|
|
7991
|
+
type: "date",
|
|
7992
|
+
label: "Dernier ticket",
|
|
7993
|
+
admin: { readOnly: true }
|
|
7994
|
+
},
|
|
7995
|
+
// ── Meta ──
|
|
7996
|
+
{
|
|
7997
|
+
name: "generatedAt",
|
|
7998
|
+
type: "date",
|
|
7999
|
+
label: "G\xE9n\xE9r\xE9 le",
|
|
8000
|
+
admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } }
|
|
8001
|
+
},
|
|
8002
|
+
{
|
|
8003
|
+
name: "aiModel",
|
|
8004
|
+
type: "text",
|
|
8005
|
+
label: "Mod\xE8le IA utilis\xE9",
|
|
8006
|
+
admin: { readOnly: true }
|
|
8007
|
+
}
|
|
8008
|
+
],
|
|
8009
|
+
access: {
|
|
8010
|
+
create: ({ req }) => req.user?.collection === "users",
|
|
8011
|
+
read: ({ req }) => req.user?.collection === "users",
|
|
8012
|
+
update: ({ req }) => req.user?.collection === "users",
|
|
8013
|
+
delete: ({ req }) => req.user?.collection === "users"
|
|
8014
|
+
},
|
|
8015
|
+
timestamps: true
|
|
8016
|
+
};
|
|
8017
|
+
}
|
|
8018
|
+
|
|
7644
8019
|
// src/plugin.ts
|
|
7645
8020
|
function viewConfig(component, path) {
|
|
7646
8021
|
return { Component: component, path };
|
|
@@ -7685,6 +8060,7 @@ function supportPlugin(config) {
|
|
|
7685
8060
|
if (features.customStatuses !== false) supportCollections.push(createTicketStatusesCollection(slugs));
|
|
7686
8061
|
if (features.chat) supportCollections.push(createChatMessagesCollection(slugs));
|
|
7687
8062
|
if (features.pendingEmails) supportCollections.push(createPendingEmailsCollection(slugs));
|
|
8063
|
+
if (features.ai !== false) supportCollections.push(createClientSummariesCollection(slugs));
|
|
7688
8064
|
const existingViews = incomingConfig.admin?.components?.views || {};
|
|
7689
8065
|
const supportViews = {
|
|
7690
8066
|
"support-inbox": viewConfig(`${viewsBase}#TicketInboxView`, `${bp}/inbox`),
|
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { CollectionConfig } from 'payload'
|
|
2
|
+
import type { CollectionSlugs } from '../utils/slugs.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client Intelligence Summaries
|
|
6
|
+
* Stores AI-generated summaries per client: recurring topics, patterns, satisfaction trends.
|
|
7
|
+
* Summaries are cached and refreshed on-demand or when a ticket is resolved.
|
|
8
|
+
*/
|
|
9
|
+
export function createClientSummariesCollection(slugs: CollectionSlugs): CollectionConfig {
|
|
10
|
+
return {
|
|
11
|
+
slug: 'client-summaries',
|
|
12
|
+
labels: { singular: 'Résumé client', plural: 'Résumés clients' },
|
|
13
|
+
admin: {
|
|
14
|
+
group: 'Support',
|
|
15
|
+
hidden: true, // Not directly editable — managed via API
|
|
16
|
+
defaultColumns: ['client', 'generatedAt', 'ticketCount'],
|
|
17
|
+
useAsTitle: 'clientName',
|
|
18
|
+
},
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
name: 'client',
|
|
22
|
+
type: 'relationship',
|
|
23
|
+
relationTo: slugs.supportClients,
|
|
24
|
+
required: true,
|
|
25
|
+
unique: true,
|
|
26
|
+
index: true,
|
|
27
|
+
label: 'Client',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'clientName',
|
|
31
|
+
type: 'text',
|
|
32
|
+
label: 'Nom client',
|
|
33
|
+
admin: { readOnly: true },
|
|
34
|
+
},
|
|
35
|
+
// ── AI-generated content ──
|
|
36
|
+
{
|
|
37
|
+
name: 'summary',
|
|
38
|
+
type: 'textarea',
|
|
39
|
+
label: 'Résumé global',
|
|
40
|
+
admin: { readOnly: true },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'recurringTopics',
|
|
44
|
+
type: 'json',
|
|
45
|
+
label: 'Sujets récurrents',
|
|
46
|
+
admin: { readOnly: true },
|
|
47
|
+
// Array of { topic: string, count: number, lastSeen: string }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'patterns',
|
|
51
|
+
type: 'json',
|
|
52
|
+
label: 'Patterns détectés',
|
|
53
|
+
admin: { readOnly: true },
|
|
54
|
+
// Array of strings: "Revient souvent pour X", "Préfère le tutoiement", etc.
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'keyFacts',
|
|
58
|
+
type: 'json',
|
|
59
|
+
label: 'Faits clés',
|
|
60
|
+
admin: { readOnly: true },
|
|
61
|
+
// Array of strings: "Hébergé chez OVH", "Site WordPress", etc.
|
|
62
|
+
},
|
|
63
|
+
// ── Stats ──
|
|
64
|
+
{
|
|
65
|
+
name: 'ticketCount',
|
|
66
|
+
type: 'number',
|
|
67
|
+
label: 'Nombre de tickets analysés',
|
|
68
|
+
defaultValue: 0,
|
|
69
|
+
admin: { readOnly: true },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'messageCount',
|
|
73
|
+
type: 'number',
|
|
74
|
+
label: 'Nombre de messages analysés',
|
|
75
|
+
defaultValue: 0,
|
|
76
|
+
admin: { readOnly: true },
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'averageSatisfaction',
|
|
80
|
+
type: 'number',
|
|
81
|
+
label: 'Satisfaction moyenne',
|
|
82
|
+
admin: { readOnly: true },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'firstTicketAt',
|
|
86
|
+
type: 'date',
|
|
87
|
+
label: 'Premier ticket',
|
|
88
|
+
admin: { readOnly: true },
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'lastTicketAt',
|
|
92
|
+
type: 'date',
|
|
93
|
+
label: 'Dernier ticket',
|
|
94
|
+
admin: { readOnly: true },
|
|
95
|
+
},
|
|
96
|
+
// ── Meta ──
|
|
97
|
+
{
|
|
98
|
+
name: 'generatedAt',
|
|
99
|
+
type: 'date',
|
|
100
|
+
label: 'Généré le',
|
|
101
|
+
admin: { readOnly: true, date: { displayFormat: 'dd/MM/yyyy HH:mm' } },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'aiModel',
|
|
105
|
+
type: 'text',
|
|
106
|
+
label: 'Modèle IA utilisé',
|
|
107
|
+
admin: { readOnly: true },
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
access: {
|
|
111
|
+
create: ({ req }) => req.user?.collection === 'users',
|
|
112
|
+
read: ({ req }) => req.user?.collection === 'users',
|
|
113
|
+
update: ({ req }) => req.user?.collection === 'users',
|
|
114
|
+
delete: ({ req }) => req.user?.collection === 'users',
|
|
115
|
+
},
|
|
116
|
+
timestamps: true,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -343,6 +343,17 @@ function createFireTicketWebhooks(slugs: CollectionSlugs): CollectionAfterChange
|
|
|
343
343
|
subject: doc.subject,
|
|
344
344
|
previousStatus: previousDoc.status,
|
|
345
345
|
})
|
|
346
|
+
|
|
347
|
+
// Invalidate client summary cache so it refreshes on next view
|
|
348
|
+
const clientId = typeof doc.client === 'object' ? doc.client?.id : doc.client
|
|
349
|
+
if (clientId) {
|
|
350
|
+
payload.update({
|
|
351
|
+
collection: 'client-summaries',
|
|
352
|
+
where: { client: { equals: clientId } },
|
|
353
|
+
data: { generatedAt: new Date(0).toISOString() }, // Force cache expiry
|
|
354
|
+
overrideAccess: true,
|
|
355
|
+
}).catch(() => { /* silent — collection might not exist yet */ })
|
|
356
|
+
}
|
|
346
357
|
}
|
|
347
358
|
|
|
348
359
|
// ticket_assigned
|
|
@@ -458,6 +469,37 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
458
469
|
|
|
459
470
|
// Billing collapsible fields
|
|
460
471
|
const billingFields: Field[] = [
|
|
472
|
+
{
|
|
473
|
+
type: 'row',
|
|
474
|
+
fields: [
|
|
475
|
+
{
|
|
476
|
+
name: 'billingType',
|
|
477
|
+
type: 'select',
|
|
478
|
+
label: 'Type de facturation',
|
|
479
|
+
defaultValue: 'hourly',
|
|
480
|
+
options: [
|
|
481
|
+
{ label: 'Au temps passé', value: 'hourly' },
|
|
482
|
+
{ label: 'Forfait', value: 'flat' },
|
|
483
|
+
],
|
|
484
|
+
admin: { width: '33%' },
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: 'flatRateAmount',
|
|
488
|
+
type: 'number',
|
|
489
|
+
label: 'Montant forfait (EUR)',
|
|
490
|
+
admin: {
|
|
491
|
+
width: '33%',
|
|
492
|
+
condition: (data) => data?.billingType === 'flat',
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: 'billedAmount',
|
|
497
|
+
type: 'number',
|
|
498
|
+
label: 'Montant facturé (EUR)',
|
|
499
|
+
admin: { width: '33%' },
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
},
|
|
461
503
|
{
|
|
462
504
|
type: 'row',
|
|
463
505
|
fields: [
|
|
@@ -498,15 +540,9 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
498
540
|
{
|
|
499
541
|
name: 'paidAt',
|
|
500
542
|
type: 'date',
|
|
501
|
-
label: '
|
|
543
|
+
label: 'Payé le',
|
|
502
544
|
admin: { width: '33%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } },
|
|
503
545
|
},
|
|
504
|
-
{
|
|
505
|
-
name: 'billedAmount',
|
|
506
|
-
type: 'number',
|
|
507
|
-
label: 'Montant facture (EUR)',
|
|
508
|
-
admin: { width: '33%' },
|
|
509
|
-
},
|
|
510
546
|
],
|
|
511
547
|
},
|
|
512
548
|
]
|
|
@@ -651,7 +687,17 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
651
687
|
{ name: 'snoozeUntil', type: 'date', label: 'Snooze jusqu\'au', admin: { position: 'sidebar', date: { pickerAppearance: 'dayAndTime', displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
652
688
|
// Billing sidebar
|
|
653
689
|
{ name: 'billable', type: 'checkbox', defaultValue: true, label: 'Facturable', admin: { position: 'sidebar' } },
|
|
654
|
-
{
|
|
690
|
+
{
|
|
691
|
+
name: 'showTimeToClient',
|
|
692
|
+
type: 'checkbox',
|
|
693
|
+
defaultValue: true,
|
|
694
|
+
label: 'Afficher le temps au client',
|
|
695
|
+
admin: {
|
|
696
|
+
position: 'sidebar',
|
|
697
|
+
description: 'Auto-désactivé en mode forfait',
|
|
698
|
+
condition: (data) => data?.billingType !== 'flat',
|
|
699
|
+
},
|
|
700
|
+
},
|
|
655
701
|
{ name: 'totalTimeMinutes', type: 'number', defaultValue: 0, label: 'Temps total (minutes)', admin: { readOnly: true, position: 'sidebar' } },
|
|
656
702
|
],
|
|
657
703
|
hooks: {
|
package/src/collections/index.ts
CHANGED
|
@@ -14,3 +14,4 @@ export { createWebhookEndpointsCollection } from './WebhookEndpoints'
|
|
|
14
14
|
export { createSlaPoliciesCollection } from './SlaPolicies'
|
|
15
15
|
export { createMacrosCollection } from './Macros'
|
|
16
16
|
export { createTicketStatusesCollection } from './TicketStatuses'
|
|
17
|
+
export { createClientSummariesCollection } from './ClientSummaries'
|