@consilioweb/payload-support 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -308,6 +308,210 @@ ${text}`;
308
308
  };
309
309
  }
310
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
+
311
515
  // src/endpoints/search.ts
312
516
  function createSearchEndpoint(slugs) {
313
517
  return {
@@ -4759,7 +4963,10 @@ function createSupportEndpoints(slugs, options) {
4759
4963
  createUserPrefsGetEndpoint(slugs),
4760
4964
  createUserPrefsPostEndpoint(slugs)
4761
4965
  ];
4762
- if (!f || f.ai !== false) endpoints.push(createAiEndpoint(slugs));
4966
+ if (!f || f.ai !== false) {
4967
+ endpoints.push(createAiEndpoint(slugs));
4968
+ endpoints.push(...createClientIntelligenceEndpoint(slugs));
4969
+ }
4763
4970
  if (!f || f.bulkActions !== false) endpoints.push(createBulkActionEndpoint(slugs));
4764
4971
  if (!f || f.merge !== false) endpoints.push(createMergeTicketsEndpoint(slugs));
4765
4972
  if (!f || f.splitTicket !== false) endpoints.push(createSplitTicketEndpoint(slugs));
@@ -5587,6 +5794,17 @@ function createFireTicketWebhooks(slugs) {
5587
5794
  subject: doc.subject,
5588
5795
  previousStatus: previousDoc.status
5589
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
+ }
5590
5808
  }
5591
5809
  const oldAssigned = typeof previousDoc.assignedTo === "object" ? previousDoc.assignedTo?.id : previousDoc.assignedTo;
5592
5810
  const newAssigned = typeof doc.assignedTo === "object" ? doc.assignedTo?.id : doc.assignedTo;
@@ -7685,6 +7903,119 @@ function createTicketStatusesCollection(slugs) {
7685
7903
  };
7686
7904
  }
7687
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
+
7688
8019
  // src/plugin.ts
7689
8020
  function viewConfig(component, path) {
7690
8021
  return { Component: component, path };
@@ -7729,6 +8060,7 @@ function supportPlugin(config) {
7729
8060
  if (features.customStatuses !== false) supportCollections.push(createTicketStatusesCollection(slugs));
7730
8061
  if (features.chat) supportCollections.push(createChatMessagesCollection(slugs));
7731
8062
  if (features.pendingEmails) supportCollections.push(createPendingEmailsCollection(slugs));
8063
+ if (features.ai !== false) supportCollections.push(createClientSummariesCollection(slugs));
7732
8064
  const existingViews = incomingConfig.admin?.components?.views || {};
7733
8065
  const supportViews = {
7734
8066
  "support-inbox": viewConfig(`${viewsBase}#TicketInboxView`, `${bp}/inbox`),
@@ -31,6 +31,9 @@ const CrmClient = () => {
31
31
  const [detailLoading, setDetailLoading] = react.useState(false);
32
32
  const [showMerge, setShowMerge] = react.useState(false);
33
33
  const [mergeSearch, setMergeSearch] = react.useState("");
34
+ const [intelligence, setIntelligence] = react.useState(null);
35
+ const [intelLoading, setIntelLoading] = react.useState(false);
36
+ const [intelRefreshing, setIntelRefreshing] = react.useState(false);
34
37
  const [mergeResults, setMergeResults] = react.useState([]);
35
38
  const [merging, setMerging] = react.useState(false);
36
39
  const [mergeSuccess, setMergeSuccess] = react.useState("");
@@ -77,11 +80,28 @@ const CrmClient = () => {
77
80
  }
78
81
  setDetailLoading(false);
79
82
  }, []);
83
+ const fetchIntelligence = react.useCallback(async (clientId, force = false) => {
84
+ if (force) setIntelRefreshing(true);
85
+ else setIntelLoading(true);
86
+ try {
87
+ const method = force ? "POST" : "GET";
88
+ const url = force ? "/api/support/client-intelligence" : `/api/support/client-intelligence?clientId=${clientId}`;
89
+ const opts = { method, credentials: "include", headers: { "Content-Type": "application/json" } };
90
+ if (force) opts.body = JSON.stringify({ clientId });
91
+ const res = await fetch(url, opts);
92
+ if (res.ok) setIntelligence(await res.json());
93
+ } catch {
94
+ }
95
+ setIntelLoading(false);
96
+ setIntelRefreshing(false);
97
+ }, []);
80
98
  const selectClient = (id) => {
81
99
  setSelectedId(id);
82
100
  fetchDetail(id);
101
+ fetchIntelligence(id);
83
102
  setShowMerge(false);
84
103
  setMergeSuccess("");
104
+ setIntelligence(null);
85
105
  };
86
106
  react.useEffect(() => {
87
107
  if (!mergeSearch || mergeSearch.length < 2) {
@@ -203,6 +223,63 @@ const CrmClient = () => {
203
223
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: "var(--theme-elevation-500)", marginBottom: 2 }, children: stat.label }),
204
224
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 18, fontWeight: 700, color: "var(--theme-text)" }, children: stat.value })
205
225
  ] }, stat.label)) }),
226
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: 16, borderRadius: 10, border: "1px solid var(--theme-elevation-150)", marginBottom: 16, background: "linear-gradient(135deg, rgba(37,99,235,0.03) 0%, rgba(139,92,246,0.03) 100%)" }, children: [
227
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
228
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { style: { fontSize: 14, fontWeight: 700, margin: 0, display: "flex", alignItems: "center", gap: 6 }, children: [
229
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 16 }, children: "\u{1F9E0}" }),
230
+ " R\xE9sum\xE9 IA"
231
+ ] }),
232
+ /* @__PURE__ */ jsxRuntime.jsx(
233
+ "button",
234
+ {
235
+ onClick: () => selectedId && fetchIntelligence(selectedId, true),
236
+ disabled: intelRefreshing,
237
+ style: { padding: "4px 10px", borderRadius: 6, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", fontSize: 11, fontWeight: 600, cursor: "pointer", color: "var(--theme-text)" },
238
+ children: intelRefreshing ? "\u23F3 G\xE9n\xE9ration..." : "\u{1F504} Actualiser"
239
+ }
240
+ )
241
+ ] }),
242
+ intelLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 20, textAlign: "center", color: "var(--theme-elevation-400)", fontSize: 13 }, children: "Chargement du r\xE9sum\xE9..." }) : intelligence ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
243
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0, fontSize: 13, lineHeight: 1.6, color: "var(--theme-text)" }, children: intelligence.summary }),
244
+ intelligence.recurringTopics && intelligence.recurringTopics.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
245
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, color: "var(--theme-elevation-500)", marginBottom: 6, textTransform: "uppercase" }, children: "Sujets r\xE9currents" }),
246
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: intelligence.recurringTopics.map((t2, i) => /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { padding: "3px 10px", borderRadius: 12, background: "rgba(37,99,235,0.08)", color: "#2563eb", fontSize: 11, fontWeight: 600 }, children: [
247
+ t2.topic,
248
+ " (",
249
+ t2.count,
250
+ "x)"
251
+ ] }, i)) })
252
+ ] }),
253
+ intelligence.patterns && intelligence.patterns.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
254
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, color: "var(--theme-elevation-500)", marginBottom: 6, textTransform: "uppercase" }, children: "Patterns d\xE9tect\xE9s" }),
255
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { style: { margin: 0, paddingLeft: 18, fontSize: 12, color: "var(--theme-text)", lineHeight: 1.8 }, children: intelligence.patterns.map((p, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: p }, i)) })
256
+ ] }),
257
+ intelligence.keyFacts && intelligence.keyFacts.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
258
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, color: "var(--theme-elevation-500)", marginBottom: 6, textTransform: "uppercase" }, children: "Faits cl\xE9s" }),
259
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: intelligence.keyFacts.map((f, i) => /* @__PURE__ */ jsxRuntime.jsx("span", { style: { padding: "3px 10px", borderRadius: 12, background: "rgba(22,163,74,0.08)", color: "#16a34a", fontSize: 11, fontWeight: 600 }, children: f }, i)) })
260
+ ] }),
261
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 10, color: "var(--theme-elevation-400)", display: "flex", gap: 12, marginTop: 4 }, children: [
262
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
263
+ intelligence.ticketCount,
264
+ " tickets analys\xE9s"
265
+ ] }),
266
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
267
+ intelligence.messageCount,
268
+ " messages"
269
+ ] }),
270
+ intelligence.averageSatisfaction && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
271
+ "Satisfaction: ",
272
+ intelligence.averageSatisfaction,
273
+ "/5"
274
+ ] }),
275
+ intelligence.fromCache && /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Cache" }),
276
+ intelligence.generatedAt && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
277
+ "G\xE9n\xE9r\xE9 ",
278
+ timeAgo(intelligence.generatedAt)
279
+ ] })
280
+ ] })
281
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 16, textAlign: "center", color: "var(--theme-elevation-400)", fontSize: 13 }, children: 'Cliquez sur "Actualiser" pour g\xE9n\xE9rer le r\xE9sum\xE9 IA de ce client.' })
282
+ ] }),
206
283
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: 16, borderRadius: 10, border: "1px solid var(--theme-elevation-150)", marginBottom: 16 }, children: [
207
284
  /* @__PURE__ */ jsxRuntime.jsxs("h3", { style: { fontSize: 14, fontWeight: 700, margin: "0 0 8px" }, children: [
208
285
  t("crm.sections.tickets"),
@@ -30,6 +30,9 @@ const CrmClient = () => {
30
30
  const [detailLoading, setDetailLoading] = useState(false);
31
31
  const [showMerge, setShowMerge] = useState(false);
32
32
  const [mergeSearch, setMergeSearch] = useState("");
33
+ const [intelligence, setIntelligence] = useState(null);
34
+ const [intelLoading, setIntelLoading] = useState(false);
35
+ const [intelRefreshing, setIntelRefreshing] = useState(false);
33
36
  const [mergeResults, setMergeResults] = useState([]);
34
37
  const [merging, setMerging] = useState(false);
35
38
  const [mergeSuccess, setMergeSuccess] = useState("");
@@ -76,11 +79,28 @@ const CrmClient = () => {
76
79
  }
77
80
  setDetailLoading(false);
78
81
  }, []);
82
+ const fetchIntelligence = useCallback(async (clientId, force = false) => {
83
+ if (force) setIntelRefreshing(true);
84
+ else setIntelLoading(true);
85
+ try {
86
+ const method = force ? "POST" : "GET";
87
+ const url = force ? "/api/support/client-intelligence" : `/api/support/client-intelligence?clientId=${clientId}`;
88
+ const opts = { method, credentials: "include", headers: { "Content-Type": "application/json" } };
89
+ if (force) opts.body = JSON.stringify({ clientId });
90
+ const res = await fetch(url, opts);
91
+ if (res.ok) setIntelligence(await res.json());
92
+ } catch {
93
+ }
94
+ setIntelLoading(false);
95
+ setIntelRefreshing(false);
96
+ }, []);
79
97
  const selectClient = (id) => {
80
98
  setSelectedId(id);
81
99
  fetchDetail(id);
100
+ fetchIntelligence(id);
82
101
  setShowMerge(false);
83
102
  setMergeSuccess("");
103
+ setIntelligence(null);
84
104
  };
85
105
  useEffect(() => {
86
106
  if (!mergeSearch || mergeSearch.length < 2) {
@@ -202,6 +222,63 @@ const CrmClient = () => {
202
222
  /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "var(--theme-elevation-500)", marginBottom: 2 }, children: stat.label }),
203
223
  /* @__PURE__ */ jsx("div", { style: { fontSize: 18, fontWeight: 700, color: "var(--theme-text)" }, children: stat.value })
204
224
  ] }, stat.label)) }),
225
+ /* @__PURE__ */ jsxs("div", { style: { padding: 16, borderRadius: 10, border: "1px solid var(--theme-elevation-150)", marginBottom: 16, background: "linear-gradient(135deg, rgba(37,99,235,0.03) 0%, rgba(139,92,246,0.03) 100%)" }, children: [
226
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
227
+ /* @__PURE__ */ jsxs("h3", { style: { fontSize: 14, fontWeight: 700, margin: 0, display: "flex", alignItems: "center", gap: 6 }, children: [
228
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 16 }, children: "\u{1F9E0}" }),
229
+ " R\xE9sum\xE9 IA"
230
+ ] }),
231
+ /* @__PURE__ */ jsx(
232
+ "button",
233
+ {
234
+ onClick: () => selectedId && fetchIntelligence(selectedId, true),
235
+ disabled: intelRefreshing,
236
+ style: { padding: "4px 10px", borderRadius: 6, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", fontSize: 11, fontWeight: 600, cursor: "pointer", color: "var(--theme-text)" },
237
+ children: intelRefreshing ? "\u23F3 G\xE9n\xE9ration..." : "\u{1F504} Actualiser"
238
+ }
239
+ )
240
+ ] }),
241
+ intelLoading ? /* @__PURE__ */ jsx("div", { style: { padding: 20, textAlign: "center", color: "var(--theme-elevation-400)", fontSize: 13 }, children: "Chargement du r\xE9sum\xE9..." }) : intelligence ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
242
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 13, lineHeight: 1.6, color: "var(--theme-text)" }, children: intelligence.summary }),
243
+ intelligence.recurringTopics && intelligence.recurringTopics.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
244
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, color: "var(--theme-elevation-500)", marginBottom: 6, textTransform: "uppercase" }, children: "Sujets r\xE9currents" }),
245
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: intelligence.recurringTopics.map((t2, i) => /* @__PURE__ */ jsxs("span", { style: { padding: "3px 10px", borderRadius: 12, background: "rgba(37,99,235,0.08)", color: "#2563eb", fontSize: 11, fontWeight: 600 }, children: [
246
+ t2.topic,
247
+ " (",
248
+ t2.count,
249
+ "x)"
250
+ ] }, i)) })
251
+ ] }),
252
+ intelligence.patterns && intelligence.patterns.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
253
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, color: "var(--theme-elevation-500)", marginBottom: 6, textTransform: "uppercase" }, children: "Patterns d\xE9tect\xE9s" }),
254
+ /* @__PURE__ */ jsx("ul", { style: { margin: 0, paddingLeft: 18, fontSize: 12, color: "var(--theme-text)", lineHeight: 1.8 }, children: intelligence.patterns.map((p, i) => /* @__PURE__ */ jsx("li", { children: p }, i)) })
255
+ ] }),
256
+ intelligence.keyFacts && intelligence.keyFacts.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
257
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, color: "var(--theme-elevation-500)", marginBottom: 6, textTransform: "uppercase" }, children: "Faits cl\xE9s" }),
258
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: intelligence.keyFacts.map((f, i) => /* @__PURE__ */ jsx("span", { style: { padding: "3px 10px", borderRadius: 12, background: "rgba(22,163,74,0.08)", color: "#16a34a", fontSize: 11, fontWeight: 600 }, children: f }, i)) })
259
+ ] }),
260
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 10, color: "var(--theme-elevation-400)", display: "flex", gap: 12, marginTop: 4 }, children: [
261
+ /* @__PURE__ */ jsxs("span", { children: [
262
+ intelligence.ticketCount,
263
+ " tickets analys\xE9s"
264
+ ] }),
265
+ /* @__PURE__ */ jsxs("span", { children: [
266
+ intelligence.messageCount,
267
+ " messages"
268
+ ] }),
269
+ intelligence.averageSatisfaction && /* @__PURE__ */ jsxs("span", { children: [
270
+ "Satisfaction: ",
271
+ intelligence.averageSatisfaction,
272
+ "/5"
273
+ ] }),
274
+ intelligence.fromCache && /* @__PURE__ */ jsx("span", { children: "Cache" }),
275
+ intelligence.generatedAt && /* @__PURE__ */ jsxs("span", { children: [
276
+ "G\xE9n\xE9r\xE9 ",
277
+ timeAgo(intelligence.generatedAt)
278
+ ] })
279
+ ] })
280
+ ] }) : /* @__PURE__ */ jsx("div", { style: { padding: 16, textAlign: "center", color: "var(--theme-elevation-400)", fontSize: 13 }, children: 'Cliquez sur "Actualiser" pour g\xE9n\xE9rer le r\xE9sum\xE9 IA de ce client.' })
281
+ ] }),
205
282
  /* @__PURE__ */ jsxs("div", { style: { padding: 16, borderRadius: 10, border: "1px solid var(--theme-elevation-150)", marginBottom: 16 }, children: [
206
283
  /* @__PURE__ */ jsxs("h3", { style: { fontSize: 14, fontWeight: 700, margin: "0 0 8px" }, children: [
207
284
  t("crm.sections.tickets"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-support",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
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",