@consilioweb/payload-support 0.9.10 → 0.9.12
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/hooks/useAI.cjs +24 -5
- package/dist/components/TicketConversation/hooks/useAI.js +24 -5
- package/dist/index.cjs +301 -13
- package/dist/index.js +301 -13
- package/dist/styles/BillingView.module.scss +132 -0
- package/dist/views/BillingView/client.cjs +287 -170
- package/dist/views/BillingView/client.js +287 -170
- package/dist/views/TicketDetailView/client.cjs +28 -6
- package/dist/views/TicketDetailView/client.js +28 -6
- package/package.json +1 -1
- package/src/collections/Tickets.ts +78 -0
- package/src/components/TicketConversation/hooks/useAI.ts +29 -5
- package/src/endpoints/ai.ts +13 -8
- package/src/endpoints/billing.ts +70 -9
- package/src/endpoints/index.ts +3 -0
- package/src/endpoints/ticket-synthesis.ts +67 -0
- package/src/styles/BillingView.module.scss +132 -0
- package/src/utils/generateTicketSynthesis.ts +178 -0
- package/src/views/BillingView/client.tsx +234 -102
- package/src/views/TicketDetailView/client.tsx +24 -6
package/dist/index.js
CHANGED
|
@@ -277,15 +277,20 @@ R\xE9dige une r\xE9ponse appropri\xE9e au dernier message du client. Sois concis
|
|
|
277
277
|
const { text, style } = body;
|
|
278
278
|
if (!text?.trim()) return Response.json({ error: "text required" }, { status: 400 });
|
|
279
279
|
const styleInstructions = {
|
|
280
|
-
auto: "Garde le
|
|
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:
|
|
284
|
-
court: "
|
|
285
|
-
amical:
|
|
280
|
+
auto: { tone: "Garde le ton actuel (neutre professionnel).", person: "IMPORTANT: Pr\xE9serve EXACTEMENT le tutoiement ou vouvoiement du texte original." },
|
|
281
|
+
tutoyer: { tone: "Ton d\xE9contract\xE9 et direct, mais correct.", person: 'CRITIQUE: Utilise IMP\xC9RATIVEMENT le tutoiement partout (tu, ton, te, toi). Si le texte vouvoie, convertis TOUT en tutoiement. Exemple: "vous pouvez" \u2192 "tu peux", "votre" \u2192 "ton".' },
|
|
282
|
+
vouvoyer: { tone: "Ton neutre et poli.", person: "CRITIQUE: Utilise IMP\xC9RATIVEMENT le vouvoiement partout (vous, votre, etc). Si le texte tutoie, convertis TOUT en vouvoiement." },
|
|
283
|
+
formel: { tone: "Ton formel et institutionnel.", person: "Utilise le vouvoiement partout." },
|
|
284
|
+
court: { tone: "Style concis et direct, phrases courtes, aller \xE0 l'essentiel.", person: "Pr\xE9serve le tutoiement/vouvoiement du texte original." },
|
|
285
|
+
amical: { tone: "Ton chaleureux, amical, sympathique, avec un peu de cordialit\xE9.", person: "CRITIQUE: Utilise IMP\xC9RATIVEMENT le tutoiement partout. Convertis le vouvoiement en tutoiement." }
|
|
286
286
|
};
|
|
287
287
|
const styleGuide = styleInstructions[style || "auto"] || styleInstructions.auto;
|
|
288
|
-
const prompt = `
|
|
288
|
+
const prompt = `Reformule le texte ci-dessous en corrigeant les fautes d'orthographe/grammaire. Ne change pas le fond du message, am\xE9liore uniquement la forme.
|
|
289
|
+
|
|
290
|
+
TON REQUIS: ${styleGuide.tone}
|
|
291
|
+
FORME REQUISE: ${styleGuide.person}
|
|
292
|
+
|
|
293
|
+
R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
|
|
289
294
|
|
|
290
295
|
Texte original :
|
|
291
296
|
${text}`;
|
|
@@ -2964,9 +2969,36 @@ function createBillingEndpoint(slugs) {
|
|
|
2964
2969
|
return Response.json({ error: "Missing from/to params" }, { status: 400 });
|
|
2965
2970
|
}
|
|
2966
2971
|
const projectId = url.searchParams.get("projectId");
|
|
2972
|
+
const toExclusive = new Date(to);
|
|
2973
|
+
toExclusive.setDate(toExclusive.getDate() + 1);
|
|
2974
|
+
const toExclusiveIso = toExclusive.toISOString();
|
|
2967
2975
|
const ticketWhere = {
|
|
2968
|
-
|
|
2969
|
-
|
|
2976
|
+
and: [
|
|
2977
|
+
{ billable: { equals: true } },
|
|
2978
|
+
...projectId ? [{ project: { equals: Number(projectId) } }] : [],
|
|
2979
|
+
{
|
|
2980
|
+
or: [
|
|
2981
|
+
{
|
|
2982
|
+
and: [
|
|
2983
|
+
{ updatedAt: { greater_than_equal: from } },
|
|
2984
|
+
{ updatedAt: { less_than: toExclusiveIso } }
|
|
2985
|
+
]
|
|
2986
|
+
},
|
|
2987
|
+
{
|
|
2988
|
+
and: [
|
|
2989
|
+
{ createdAt: { greater_than_equal: from } },
|
|
2990
|
+
{ createdAt: { less_than: toExclusiveIso } }
|
|
2991
|
+
]
|
|
2992
|
+
},
|
|
2993
|
+
{
|
|
2994
|
+
and: [
|
|
2995
|
+
{ resolvedAt: { greater_than_equal: from } },
|
|
2996
|
+
{ resolvedAt: { less_than: toExclusiveIso } }
|
|
2997
|
+
]
|
|
2998
|
+
}
|
|
2999
|
+
]
|
|
3000
|
+
}
|
|
3001
|
+
]
|
|
2970
3002
|
};
|
|
2971
3003
|
const allTickets = [];
|
|
2972
3004
|
let ticketPage = 1;
|
|
@@ -3019,8 +3051,8 @@ function createBillingEndpoint(slugs) {
|
|
|
3019
3051
|
const projectGroups = /* @__PURE__ */ new Map();
|
|
3020
3052
|
for (const ticket of allTickets) {
|
|
3021
3053
|
const t = ticket;
|
|
3022
|
-
const ticketEntries = entriesByTicket.get(t.id);
|
|
3023
|
-
|
|
3054
|
+
const ticketEntries = entriesByTicket.get(t.id) || [];
|
|
3055
|
+
const hasNoTimeEntries = ticketEntries.length === 0;
|
|
3024
3056
|
const project = typeof t.project === "object" && t.project ? { id: t.project.id, name: t.project.name || "Sans nom" } : null;
|
|
3025
3057
|
const projectKey = project ? String(project.id) : "no-project";
|
|
3026
3058
|
if (!projectGroups.has(projectKey)) {
|
|
@@ -3048,9 +3080,14 @@ function createBillingEndpoint(slugs) {
|
|
|
3048
3080
|
id: t.id,
|
|
3049
3081
|
ticketNumber: t.ticketNumber || "",
|
|
3050
3082
|
subject: t.subject || "",
|
|
3083
|
+
status: t.status || "",
|
|
3051
3084
|
entries: ticketEntries,
|
|
3052
3085
|
totalMinutes: ticketTotalMinutes,
|
|
3053
|
-
billedAmount
|
|
3086
|
+
billedAmount,
|
|
3087
|
+
hasNoTimeEntries,
|
|
3088
|
+
aiSummary: t.aiSummary || null,
|
|
3089
|
+
aiSummaryGeneratedAt: t.aiSummaryGeneratedAt || null,
|
|
3090
|
+
aiSummaryStatus: t.aiSummaryStatus || null
|
|
3054
3091
|
});
|
|
3055
3092
|
projectGroups.get(projectKey).totalMinutes += ticketTotalMinutes;
|
|
3056
3093
|
if (billedAmount) projectGroups.get(projectKey).totalBilledAmount += billedAmount;
|
|
@@ -3058,7 +3095,16 @@ function createBillingEndpoint(slugs) {
|
|
|
3058
3095
|
const groups = Array.from(projectGroups.values());
|
|
3059
3096
|
const grandTotalMinutes = groups.reduce((sum, g) => sum + g.totalMinutes, 0);
|
|
3060
3097
|
const grandTotalBilledAmount = groups.reduce((sum, g) => sum + g.totalBilledAmount, 0);
|
|
3061
|
-
|
|
3098
|
+
const ticketsWithoutTime = groups.reduce(
|
|
3099
|
+
(sum, g) => sum + g.tickets.filter((t) => t.hasNoTimeEntries).length,
|
|
3100
|
+
0
|
|
3101
|
+
);
|
|
3102
|
+
return new Response(JSON.stringify({
|
|
3103
|
+
groups,
|
|
3104
|
+
grandTotalMinutes,
|
|
3105
|
+
grandTotalBilledAmount,
|
|
3106
|
+
ticketsWithoutTime
|
|
3107
|
+
}), {
|
|
3062
3108
|
headers: {
|
|
3063
3109
|
"Content-Type": "application/json",
|
|
3064
3110
|
"Cache-Control": "private, max-age=300, stale-while-revalidate=600"
|
|
@@ -3074,6 +3120,177 @@ function createBillingEndpoint(slugs) {
|
|
|
3074
3120
|
};
|
|
3075
3121
|
}
|
|
3076
3122
|
|
|
3123
|
+
// src/utils/generateTicketSynthesis.ts
|
|
3124
|
+
function getClient3(aiSettings) {
|
|
3125
|
+
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
3126
|
+
if (aiSettings.provider === "ollama") {
|
|
3127
|
+
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
3128
|
+
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
3129
|
+
}
|
|
3130
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
3131
|
+
}
|
|
3132
|
+
function getModel3(aiSettings) {
|
|
3133
|
+
return aiSettings.model || "claude-haiku-4-5-20251001";
|
|
3134
|
+
}
|
|
3135
|
+
async function generateTicketSynthesis(args) {
|
|
3136
|
+
const { payload, slugs, ticketId } = args;
|
|
3137
|
+
const settings = await readSupportSettings(payload);
|
|
3138
|
+
if (settings.ai.enableSynthesis === false) {
|
|
3139
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "skipped", reason: "synthesis disabled" };
|
|
3140
|
+
}
|
|
3141
|
+
const ticket = await payload.findByID({
|
|
3142
|
+
collection: slugs.tickets,
|
|
3143
|
+
id: ticketId,
|
|
3144
|
+
depth: 1,
|
|
3145
|
+
overrideAccess: true
|
|
3146
|
+
});
|
|
3147
|
+
if (!ticket) {
|
|
3148
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "error", reason: "ticket not found" };
|
|
3149
|
+
}
|
|
3150
|
+
await payload.update({
|
|
3151
|
+
collection: slugs.tickets,
|
|
3152
|
+
id: ticketId,
|
|
3153
|
+
data: { aiSummaryStatus: "pending" },
|
|
3154
|
+
overrideAccess: true
|
|
3155
|
+
}).catch(() => {
|
|
3156
|
+
});
|
|
3157
|
+
const messagesResult = await payload.find({
|
|
3158
|
+
collection: slugs.ticketMessages,
|
|
3159
|
+
where: { ticket: { equals: ticketId } },
|
|
3160
|
+
sort: "createdAt",
|
|
3161
|
+
limit: 500,
|
|
3162
|
+
depth: 0,
|
|
3163
|
+
overrideAccess: true
|
|
3164
|
+
});
|
|
3165
|
+
const messages = messagesResult.docs;
|
|
3166
|
+
if (messages.length === 0) {
|
|
3167
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3168
|
+
await payload.update({
|
|
3169
|
+
collection: slugs.tickets,
|
|
3170
|
+
id: ticketId,
|
|
3171
|
+
data: {
|
|
3172
|
+
aiSummary: "(Aucun message dans ce ticket)",
|
|
3173
|
+
aiSummaryGeneratedAt: generatedAt,
|
|
3174
|
+
aiSummaryStatus: "done"
|
|
3175
|
+
},
|
|
3176
|
+
overrideAccess: true
|
|
3177
|
+
});
|
|
3178
|
+
return { summary: "(Aucun message dans ce ticket)", generatedAt, status: "done" };
|
|
3179
|
+
}
|
|
3180
|
+
const conversation = messages.map((m) => {
|
|
3181
|
+
const author = m.authorType === "admin" ? "Support" : "Client";
|
|
3182
|
+
const date = m.createdAt ? new Date(m.createdAt).toLocaleDateString("fr-FR", {
|
|
3183
|
+
day: "numeric",
|
|
3184
|
+
month: "short",
|
|
3185
|
+
hour: "2-digit",
|
|
3186
|
+
minute: "2-digit",
|
|
3187
|
+
timeZone: "Europe/Paris"
|
|
3188
|
+
}) : "";
|
|
3189
|
+
return `[${date}] ${author}: ${m.body || ""}`;
|
|
3190
|
+
}).join("\n\n");
|
|
3191
|
+
const clientObj = typeof ticket.client === "object" && ticket.client ? ticket.client : null;
|
|
3192
|
+
const clientCompany = clientObj?.company || "";
|
|
3193
|
+
const clientName = clientObj ? [clientObj.firstName, clientObj.lastName].filter(Boolean).join(" ") : "";
|
|
3194
|
+
const prompt = `Tu es un consultant technique qui prepare un recap factuel pour une facturation client.
|
|
3195
|
+
|
|
3196
|
+
Sujet du ticket : ${ticket.subject || "(sans sujet)"}
|
|
3197
|
+
Client : ${clientName || "Inconnu"}${clientCompany ? ` \u2014 ${clientCompany}` : ""}
|
|
3198
|
+
|
|
3199
|
+
Conversation complete du ticket :
|
|
3200
|
+
${conversation}
|
|
3201
|
+
|
|
3202
|
+
Genere un recap factuel sous forme d'une liste a puces courtes et actionnables, decrivant CE QUI A ETE FAIT cote support pendant ce ticket. C'est destine a etre colle dans un devis ou une facture.
|
|
3203
|
+
|
|
3204
|
+
Regles strictes :
|
|
3205
|
+
- Une puce = une action realisee, formulee en groupe nominal court (ex : "Diagnostic configuration DNS et authentification Mailchimp")
|
|
3206
|
+
- Pas de phrases completes, pas de "j'ai fait", pas de pronoms
|
|
3207
|
+
- Pas de salutations, pas d'introduction, pas de conclusion
|
|
3208
|
+
- Pas de markdown autre que les puces "- "
|
|
3209
|
+
- 5 a 10 puces maximum, ordonnees chronologiquement
|
|
3210
|
+
- Ne mentionne PAS le client par son nom dans les puces
|
|
3211
|
+
- Si le ticket n'a pas abouti, decris quand meme le travail d'analyse realise
|
|
3212
|
+
|
|
3213
|
+
Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
3214
|
+
const anthropic = getClient3(settings.ai);
|
|
3215
|
+
const model = getModel3(settings.ai);
|
|
3216
|
+
try {
|
|
3217
|
+
const res = await anthropic.messages.create({
|
|
3218
|
+
model,
|
|
3219
|
+
max_tokens: 600,
|
|
3220
|
+
messages: [{ role: "user", content: prompt }]
|
|
3221
|
+
});
|
|
3222
|
+
const summary = res.content[0]?.type === "text" ? res.content[0].text.trim() : "";
|
|
3223
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3224
|
+
await payload.update({
|
|
3225
|
+
collection: slugs.tickets,
|
|
3226
|
+
id: ticketId,
|
|
3227
|
+
data: {
|
|
3228
|
+
aiSummary: summary,
|
|
3229
|
+
aiSummaryGeneratedAt: generatedAt,
|
|
3230
|
+
aiSummaryStatus: "done"
|
|
3231
|
+
},
|
|
3232
|
+
overrideAccess: true
|
|
3233
|
+
});
|
|
3234
|
+
return { summary, generatedAt, status: "done" };
|
|
3235
|
+
} catch (err) {
|
|
3236
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3237
|
+
await payload.update({
|
|
3238
|
+
collection: slugs.tickets,
|
|
3239
|
+
id: ticketId,
|
|
3240
|
+
data: { aiSummaryStatus: "error" },
|
|
3241
|
+
overrideAccess: true
|
|
3242
|
+
}).catch(() => {
|
|
3243
|
+
});
|
|
3244
|
+
return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "error", reason: message };
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
// src/endpoints/ticket-synthesis.ts
|
|
3249
|
+
function createTicketSynthesisEndpoint(slugs) {
|
|
3250
|
+
return {
|
|
3251
|
+
path: "/support/ticket-synthesis",
|
|
3252
|
+
method: "post",
|
|
3253
|
+
handler: async (req) => {
|
|
3254
|
+
try {
|
|
3255
|
+
requireAdmin(req, slugs);
|
|
3256
|
+
const payload = req.payload;
|
|
3257
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
3258
|
+
const ticketIdRaw = url.searchParams.get("ticketId");
|
|
3259
|
+
const force = url.searchParams.get("force") === "true";
|
|
3260
|
+
if (!ticketIdRaw) {
|
|
3261
|
+
return Response.json({ error: "ticketId required" }, { status: 400 });
|
|
3262
|
+
}
|
|
3263
|
+
const ticketId = Number(ticketIdRaw);
|
|
3264
|
+
if (Number.isNaN(ticketId)) {
|
|
3265
|
+
return Response.json({ error: "ticketId must be a number" }, { status: 400 });
|
|
3266
|
+
}
|
|
3267
|
+
if (!force) {
|
|
3268
|
+
const existing = await payload.findByID({
|
|
3269
|
+
collection: slugs.tickets,
|
|
3270
|
+
id: ticketId,
|
|
3271
|
+
depth: 0,
|
|
3272
|
+
overrideAccess: true
|
|
3273
|
+
});
|
|
3274
|
+
if (existing?.aiSummary && existing.aiSummaryStatus === "done") {
|
|
3275
|
+
return Response.json({
|
|
3276
|
+
summary: existing.aiSummary,
|
|
3277
|
+
generatedAt: existing.aiSummaryGeneratedAt,
|
|
3278
|
+
status: "cached"
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
const result = await generateTicketSynthesis({ payload, slugs, ticketId });
|
|
3283
|
+
return Response.json(result);
|
|
3284
|
+
} catch (err) {
|
|
3285
|
+
const authResponse = handleAuthError(err);
|
|
3286
|
+
if (authResponse) return authResponse;
|
|
3287
|
+
console.error("[support/ticket-synthesis] Error:", err);
|
|
3288
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3077
3294
|
// src/endpoints/email-stats.ts
|
|
3078
3295
|
function createEmailStatsEndpoint(slugs) {
|
|
3079
3296
|
return {
|
|
@@ -5012,6 +5229,7 @@ function createSupportEndpoints(slugs, options) {
|
|
|
5012
5229
|
if (!f || f.ai !== false) {
|
|
5013
5230
|
endpoints.push(createAiEndpoint(slugs));
|
|
5014
5231
|
endpoints.push(...createClientIntelligenceEndpoint(slugs));
|
|
5232
|
+
endpoints.push(createTicketSynthesisEndpoint(slugs));
|
|
5015
5233
|
}
|
|
5016
5234
|
if (!f || f.bulkActions !== false) endpoints.push(createBulkActionEndpoint(slugs));
|
|
5017
5235
|
if (!f || f.merge !== false) endpoints.push(createMergeTicketsEndpoint(slugs));
|
|
@@ -5668,6 +5886,32 @@ function createTrackSLA(slugs) {
|
|
|
5668
5886
|
return doc;
|
|
5669
5887
|
};
|
|
5670
5888
|
}
|
|
5889
|
+
function createTrackAiSummaryOnResolve(slugs) {
|
|
5890
|
+
return async ({ doc, previousDoc, operation, req }) => {
|
|
5891
|
+
if (operation !== "update" || !previousDoc) return doc;
|
|
5892
|
+
const wasResolved = previousDoc.status === "resolved";
|
|
5893
|
+
const isResolved = doc.status === "resolved";
|
|
5894
|
+
if (wasResolved && !isResolved && doc.aiSummary) {
|
|
5895
|
+
try {
|
|
5896
|
+
await req.payload.update({
|
|
5897
|
+
collection: slugs.tickets,
|
|
5898
|
+
id: doc.id,
|
|
5899
|
+
data: { aiSummary: null, aiSummaryGeneratedAt: null, aiSummaryStatus: null },
|
|
5900
|
+
overrideAccess: true
|
|
5901
|
+
});
|
|
5902
|
+
} catch (err) {
|
|
5903
|
+
console.error("[support] Failed to clear ai summary on reopen:", err);
|
|
5904
|
+
}
|
|
5905
|
+
return doc;
|
|
5906
|
+
}
|
|
5907
|
+
if (!wasResolved && isResolved && !doc.aiSummary) {
|
|
5908
|
+
setImmediate(() => {
|
|
5909
|
+
generateTicketSynthesis({ payload: req.payload, slugs, ticketId: doc.id }).catch((err) => console.error("[support] Background ai synthesis failed:", err));
|
|
5910
|
+
});
|
|
5911
|
+
}
|
|
5912
|
+
return doc;
|
|
5913
|
+
};
|
|
5914
|
+
}
|
|
5671
5915
|
function createLogTicketActivity(slugs) {
|
|
5672
5916
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
5673
5917
|
if (operation !== "update" || !previousDoc) return doc;
|
|
@@ -6072,6 +6316,49 @@ function createTicketsCollection(slugs, options) {
|
|
|
6072
6316
|
admin: { initCollapsed: true },
|
|
6073
6317
|
fields: billingFields
|
|
6074
6318
|
},
|
|
6319
|
+
// AI Synthesis collapsible — auto-filled when ticket is resolved
|
|
6320
|
+
{
|
|
6321
|
+
type: "collapsible",
|
|
6322
|
+
label: "Synthese IA",
|
|
6323
|
+
admin: {
|
|
6324
|
+
initCollapsed: true,
|
|
6325
|
+
description: "Recap factuel genere automatiquement au passage en resolu. Sert au copier-coller dans devis/factures."
|
|
6326
|
+
},
|
|
6327
|
+
fields: [
|
|
6328
|
+
{
|
|
6329
|
+
name: "aiSummary",
|
|
6330
|
+
type: "textarea",
|
|
6331
|
+
label: "Synthese",
|
|
6332
|
+
admin: {
|
|
6333
|
+
readOnly: true,
|
|
6334
|
+
rows: 8,
|
|
6335
|
+
description: "Vide tant que le ticket n'est pas resolu. Effacee si le ticket est reouvert."
|
|
6336
|
+
}
|
|
6337
|
+
},
|
|
6338
|
+
{
|
|
6339
|
+
type: "row",
|
|
6340
|
+
fields: [
|
|
6341
|
+
{
|
|
6342
|
+
name: "aiSummaryGeneratedAt",
|
|
6343
|
+
type: "date",
|
|
6344
|
+
label: "Genere le",
|
|
6345
|
+
admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } }
|
|
6346
|
+
},
|
|
6347
|
+
{
|
|
6348
|
+
name: "aiSummaryStatus",
|
|
6349
|
+
type: "select",
|
|
6350
|
+
label: "Statut",
|
|
6351
|
+
options: [
|
|
6352
|
+
{ label: "En cours", value: "pending" },
|
|
6353
|
+
{ label: "Genere", value: "done" },
|
|
6354
|
+
{ label: "Erreur", value: "error" }
|
|
6355
|
+
],
|
|
6356
|
+
admin: { readOnly: true, width: "50%" }
|
|
6357
|
+
}
|
|
6358
|
+
]
|
|
6359
|
+
}
|
|
6360
|
+
]
|
|
6361
|
+
},
|
|
6075
6362
|
// SLA & Delais
|
|
6076
6363
|
{
|
|
6077
6364
|
type: "collapsible",
|
|
@@ -6196,6 +6483,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
6196
6483
|
],
|
|
6197
6484
|
afterChange: [
|
|
6198
6485
|
createTrackSLA(slugs),
|
|
6486
|
+
createTrackAiSummaryOnResolve(slugs),
|
|
6199
6487
|
createAutoCalculateSLA(slugs),
|
|
6200
6488
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
6201
6489
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
@@ -276,6 +276,138 @@
|
|
|
276
276
|
color: #16a34a;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
// Warning banner — tickets without time
|
|
280
|
+
.warningBanner {
|
|
281
|
+
padding: 10px 16px;
|
|
282
|
+
border: 1px solid #f59e0b;
|
|
283
|
+
border-radius: 8px;
|
|
284
|
+
background: rgba(245, 158, 11, 0.08);
|
|
285
|
+
margin-bottom: 16px;
|
|
286
|
+
display: flex;
|
|
287
|
+
justify-content: space-between;
|
|
288
|
+
align-items: center;
|
|
289
|
+
flex-wrap: wrap;
|
|
290
|
+
gap: 12px;
|
|
291
|
+
font-size: 13px;
|
|
292
|
+
color: var(--theme-text);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.toggleLabel {
|
|
296
|
+
display: flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
gap: 6px;
|
|
299
|
+
cursor: pointer;
|
|
300
|
+
font-weight: 600;
|
|
301
|
+
user-select: none;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.noTimeBadge {
|
|
305
|
+
display: inline-block;
|
|
306
|
+
margin-left: 8px;
|
|
307
|
+
padding: 2px 8px;
|
|
308
|
+
border-radius: 999px;
|
|
309
|
+
background: rgba(245, 158, 11, 0.15);
|
|
310
|
+
color: #b45309;
|
|
311
|
+
font-size: 11px;
|
|
312
|
+
font-weight: 700;
|
|
313
|
+
white-space: nowrap;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.tableRowNoTime {
|
|
317
|
+
border-bottom: 1px solid var(--theme-elevation-200);
|
|
318
|
+
background: rgba(245, 158, 11, 0.04);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// AI summary toggle button
|
|
322
|
+
.summaryBtn {
|
|
323
|
+
display: inline-block;
|
|
324
|
+
margin-left: 6px;
|
|
325
|
+
padding: 1px 6px;
|
|
326
|
+
border-radius: 4px;
|
|
327
|
+
border: 1px solid var(--theme-elevation-300);
|
|
328
|
+
background: var(--theme-elevation-100);
|
|
329
|
+
color: var(--theme-elevation-500);
|
|
330
|
+
font-size: 10px;
|
|
331
|
+
font-weight: 600;
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
vertical-align: middle;
|
|
334
|
+
transition: background-color 100ms, color 100ms;
|
|
335
|
+
|
|
336
|
+
&:hover {
|
|
337
|
+
background: #2563eb;
|
|
338
|
+
color: #fff;
|
|
339
|
+
border-color: #2563eb;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// AI summary expanded row
|
|
344
|
+
.summaryRow {
|
|
345
|
+
background: var(--theme-elevation-50);
|
|
346
|
+
border-bottom: 1px solid var(--theme-elevation-300);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.summaryCell {
|
|
350
|
+
padding: 12px 16px 16px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.summaryHeader {
|
|
354
|
+
display: flex;
|
|
355
|
+
justify-content: space-between;
|
|
356
|
+
align-items: center;
|
|
357
|
+
margin-bottom: 8px;
|
|
358
|
+
flex-wrap: wrap;
|
|
359
|
+
gap: 8px;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.summaryActions {
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
gap: 8px;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.summaryMeta {
|
|
369
|
+
font-size: 11px;
|
|
370
|
+
color: var(--theme-elevation-500);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.summaryAction {
|
|
374
|
+
padding: 4px 10px;
|
|
375
|
+
border-radius: 6px;
|
|
376
|
+
border: 1px solid var(--theme-elevation-300);
|
|
377
|
+
background: var(--theme-elevation-100);
|
|
378
|
+
color: var(--theme-text);
|
|
379
|
+
font-size: 12px;
|
|
380
|
+
font-weight: 600;
|
|
381
|
+
cursor: pointer;
|
|
382
|
+
transition: background-color 100ms;
|
|
383
|
+
|
|
384
|
+
&:hover { background: var(--theme-elevation-200); }
|
|
385
|
+
&:disabled { cursor: not-allowed; opacity: 0.5; }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.summaryText {
|
|
389
|
+
margin: 0;
|
|
390
|
+
padding: 10px 12px;
|
|
391
|
+
background: var(--theme-elevation-100);
|
|
392
|
+
border: 1px solid var(--theme-elevation-300);
|
|
393
|
+
border-radius: 6px;
|
|
394
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif;
|
|
395
|
+
font-size: 13px;
|
|
396
|
+
color: var(--theme-text);
|
|
397
|
+
white-space: pre-wrap;
|
|
398
|
+
word-break: break-word;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.summaryEmpty {
|
|
402
|
+
padding: 10px 12px;
|
|
403
|
+
background: var(--theme-elevation-100);
|
|
404
|
+
border: 1px dashed var(--theme-elevation-300);
|
|
405
|
+
border-radius: 6px;
|
|
406
|
+
color: var(--theme-elevation-500);
|
|
407
|
+
font-size: 12px;
|
|
408
|
+
font-style: italic;
|
|
409
|
+
}
|
|
410
|
+
|
|
279
411
|
// Grand total
|
|
280
412
|
.grandTotal {
|
|
281
413
|
padding: 16px;
|