@consilioweb/payload-support 0.8.2 → 0.9.5
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/RichTextEditor/index.cjs +233 -0
- package/dist/components/RichTextEditor/index.js +232 -0
- package/dist/components/TicketConversation/components/CodeBlock.cjs +24 -7
- package/dist/components/TicketConversation/components/CodeBlock.js +23 -9
- package/dist/index.cjs +19 -1
- package/dist/index.js +19 -1
- package/dist/views/BillingView/client.cjs +260 -103
- package/dist/views/BillingView/client.js +259 -103
- package/dist/views/ChatView/client.cjs +184 -137
- package/dist/views/ChatView/client.js +180 -137
- package/dist/views/CrmView/client.cjs +270 -122
- package/dist/views/CrmView/client.js +266 -122
- package/dist/views/EmailTrackingView/client.cjs +80 -69
- package/dist/views/EmailTrackingView/client.js +80 -70
- package/dist/views/ImportConversationView/client.cjs +127 -94
- package/dist/views/ImportConversationView/client.js +123 -94
- package/dist/views/LogsView/client.cjs +56 -58
- package/dist/views/LogsView/client.js +52 -58
- package/dist/views/NewTicketView/client.cjs +39 -55
- package/dist/views/NewTicketView/client.js +38 -55
- package/dist/views/PendingEmailsView/client.cjs +399 -102
- package/dist/views/PendingEmailsView/client.js +396 -103
- package/dist/views/SupportDashboardView/client.cjs +276 -137
- package/dist/views/SupportDashboardView/client.js +275 -137
- package/dist/views/TicketDetailView/client.cjs +487 -204
- package/dist/views/TicketDetailView/client.js +486 -204
- package/dist/views/TicketInboxView/client.cjs +62 -65
- package/dist/views/TicketInboxView/client.js +62 -66
- package/dist/views/TicketingSettingsView/client.cjs +10 -8
- package/dist/views/TicketingSettingsView/client.js +10 -8
- package/dist/views/TimeDashboardView/client.cjs +70 -59
- package/dist/views/TimeDashboardView/client.js +69 -59
- package/package.json +7 -3
- package/src/components/RichTextEditor/index.tsx +261 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +53 -14
- package/src/plugin.ts +2 -0
- package/src/utils/emailTemplate.ts +37 -0
- package/src/views/BillingView/client.tsx +362 -69
- package/src/views/ChatView/client.tsx +225 -140
- package/src/views/CrmView/client.tsx +447 -189
- package/src/views/EmailTrackingView/client.tsx +111 -71
- package/src/views/ImportConversationView/client.tsx +255 -70
- package/src/views/LogsView/client.tsx +85 -50
- package/src/views/NewTicketView/client.tsx +37 -53
- package/src/views/PendingEmailsView/client.tsx +512 -92
- package/src/views/SupportDashboardView/client.tsx +294 -134
- package/src/views/TicketDetailView/client.tsx +486 -213
- package/src/views/TicketInboxView/client.tsx +52 -61
- package/src/views/TicketingSettingsView/client.tsx +10 -9
- package/src/views/TimeDashboardView/client.tsx +184 -69
|
@@ -3,13 +3,17 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
|
3
3
|
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import Link from 'next/link';
|
|
6
|
+
import { RichTextEditor } from '../../components/RichTextEditor/index.js';
|
|
7
|
+
import { hasCodeBlocks, CodeBlockRendererHtml, MessageWithCodeBlocks } from '../../components/TicketConversation/components/CodeBlock.js';
|
|
8
|
+
import { CodeBlockInserter } from '../../components/TicketConversation/components/CodeBlockInserter.js';
|
|
6
9
|
import { getFeatures } from '../shared/config.js';
|
|
7
10
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation.js';
|
|
11
|
+
import s from '../../styles/TicketDetail.module.scss';
|
|
8
12
|
|
|
9
|
-
const
|
|
10
|
-
open: {
|
|
11
|
-
waiting_client: {
|
|
12
|
-
resolved: {
|
|
13
|
+
const STATUS_STYLE = {
|
|
14
|
+
open: { bg: "#dbeafe", color: "#1e40af" },
|
|
15
|
+
waiting_client: { bg: "#fef3c7", color: "#92400e" },
|
|
16
|
+
resolved: { bg: "#dcfce7", color: "#166534" }
|
|
13
17
|
};
|
|
14
18
|
function timeAgo(d) {
|
|
15
19
|
const date = new Date(d), now = /* @__PURE__ */ new Date();
|
|
@@ -27,11 +31,92 @@ function dateLabel(d) {
|
|
|
27
31
|
if (date.toDateString() === y.toDateString()) return "Hier";
|
|
28
32
|
return date.toLocaleDateString("fr-FR", { day: "numeric", month: "long" });
|
|
29
33
|
}
|
|
34
|
+
const REWRITE_STYLES = [
|
|
35
|
+
{ id: "auto", label: "\u270F\uFE0F Auto", desc: "Garde le ton actuel" },
|
|
36
|
+
{ id: "tutoyer", label: "\u{1F44B} Tutoyer", desc: "Passe en tu" },
|
|
37
|
+
{ id: "vouvoyer", label: "\u{1F3A9} Vouvoyer", desc: "Passe en vous" },
|
|
38
|
+
{ id: "formel", label: "\u{1F4BC} Formel", desc: "Ton professionnel" },
|
|
39
|
+
{ id: "amical", label: "\u{1F60A} Amical", desc: "Ton chaleureux" },
|
|
40
|
+
{ id: "court", label: "\u26A1 Court", desc: "Version concise" }
|
|
41
|
+
];
|
|
42
|
+
const RewriteDropdown = ({ disabled, loading, onSelect, toolbarBtnClass }) => {
|
|
43
|
+
const [open, setOpen] = useState(false);
|
|
44
|
+
const ref = useRef(null);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!open) return;
|
|
47
|
+
const close = (e) => {
|
|
48
|
+
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
49
|
+
};
|
|
50
|
+
document.addEventListener("mousedown", close);
|
|
51
|
+
return () => document.removeEventListener("mousedown", close);
|
|
52
|
+
}, [open]);
|
|
53
|
+
return /* @__PURE__ */ jsxs("div", { ref, style: { position: "relative", display: "inline-block" }, children: [
|
|
54
|
+
/* @__PURE__ */ jsxs(
|
|
55
|
+
"button",
|
|
56
|
+
{
|
|
57
|
+
type: "button",
|
|
58
|
+
className: toolbarBtnClass,
|
|
59
|
+
onClick: () => setOpen(!open),
|
|
60
|
+
disabled,
|
|
61
|
+
style: { fontSize: 11, fontWeight: 700, padding: "4px 10px", width: "auto", display: "flex", alignItems: "center", gap: 4 },
|
|
62
|
+
children: [
|
|
63
|
+
loading ? "..." : "\u270F\uFE0F Reformuler",
|
|
64
|
+
!loading && /* @__PURE__ */ jsx("span", { style: { fontSize: 9, opacity: 0.6 }, children: "\u25BC" })
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
open && !disabled && !loading && /* @__PURE__ */ jsx("div", { style: {
|
|
69
|
+
position: "absolute",
|
|
70
|
+
bottom: "100%",
|
|
71
|
+
left: 0,
|
|
72
|
+
marginBottom: 4,
|
|
73
|
+
background: "#fff",
|
|
74
|
+
border: "1px solid #e5e7eb",
|
|
75
|
+
borderRadius: 8,
|
|
76
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
|
|
77
|
+
zIndex: 100,
|
|
78
|
+
minWidth: 180,
|
|
79
|
+
overflow: "hidden"
|
|
80
|
+
}, children: REWRITE_STYLES.map((style) => /* @__PURE__ */ jsxs(
|
|
81
|
+
"button",
|
|
82
|
+
{
|
|
83
|
+
type: "button",
|
|
84
|
+
onClick: () => {
|
|
85
|
+
setOpen(false);
|
|
86
|
+
onSelect(style.id);
|
|
87
|
+
},
|
|
88
|
+
style: {
|
|
89
|
+
display: "flex",
|
|
90
|
+
flexDirection: "column",
|
|
91
|
+
width: "100%",
|
|
92
|
+
padding: "8px 12px",
|
|
93
|
+
border: "none",
|
|
94
|
+
background: "transparent",
|
|
95
|
+
cursor: "pointer",
|
|
96
|
+
textAlign: "left",
|
|
97
|
+
borderBottom: "1px solid #f3f4f6"
|
|
98
|
+
},
|
|
99
|
+
onMouseEnter: (e) => {
|
|
100
|
+
e.currentTarget.style.background = "#f9fafb";
|
|
101
|
+
},
|
|
102
|
+
onMouseLeave: (e) => {
|
|
103
|
+
e.currentTarget.style.background = "transparent";
|
|
104
|
+
},
|
|
105
|
+
children: [
|
|
106
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 12, fontWeight: 600 }, children: style.label }),
|
|
107
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "#9ca3af" }, children: style.desc })
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
style.id
|
|
111
|
+
)) })
|
|
112
|
+
] });
|
|
113
|
+
};
|
|
30
114
|
const TicketDetailClient = () => {
|
|
31
115
|
const { t } = useTranslation();
|
|
32
116
|
const searchParams = useSearchParams();
|
|
33
117
|
const ticketId = searchParams.get("id");
|
|
34
118
|
const features = getFeatures();
|
|
119
|
+
const editorRef = useRef(null);
|
|
35
120
|
const threadEndRef = useRef(null);
|
|
36
121
|
const dropdownRef = useRef(null);
|
|
37
122
|
const fileInputRef = useRef(null);
|
|
@@ -43,6 +128,7 @@ const TicketDetailClient = () => {
|
|
|
43
128
|
const [cannedResponses, setCannedResponses] = useState([]);
|
|
44
129
|
const [loading, setLoading] = useState(true);
|
|
45
130
|
const [replyBody, setReplyBody] = useState("");
|
|
131
|
+
const [replyHtml, setReplyHtml] = useState("");
|
|
46
132
|
const [isInternal, setIsInternal] = useState(false);
|
|
47
133
|
const [notifyClient, setNotifyClient] = useState(true);
|
|
48
134
|
const [sending, setSending] = useState(false);
|
|
@@ -53,6 +139,8 @@ const TicketDetailClient = () => {
|
|
|
53
139
|
const [sentiment, setSentiment] = useState(null);
|
|
54
140
|
const [statusUpdating, setStatusUpdating] = useState(false);
|
|
55
141
|
const [showActivity, setShowActivity] = useState(false);
|
|
142
|
+
const [clientSummary, setClientSummary] = useState(null);
|
|
143
|
+
const [summaryLoading, setSummaryLoading] = useState(false);
|
|
56
144
|
const [timerRunning, setTimerRunning] = useState(() => {
|
|
57
145
|
if (typeof window === "undefined" || !ticketId) return false;
|
|
58
146
|
return localStorage.getItem(`timer-run-${ticketId}`) === "1";
|
|
@@ -112,26 +200,41 @@ const TicketDetailClient = () => {
|
|
|
112
200
|
}
|
|
113
201
|
setLoading(false);
|
|
114
202
|
}, [ticketId]);
|
|
203
|
+
const failCountRef = useRef({ messages: 0, typing: 0, presence: 0 });
|
|
204
|
+
const MAX_FAILS = 3;
|
|
115
205
|
useEffect(() => {
|
|
116
206
|
fetchAll();
|
|
117
207
|
}, [fetchAll]);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!ticket) return;
|
|
210
|
+
const clientId = typeof ticket.client === "object" ? ticket.client?.id : ticket.client;
|
|
211
|
+
if (!clientId) return;
|
|
212
|
+
setSummaryLoading(true);
|
|
213
|
+
fetch(`/api/support/client-intelligence?clientId=${clientId}`, { credentials: "include" }).then((r) => r.ok ? r.json() : null).then((d) => {
|
|
214
|
+
if (d) setClientSummary(d);
|
|
215
|
+
}).catch(() => {
|
|
216
|
+
}).finally(() => setSummaryLoading(false));
|
|
217
|
+
}, [ticket?.id]);
|
|
118
218
|
useEffect(() => {
|
|
119
219
|
if (!ticketId || loading) return;
|
|
120
220
|
const iv = setInterval(async () => {
|
|
221
|
+
if (failCountRef.current.messages >= MAX_FAILS) return;
|
|
121
222
|
try {
|
|
122
223
|
const [mr, tr] = await Promise.all([
|
|
123
224
|
fetch(`/api/ticket-messages?where[ticket][equals]=${ticketId}&sort=createdAt&limit=200&depth=1`, { credentials: "include" }),
|
|
124
225
|
fetch(`/api/tickets/${ticketId}?depth=0`, { credentials: "include" })
|
|
125
226
|
]);
|
|
126
|
-
if (mr.ok) {
|
|
227
|
+
if (mr.ok && tr.ok) {
|
|
228
|
+
failCountRef.current.messages = 0;
|
|
127
229
|
const d = await mr.json();
|
|
128
230
|
setMessages(d.docs || []);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
231
|
+
const td = await tr.json();
|
|
232
|
+
setTicket((p) => p ? { ...p, ...td } : td);
|
|
233
|
+
} else {
|
|
234
|
+
failCountRef.current.messages++;
|
|
133
235
|
}
|
|
134
236
|
} catch {
|
|
237
|
+
failCountRef.current.messages++;
|
|
135
238
|
}
|
|
136
239
|
}, 1e4);
|
|
137
240
|
return () => clearInterval(iv);
|
|
@@ -144,33 +247,40 @@ const TicketDetailClient = () => {
|
|
|
144
247
|
useEffect(() => {
|
|
145
248
|
if (!ticketId) return;
|
|
146
249
|
const iv = setInterval(async () => {
|
|
250
|
+
if (failCountRef.current.typing >= MAX_FAILS) return;
|
|
147
251
|
try {
|
|
148
252
|
const r = await fetch(`/api/support/typing?ticketId=${ticketId}`, { credentials: "include" });
|
|
149
253
|
if (r.ok) {
|
|
254
|
+
failCountRef.current.typing = 0;
|
|
150
255
|
const d = await r.json();
|
|
151
256
|
setClientTyping(d.typing);
|
|
257
|
+
} else {
|
|
258
|
+
failCountRef.current.typing++;
|
|
152
259
|
}
|
|
153
|
-
} catch
|
|
154
|
-
|
|
260
|
+
} catch {
|
|
261
|
+
failCountRef.current.typing++;
|
|
155
262
|
}
|
|
156
|
-
},
|
|
263
|
+
}, 3e3);
|
|
157
264
|
return () => clearInterval(iv);
|
|
158
265
|
}, [ticketId]);
|
|
159
266
|
useEffect(() => {
|
|
160
267
|
if (!features.ai || messages.length === 0) return;
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
268
|
+
const clientMsgs = messages.filter((m) => m.authorType === "client" || m.authorType === "email").slice(-3);
|
|
269
|
+
if (clientMsgs.length === 0) return;
|
|
270
|
+
const contextText = clientMsgs.map((m) => m.body).join("\n---\n").slice(0, 1e3);
|
|
271
|
+
fetch("/api/support/ai", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ action: "sentiment", text: contextText }) }).then((r) => r.json()).then((d) => {
|
|
272
|
+
const raw = (d.sentiment || "").toLowerCase().replace(/[^a-zéèêàùûîôëüöç]/g, "");
|
|
165
273
|
const map = {
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
274
|
+
"frustr\xE9": { emoji: "\u{1F624}", label: "Frustr\xE9", color: "#dc2626" },
|
|
275
|
+
"frustre": { emoji: "\u{1F624}", label: "Frustr\xE9", color: "#dc2626" },
|
|
276
|
+
"m\xE9content": { emoji: "\u{1F620}", label: "M\xE9content", color: "#ea580c" },
|
|
277
|
+
"mecontent": { emoji: "\u{1F620}", label: "M\xE9content", color: "#ea580c" },
|
|
278
|
+
"urgent": { emoji: "\u{1F525}", label: "Urgent", color: "#dc2626" },
|
|
279
|
+
"neutre": { emoji: "\u{1F610}", label: "Neutre", color: "#6b7280" },
|
|
280
|
+
"satisfait": { emoji: "\u{1F60A}", label: "Satisfait", color: "#16a34a" }
|
|
171
281
|
};
|
|
172
282
|
const m = Object.keys(map).find((k) => raw.includes(k));
|
|
173
|
-
setSentiment(m ? map[m] : { emoji: "
|
|
283
|
+
setSentiment(m ? map[m] : { emoji: "\u{1F610}", label: "Neutre", color: "#6b7280" });
|
|
174
284
|
}).catch(() => {
|
|
175
285
|
});
|
|
176
286
|
}, [messages.length, features.ai]);
|
|
@@ -224,13 +334,18 @@ const TicketDetailClient = () => {
|
|
|
224
334
|
useEffect(() => {
|
|
225
335
|
if (!ticketId) return;
|
|
226
336
|
const poll = setInterval(async () => {
|
|
337
|
+
if (failCountRef.current.presence >= MAX_FAILS) return;
|
|
227
338
|
try {
|
|
228
339
|
const r = await fetch(`/api/support/presence?ticketId=${ticketId}`, { credentials: "include" });
|
|
229
340
|
if (r.ok) {
|
|
341
|
+
failCountRef.current.presence = 0;
|
|
230
342
|
const d = await r.json();
|
|
231
343
|
setOtherViewers(d.viewers || []);
|
|
344
|
+
} else {
|
|
345
|
+
failCountRef.current.presence++;
|
|
232
346
|
}
|
|
233
347
|
} catch {
|
|
348
|
+
failCountRef.current.presence++;
|
|
234
349
|
}
|
|
235
350
|
}, 5e3);
|
|
236
351
|
return () => clearInterval(poll);
|
|
@@ -272,7 +387,7 @@ const TicketDetailClient = () => {
|
|
|
272
387
|
return () => document.removeEventListener("keydown", handler);
|
|
273
388
|
}, []);
|
|
274
389
|
const handleSend = async () => {
|
|
275
|
-
if (!replyBody.trim() || !ticketId) return;
|
|
390
|
+
if (!replyBody.trim() && !replyHtml.trim() || !ticketId) return;
|
|
276
391
|
setSending(true);
|
|
277
392
|
try {
|
|
278
393
|
const uploadedLinks = [];
|
|
@@ -291,21 +406,23 @@ const TicketDetailClient = () => {
|
|
|
291
406
|
}
|
|
292
407
|
const finalBody = uploadedLinks.length > 0 ? `${replyBody.trim()}
|
|
293
408
|
|
|
294
|
-
${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
409
|
+
${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
410
|
+
const finalHtml = uploadedLinks.length > 0 ? `${replyHtml || replyBody.trim()}<br/><br/>${uploadedLinks.map((l) => l.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')).join("<br/>")}` : replyHtml || void 0;
|
|
295
411
|
const res = await fetch("/api/ticket-messages", {
|
|
296
412
|
method: "POST",
|
|
297
413
|
headers: { "Content-Type": "application/json" },
|
|
298
414
|
credentials: "include",
|
|
299
|
-
body: JSON.stringify({ ticket: Number(ticketId), body: finalBody, authorType: "admin", isInternal, skipNotification: isInternal || !notifyClient })
|
|
415
|
+
body: JSON.stringify({ ticket: Number(ticketId), body: finalBody, ...finalHtml ? { bodyHtml: finalHtml } : {}, authorType: "admin", isInternal, skipNotification: isInternal || !notifyClient })
|
|
300
416
|
});
|
|
301
417
|
if (res.ok) {
|
|
302
418
|
setReplyBody("");
|
|
419
|
+
setReplyHtml("");
|
|
303
420
|
setIsInternal(false);
|
|
304
421
|
setPendingFiles([]);
|
|
422
|
+
editorRef.current?.clear();
|
|
305
423
|
fetchAll();
|
|
306
424
|
}
|
|
307
|
-
} catch
|
|
308
|
-
console.warn("[support] send reply error:", err);
|
|
425
|
+
} catch {
|
|
309
426
|
} finally {
|
|
310
427
|
setSending(false);
|
|
311
428
|
}
|
|
@@ -316,8 +433,7 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
316
433
|
try {
|
|
317
434
|
await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ status: v }) });
|
|
318
435
|
fetchAll();
|
|
319
|
-
} catch
|
|
320
|
-
console.warn("[support] status change error:", err);
|
|
436
|
+
} catch {
|
|
321
437
|
} finally {
|
|
322
438
|
setStatusUpdating(false);
|
|
323
439
|
}
|
|
@@ -371,27 +487,29 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
371
487
|
const d = await r.json();
|
|
372
488
|
if (d.reply) {
|
|
373
489
|
setReplyBody(d.reply);
|
|
490
|
+
setReplyHtml(d.reply.replace(/\n/g, "<br/>"));
|
|
491
|
+
editorRef.current?.setContent(d.reply.replace(/\n/g, "<br/>"));
|
|
374
492
|
}
|
|
375
493
|
}
|
|
376
|
-
} catch
|
|
377
|
-
console.warn("[support] AI suggest error:", err);
|
|
494
|
+
} catch {
|
|
378
495
|
} finally {
|
|
379
496
|
setAiReplying(false);
|
|
380
497
|
}
|
|
381
498
|
};
|
|
382
|
-
const handleAiRewrite = async () => {
|
|
499
|
+
const handleAiRewrite = async (style = "auto") => {
|
|
383
500
|
if (!replyBody.trim()) return;
|
|
384
501
|
setAiRewriting(true);
|
|
385
502
|
try {
|
|
386
|
-
const r = await fetch("/api/support/ai", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ action: "rewrite", text: replyBody }) });
|
|
503
|
+
const r = await fetch("/api/support/ai", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ action: "rewrite", text: replyBody, style }) });
|
|
387
504
|
if (r.ok) {
|
|
388
505
|
const d = await r.json();
|
|
389
506
|
if (d.rewritten) {
|
|
390
507
|
setReplyBody(d.rewritten);
|
|
508
|
+
setReplyHtml(d.rewritten.replace(/\n/g, "<br/>"));
|
|
509
|
+
editorRef.current?.setContent(d.rewritten.replace(/\n/g, "<br/>"));
|
|
391
510
|
}
|
|
392
511
|
}
|
|
393
|
-
} catch
|
|
394
|
-
console.warn("[support] AI rewrite error:", err);
|
|
512
|
+
} catch {
|
|
395
513
|
} finally {
|
|
396
514
|
setAiRewriting(false);
|
|
397
515
|
}
|
|
@@ -403,8 +521,7 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
403
521
|
setTimerSeconds(0);
|
|
404
522
|
setTimerRunning(false);
|
|
405
523
|
fetchAll();
|
|
406
|
-
} catch
|
|
407
|
-
console.warn("[support] timer save error:", err);
|
|
524
|
+
} catch {
|
|
408
525
|
}
|
|
409
526
|
};
|
|
410
527
|
const handleApplyMacro = async (macroId) => {
|
|
@@ -420,8 +537,7 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
420
537
|
if (r.ok) {
|
|
421
538
|
fetchAll();
|
|
422
539
|
}
|
|
423
|
-
} catch
|
|
424
|
-
console.warn("[support] apply macro error:", err);
|
|
540
|
+
} catch {
|
|
425
541
|
} finally {
|
|
426
542
|
setApplyingMacro(false);
|
|
427
543
|
}
|
|
@@ -466,127 +582,128 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
466
582
|
if (!ticketId) return /* @__PURE__ */ jsx("div", { style: { padding: 60, textAlign: "center", color: "#94a3b8", fontSize: 14 }, children: t("detail.selectTicket") });
|
|
467
583
|
if (loading) return /* @__PURE__ */ jsx("div", { style: { padding: 60, textAlign: "center", color: "#94a3b8", fontSize: 14 }, children: t("common.loading") });
|
|
468
584
|
if (!ticket) return /* @__PURE__ */ jsx("div", { style: { padding: 60, textAlign: "center", color: "#94a3b8", fontSize: 14 }, children: t("detail.notFound") });
|
|
469
|
-
const st =
|
|
585
|
+
const st = STATUS_STYLE[ticket.status || "open"] || STATUS_STYLE.open;
|
|
470
586
|
const totalMin = timeEntries.reduce((a, e) => a + (e.duration || 0), 0);
|
|
471
587
|
const initials = client ? `${(client.firstName?.[0] || "").toUpperCase()}${(client.lastName?.[0] || "").toUpperCase()}` : "?";
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
statusChip: { padding: "4px 10px", borderRadius: 6, border: "none", fontWeight: 600, fontSize: 12, cursor: "pointer" },
|
|
479
|
-
layout: { display: "grid", gridTemplateColumns: "1fr 280px", gap: 20 },
|
|
480
|
-
thread: { display: "flex", flexDirection: "column", gap: 12, maxHeight: "calc(100vh - 400px)", overflowY: "auto", padding: "8px 0" },
|
|
481
|
-
message: { display: "flex", gap: 10, padding: "12px 14px", borderRadius: 10, border: "1px solid var(--theme-elevation-150)", position: "relative" },
|
|
482
|
-
avatar: { width: 32, height: 32, borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", color: "#fff", fontSize: 11, fontWeight: 700, flexShrink: 0 },
|
|
483
|
-
messageBody: { fontSize: 14, lineHeight: 1.6, color: "var(--theme-text)", whiteSpace: "pre-wrap" },
|
|
484
|
-
composer: { marginTop: 12, border: "1px solid var(--theme-elevation-200)", borderRadius: 10, padding: 12 },
|
|
485
|
-
composerInternal: { borderColor: "#fbbf24", background: "#fefce8" },
|
|
486
|
-
textarea: { width: "100%", minHeight: 100, padding: 10, border: "none", outline: "none", resize: "vertical", fontSize: 14, fontFamily: "inherit", background: "transparent", color: "var(--theme-text)" },
|
|
487
|
-
composerFooter: { display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 8 },
|
|
488
|
-
sendBtn: { padding: "8px 16px", borderRadius: 6, border: "none", background: "#2563eb", color: "#fff", fontWeight: 600, fontSize: 13, cursor: "pointer" },
|
|
489
|
-
sidebar: { display: "flex", flexDirection: "column", gap: 16 },
|
|
490
|
-
sideSection: { padding: "12px 14px", borderRadius: 10, border: "1px solid var(--theme-elevation-150)", fontSize: 13 },
|
|
491
|
-
sideSectionTitle: { fontSize: 12, fontWeight: 700, textTransform: "uppercase", color: "var(--theme-elevation-500)", marginBottom: 8, display: "flex", justifyContent: "space-between", alignItems: "center" },
|
|
492
|
-
sideField: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "4px 0" },
|
|
493
|
-
sideLabel: { fontSize: 12, color: "var(--theme-elevation-500)" },
|
|
494
|
-
sideSelect: { padding: "2px 6px", borderRadius: 4, border: "1px solid var(--theme-elevation-200)", fontSize: 12, background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
|
|
495
|
-
sideValue: { fontSize: 12, fontWeight: 500, color: "var(--theme-text)" },
|
|
496
|
-
badge: { padding: "2px 6px", borderRadius: 4, fontSize: 10, fontWeight: 600 },
|
|
497
|
-
tagChip: { display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", borderRadius: 4, background: "var(--theme-elevation-100)", fontSize: 11, fontWeight: 500 },
|
|
498
|
-
tagRemove: { background: "none", border: "none", cursor: "pointer", fontSize: 14, color: "var(--theme-elevation-400)", padding: 0 },
|
|
499
|
-
tagInput: { padding: "2px 6px", borderRadius: 4, border: "1px solid var(--theme-elevation-200)", fontSize: 11, width: 80 },
|
|
500
|
-
tagAddBtn: { padding: "2px 8px", borderRadius: 4, border: "1px dashed var(--theme-elevation-300)", background: "none", fontSize: 11, cursor: "pointer", color: "var(--theme-elevation-400)" },
|
|
501
|
-
toolbarBtn: { padding: "4px 10px", borderRadius: 4, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", fontSize: 11, fontWeight: 700, cursor: "pointer", color: "var(--theme-text)" },
|
|
502
|
-
dateSeparator: { textAlign: "center", padding: "8px 0", fontSize: 11, color: "var(--theme-elevation-400)" }
|
|
503
|
-
};
|
|
504
|
-
return /* @__PURE__ */ jsxs("div", { style: S.page, children: [
|
|
505
|
-
/* @__PURE__ */ jsxs("div", { style: S.topBar, children: [
|
|
506
|
-
/* @__PURE__ */ jsx(Link, { href: "/admin/support/inbox", style: S.backLink, "aria-label": "Retour", children: "\u2190" }),
|
|
507
|
-
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, flex: 1 }, children: [
|
|
508
|
-
/* @__PURE__ */ jsx("span", { style: S.ticketNumber, children: ticket.ticketNumber }),
|
|
509
|
-
/* @__PURE__ */ jsx("span", { style: S.ticketSubject, children: ticket.subject })
|
|
588
|
+
return /* @__PURE__ */ jsxs("div", { className: s.page, children: [
|
|
589
|
+
/* @__PURE__ */ jsxs("div", { className: s.topBar, children: [
|
|
590
|
+
/* @__PURE__ */ jsx(Link, { href: "/admin/support/inbox", className: s.backLink, "aria-label": "Retour \xE0 la bo\xEEte de r\xE9ception", children: "\u2190" }),
|
|
591
|
+
/* @__PURE__ */ jsxs("div", { className: s.ticketMeta, children: [
|
|
592
|
+
/* @__PURE__ */ jsx("span", { className: s.ticketNumber, children: ticket.ticketNumber }),
|
|
593
|
+
/* @__PURE__ */ jsx("span", { className: s.ticketSubject, children: ticket.subject })
|
|
510
594
|
] }),
|
|
511
|
-
/* @__PURE__ */ jsxs("div", {
|
|
512
|
-
/* @__PURE__ */ jsxs("select", {
|
|
595
|
+
/* @__PURE__ */ jsxs("div", { className: s.topBarRight, children: [
|
|
596
|
+
/* @__PURE__ */ jsxs("select", { className: s.statusChip, style: { background: st.bg, color: st.color }, value: ticket.status || "open", onChange: (e) => handleStatusChange(e.target.value), disabled: statusUpdating, "aria-label": t("ticket.status.label"), children: [
|
|
513
597
|
/* @__PURE__ */ jsx("option", { value: "open", children: t("detail.statusOpen") }),
|
|
514
598
|
/* @__PURE__ */ jsx("option", { value: "waiting_client", children: t("detail.statusWaiting") }),
|
|
515
599
|
/* @__PURE__ */ jsx("option", { value: "resolved", children: t("detail.statusResolved") })
|
|
516
600
|
] }),
|
|
517
|
-
sentiment && features.ai && /* @__PURE__ */ jsxs("span", { style: {
|
|
601
|
+
sentiment && features.ai && /* @__PURE__ */ jsxs("span", { className: s.sentimentBadge, style: { background: `${sentiment.color}12`, color: sentiment.color }, children: [
|
|
518
602
|
sentiment.emoji,
|
|
519
603
|
" ",
|
|
520
604
|
sentiment.label
|
|
521
605
|
] }),
|
|
522
|
-
/* @__PURE__ */ jsxs("div", {
|
|
523
|
-
/* @__PURE__ */ jsx("button", { onClick: () => setShowMenu(!showMenu),
|
|
524
|
-
showMenu && /* @__PURE__ */ jsxs("div", {
|
|
525
|
-
/* @__PURE__ */ jsx("button", {
|
|
606
|
+
/* @__PURE__ */ jsxs("div", { className: s.dropdown, ref: dropdownRef, children: [
|
|
607
|
+
/* @__PURE__ */ jsx("button", { className: s.moreBtn, onClick: () => setShowMenu(!showMenu), "aria-label": "Plus d'options", children: "\xB7\xB7\xB7" }),
|
|
608
|
+
showMenu && /* @__PURE__ */ jsxs("div", { className: s.dropdownMenu, children: [
|
|
609
|
+
/* @__PURE__ */ jsx("button", { className: s.dropdownItem, onClick: () => {
|
|
526
610
|
navigator.clipboard.writeText(window.location.href);
|
|
527
611
|
setShowMenu(false);
|
|
528
612
|
}, children: t("detail.copyLink") }),
|
|
529
|
-
/* @__PURE__ */ jsx(Link, { href: `/admin/collections/tickets/${ticketId}`,
|
|
530
|
-
/* @__PURE__ */ jsx("a", { href: `/support/tickets/${ticketId}`, target: "_blank", rel: "noopener noreferrer",
|
|
613
|
+
/* @__PURE__ */ jsx(Link, { href: `/admin/collections/tickets/${ticketId}`, className: s.dropdownItem, onClick: () => setShowMenu(false), children: t("detail.payloadView") }),
|
|
614
|
+
/* @__PURE__ */ jsx("a", { href: `/support/tickets/${ticketId}`, target: "_blank", rel: "noopener noreferrer", className: s.dropdownItem, onClick: () => setShowMenu(false), children: t("detail.clientView") })
|
|
531
615
|
] })
|
|
532
616
|
] })
|
|
533
617
|
] })
|
|
534
618
|
] }),
|
|
535
|
-
otherViewers.length > 0 && /* @__PURE__ */ jsxs("div", { style: {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
619
|
+
otherViewers.length > 0 && /* @__PURE__ */ jsxs("div", { style: {
|
|
620
|
+
display: "flex",
|
|
621
|
+
alignItems: "center",
|
|
622
|
+
gap: 8,
|
|
623
|
+
padding: "8px 14px",
|
|
624
|
+
marginBottom: 12,
|
|
625
|
+
borderRadius: 8,
|
|
626
|
+
background: "#fef3c7",
|
|
627
|
+
border: "1px solid #fde68a",
|
|
628
|
+
fontSize: 13,
|
|
629
|
+
fontWeight: 500,
|
|
630
|
+
color: "#92400e"
|
|
631
|
+
}, children: [
|
|
632
|
+
/* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx("path", { d: "M9 1L3 9h4l-1 6 6-8H8l1-6z", fill: "#92400e" }) }),
|
|
633
|
+
otherViewers.length === 1 ? t("detail.viewingAlso", { names: otherViewers.map((v) => v.name).join(", ") }) : t("detail.viewingAlsoPlural", { names: otherViewers.map((v) => v.name).join(", ") })
|
|
539
634
|
] }),
|
|
540
|
-
/* @__PURE__ */ jsxs("div", {
|
|
541
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
542
|
-
/* @__PURE__ */ jsxs("div", {
|
|
635
|
+
/* @__PURE__ */ jsxs("div", { className: s.layout, children: [
|
|
636
|
+
/* @__PURE__ */ jsxs("div", { className: s.conversationCol, children: [
|
|
637
|
+
/* @__PURE__ */ jsxs("div", { className: s.thread, children: [
|
|
543
638
|
messages.map((msg, idx) => {
|
|
544
639
|
const prev = idx > 0 ? messages[idx - 1] : null;
|
|
545
640
|
const showDate = msg.createdAt && (!prev?.createdAt || new Date(msg.createdAt).toDateString() !== new Date(prev.createdAt).toDateString());
|
|
546
641
|
const isAdmin = msg.authorType === "admin";
|
|
547
642
|
const isPendingDelete = undoToast?.msgId === msg.id;
|
|
548
643
|
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
549
|
-
showDate && /* @__PURE__ */ jsx("div", {
|
|
550
|
-
/* @__PURE__ */ jsxs("div", {
|
|
551
|
-
/* @__PURE__ */ jsx("div", {
|
|
552
|
-
/* @__PURE__ */ jsxs("div", {
|
|
553
|
-
/* @__PURE__ */ jsxs("div", {
|
|
554
|
-
/* @__PURE__ */ jsx("span", {
|
|
555
|
-
/* @__PURE__ */ jsx("span", {
|
|
556
|
-
msg.isInternal && /* @__PURE__ */ jsx("span", {
|
|
557
|
-
msg.isSolution && /* @__PURE__ */ jsx("span", {
|
|
644
|
+
showDate && /* @__PURE__ */ jsx("div", { className: s.dateSeparator, children: /* @__PURE__ */ jsx("span", { className: s.dateSeparatorText, children: dateLabel(msg.createdAt) }) }),
|
|
645
|
+
/* @__PURE__ */ jsxs("div", { className: `${s.message} ${msg.isInternal ? s.messageInternal : ""}`, style: isPendingDelete ? { opacity: 0.3, pointerEvents: "none" } : void 0, children: [
|
|
646
|
+
/* @__PURE__ */ jsx("div", { className: s.avatar, style: { backgroundColor: isAdmin ? "#2563eb" : msg.authorType === "email" ? "#ea580c" : "#7c3aed" }, children: isAdmin ? "CW" : initials }),
|
|
647
|
+
/* @__PURE__ */ jsxs("div", { className: s.messageContent, children: [
|
|
648
|
+
/* @__PURE__ */ jsxs("div", { className: s.messageHeader, children: [
|
|
649
|
+
/* @__PURE__ */ jsx("span", { className: s.messageAuthor, children: isAdmin ? "Support" : msg.authorType === "email" ? "Email" : client?.firstName || "Client" }),
|
|
650
|
+
/* @__PURE__ */ jsx("span", { className: s.messageTime, children: timeAgo(msg.createdAt) }),
|
|
651
|
+
msg.isInternal && /* @__PURE__ */ jsx("span", { className: s.badge, style: { background: "#fef3c7", color: "#92400e" }, children: "Interne" }),
|
|
652
|
+
msg.isSolution && /* @__PURE__ */ jsx("span", { className: s.badge, style: { background: "#dcfce7", color: "#166534" }, children: "Solution" }),
|
|
653
|
+
/* @__PURE__ */ jsx("span", { className: s.messageMeta, children: isAdmin && !msg.isInternal && (() => {
|
|
654
|
+
const ext = msg;
|
|
655
|
+
if (ext.emailOpenedAt) {
|
|
656
|
+
const d = new Date(ext.emailOpenedAt).toLocaleString("fr-FR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
|
|
657
|
+
return /* @__PURE__ */ jsxs("span", { style: { color: "#16a34a", cursor: "help" }, title: `Ouvert le ${d}`, children: [
|
|
658
|
+
"\u2713\u2713 Lu ",
|
|
659
|
+
d
|
|
660
|
+
] });
|
|
661
|
+
}
|
|
662
|
+
if (ext.emailSentAt) {
|
|
663
|
+
const d = new Date(ext.emailSentAt).toLocaleString("fr-FR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
|
|
664
|
+
return /* @__PURE__ */ jsxs("span", { style: { color: "#2563eb", cursor: "help" }, title: `Envoy\xE9 le ${d}`, children: [
|
|
665
|
+
"\u2713 Envoy\xE9 ",
|
|
666
|
+
d
|
|
667
|
+
] });
|
|
668
|
+
}
|
|
669
|
+
return /* @__PURE__ */ jsx("span", { style: { color: "#94a3b8" }, children: "\u2713" });
|
|
670
|
+
})() })
|
|
558
671
|
] }),
|
|
559
|
-
msg.
|
|
560
|
-
Array.isArray(msg.attachments) && msg.attachments.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
672
|
+
msg.deletedAt ? /* @__PURE__ */ jsx("div", { className: s.messageBody, style: { color: "#94a3b8", fontStyle: "italic" }, children: t("detail.messageDeleted") }) : msg.bodyHtml && hasCodeBlocks(msg.bodyHtml.replace(/<[^>]+>/g, "")) ? /* @__PURE__ */ jsx(CodeBlockRendererHtml, { html: msg.bodyHtml }) : msg.bodyHtml ? /* @__PURE__ */ jsx("div", { className: `${s.messageBody} ${s.rteDisplay}`, dangerouslySetInnerHTML: { __html: msg.bodyHtml } }) : hasCodeBlocks(msg.body) ? /* @__PURE__ */ jsx(MessageWithCodeBlocks, { text: msg.body, style: { fontSize: "13px", lineHeight: 1.5 } }) : /* @__PURE__ */ jsx("div", { className: s.messageBody, children: msg.body }),
|
|
673
|
+
Array.isArray(msg.attachments) && msg.attachments.length > 0 && /* @__PURE__ */ jsx("div", { className: s.attachments, children: msg.attachments.map((att, i) => {
|
|
561
674
|
const file = typeof att.file === "object" ? att.file : null;
|
|
562
675
|
if (!file) return null;
|
|
563
|
-
return (file.mimeType || "").startsWith("image/") ? /* @__PURE__ */ jsx("a", { href: file.url || "#", target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ jsx("img", { src: file.url || "", alt: "",
|
|
676
|
+
return (file.mimeType || "").startsWith("image/") ? /* @__PURE__ */ jsx("a", { href: file.url || "#", target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ jsx("img", { src: file.url || "", alt: "", className: s.attachmentImg }) }, i) : /* @__PURE__ */ jsxs("a", { href: file.url || "#", target: "_blank", rel: "noopener noreferrer", className: s.attachmentFile, children: [
|
|
564
677
|
"PJ ",
|
|
565
678
|
file.filename || "Fichier"
|
|
566
679
|
] }, i);
|
|
567
680
|
}) })
|
|
568
681
|
] }),
|
|
569
|
-
/* @__PURE__ */ jsxs("div", {
|
|
570
|
-
/* @__PURE__ */ jsx("button", {
|
|
571
|
-
features.splitTicket && !msg.isInternal && /* @__PURE__ */ jsx("button", {
|
|
682
|
+
/* @__PURE__ */ jsxs("div", { className: s.messageActions, children: [
|
|
683
|
+
/* @__PURE__ */ jsx("button", { className: `${s.actionIcon} ${s.danger}`, title: t("actions.deleteMessage"), "aria-label": t("actions.deleteMessage"), onClick: () => handleDeleteMessage(msg.id), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: t("actions.deleteMessage") }),
|
|
684
|
+
features.splitTicket && !msg.isInternal && /* @__PURE__ */ jsx("button", { className: s.actionIcon, title: t("actions.extractMessage"), "aria-label": t("actions.extractToNewTicket"), onClick: () => {
|
|
572
685
|
setSplitModal({ messageId: msg.id, preview: msg.body.slice(0, 200) });
|
|
573
686
|
setSplitSubject(`Split: ${ticket.subject}`);
|
|
574
|
-
}, children: t("actions.extractMessage") }),
|
|
575
|
-
isAdmin && !msg.isInternal && /* @__PURE__ */ jsx("button", {
|
|
687
|
+
}, style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: t("actions.extractMessage") }),
|
|
688
|
+
isAdmin && !msg.isInternal && /* @__PURE__ */ jsx("button", { className: s.actionIcon, title: t("actions.resendEmail"), "aria-label": t("actions.resendEmail"), onClick: () => fetch("/api/support/resend-notification", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ messageId: msg.id }) }), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: t("actions.resendEmail") })
|
|
576
689
|
] })
|
|
577
690
|
] })
|
|
578
691
|
] }, msg.id);
|
|
579
692
|
}),
|
|
580
693
|
/* @__PURE__ */ jsx("div", { ref: threadEndRef })
|
|
581
694
|
] }),
|
|
582
|
-
clientTyping && /* @__PURE__ */ jsxs("div", {
|
|
583
|
-
|
|
584
|
-
|
|
695
|
+
clientTyping && /* @__PURE__ */ jsxs("div", { className: s.typing, children: [
|
|
696
|
+
/* @__PURE__ */ jsxs("span", { className: s.typingDots, children: [
|
|
697
|
+
/* @__PURE__ */ jsx("span", {}),
|
|
698
|
+
/* @__PURE__ */ jsx("span", {}),
|
|
699
|
+
/* @__PURE__ */ jsx("span", {})
|
|
700
|
+
] }),
|
|
701
|
+
t("detail.typing", { name: client?.firstName || "Client" })
|
|
585
702
|
] }),
|
|
586
703
|
/* @__PURE__ */ jsxs(
|
|
587
704
|
"div",
|
|
588
705
|
{
|
|
589
|
-
|
|
706
|
+
className: `${s.composer} ${isInternal ? s.composerInternal : ""} ${composerDragOver ? s.composerDragOver : ""}`,
|
|
590
707
|
onDragOver: (e) => {
|
|
591
708
|
e.preventDefault();
|
|
592
709
|
setComposerDragOver(true);
|
|
@@ -594,30 +711,48 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
594
711
|
onDragLeave: () => setComposerDragOver(false),
|
|
595
712
|
onDrop: handleFileDrop,
|
|
596
713
|
children: [
|
|
597
|
-
/* @__PURE__ */ jsxs("div", {
|
|
714
|
+
/* @__PURE__ */ jsxs("div", { className: s.composerToolbar, children: [
|
|
598
715
|
features.ai && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
599
|
-
/* @__PURE__ */ jsx("button", {
|
|
600
|
-
/* @__PURE__ */ jsx(
|
|
716
|
+
/* @__PURE__ */ jsx("button", { className: s.toolbarBtn, "data-tooltip": t("detail.iaSuggestion"), "aria-label": t("detail.iaSuggestion"), onClick: handleAiSuggest, disabled: aiReplying || messages.length === 0, style: { fontSize: 11, fontWeight: 700, padding: "4px 10px", width: "auto" }, children: aiReplying ? "..." : `\u2728 ${t("detail.iaSuggestion")}` }),
|
|
717
|
+
/* @__PURE__ */ jsx(RewriteDropdown, { disabled: aiRewriting || !replyBody.trim(), loading: aiRewriting, onSelect: (style) => handleAiRewrite(style), toolbarBtnClass: s.toolbarBtn })
|
|
718
|
+
] }),
|
|
719
|
+
/* @__PURE__ */ jsx(
|
|
720
|
+
CodeBlockInserter,
|
|
721
|
+
{
|
|
722
|
+
className: s.toolbarBtn,
|
|
723
|
+
onInsert: (block) => {
|
|
724
|
+
const nb = replyBody ? replyBody + block : block;
|
|
725
|
+
setReplyBody(nb);
|
|
726
|
+
setReplyHtml(nb.replace(/\n/g, "<br/>"));
|
|
727
|
+
editorRef.current?.setContent(nb.replace(/\n/g, "<br/>"));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
),
|
|
731
|
+
/* @__PURE__ */ jsxs("button", { className: s.toolbarBtn, "data-tooltip": t("detail.file"), "aria-label": t("detail.file"), onClick: () => fileInputRef.current?.click(), style: { fontSize: 11, fontWeight: 700, padding: "4px 10px", width: "auto" }, children: [
|
|
732
|
+
"\u{1F4CE} ",
|
|
733
|
+
t("detail.file")
|
|
601
734
|
] }),
|
|
602
|
-
/* @__PURE__ */ jsx("
|
|
603
|
-
/* @__PURE__ */ jsx("input", { ref: fileInputRef, type: "file", multiple: true, style: { display: "none" }, onChange: (e) => handleFileSelect(e.target.files) }),
|
|
735
|
+
/* @__PURE__ */ jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: s.hiddenFileInput, onChange: (e) => handleFileSelect(e.target.files) }),
|
|
604
736
|
macros.length > 0 && /* @__PURE__ */ jsxs(
|
|
605
737
|
"select",
|
|
606
738
|
{
|
|
607
|
-
|
|
739
|
+
className: s.cannedSelect,
|
|
608
740
|
onChange: (e) => {
|
|
609
741
|
const id = Number(e.target.value);
|
|
610
742
|
if (id) handleApplyMacro(id);
|
|
611
743
|
e.target.value = "";
|
|
612
744
|
},
|
|
613
745
|
disabled: applyingMacro,
|
|
746
|
+
style: { marginLeft: 0 },
|
|
747
|
+
"aria-label": "Appliquer une macro",
|
|
614
748
|
children: [
|
|
615
749
|
/* @__PURE__ */ jsx("option", { value: "", children: applyingMacro ? t("detail.applyingMacro") : t("detail.macros") }),
|
|
616
750
|
macros.map((m) => /* @__PURE__ */ jsx("option", { value: m.id, children: m.name }, m.id))
|
|
617
751
|
]
|
|
618
752
|
}
|
|
619
753
|
),
|
|
620
|
-
|
|
754
|
+
/* @__PURE__ */ jsx("span", { className: s.toolbarDivider }),
|
|
755
|
+
features.canned && cannedResponses.length > 0 && /* @__PURE__ */ jsxs("select", { className: s.cannedSelect, "aria-label": "R\xE9ponses types", onChange: (e) => {
|
|
621
756
|
const cr = cannedResponses.find((c) => String(c.id) === e.target.value);
|
|
622
757
|
if (cr) {
|
|
623
758
|
let b = cr.body;
|
|
@@ -625,6 +760,8 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
625
760
|
b = b.replace(/\{\{client\.firstName\}\}/g, client.firstName).replace(/\{\{client\.company\}\}/g, client.company);
|
|
626
761
|
}
|
|
627
762
|
setReplyBody(b);
|
|
763
|
+
setReplyHtml(b.replace(/\n/g, "<br/>"));
|
|
764
|
+
editorRef.current?.setContent(b.replace(/\n/g, "<br/>"));
|
|
628
765
|
}
|
|
629
766
|
e.target.value = "";
|
|
630
767
|
}, children: [
|
|
@@ -632,24 +769,19 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
632
769
|
cannedResponses.map((cr) => /* @__PURE__ */ jsx("option", { value: String(cr.id), children: cr.title }, cr.id))
|
|
633
770
|
] })
|
|
634
771
|
] }),
|
|
635
|
-
/* @__PURE__ */ jsx(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
onChange: (e) => setReplyBody(e.target.value),
|
|
641
|
-
placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" })
|
|
642
|
-
}
|
|
643
|
-
),
|
|
644
|
-
pendingFiles.length > 0 && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 6, flexWrap: "wrap", marginTop: 6 }, children: pendingFiles.map((f, i) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4, padding: "2px 8px", borderRadius: 4, background: "var(--theme-elevation-100)", fontSize: 11 }, children: [
|
|
772
|
+
/* @__PURE__ */ jsx(RichTextEditor, { ref: editorRef, onChange: (html, text) => {
|
|
773
|
+
setReplyHtml(html);
|
|
774
|
+
setReplyBody(text);
|
|
775
|
+
}, placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" }), minHeight: 100, borderColor: "transparent" }),
|
|
776
|
+
pendingFiles.length > 0 && /* @__PURE__ */ jsx("div", { className: s.uploadPreview, children: pendingFiles.map((f, i) => /* @__PURE__ */ jsxs("div", { className: s.uploadPreviewItem, children: [
|
|
645
777
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
646
778
|
"PJ ",
|
|
647
779
|
f.name
|
|
648
780
|
] }),
|
|
649
|
-
/* @__PURE__ */ jsx("button", {
|
|
781
|
+
/* @__PURE__ */ jsx("button", { className: s.uploadRemoveBtn, "aria-label": `Retirer ${f.name}`, onClick: () => setPendingFiles((prev) => prev.filter((_, j) => j !== i)), children: "\xD7" })
|
|
650
782
|
] }, i)) }),
|
|
651
|
-
/* @__PURE__ */ jsxs("div", {
|
|
652
|
-
/* @__PURE__ */ jsxs("div", {
|
|
783
|
+
/* @__PURE__ */ jsxs("div", { className: s.composerFooter, children: [
|
|
784
|
+
/* @__PURE__ */ jsxs("div", { className: s.composerOptions, children: [
|
|
653
785
|
/* @__PURE__ */ jsxs("label", { children: [
|
|
654
786
|
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => setIsInternal(e.target.checked) }),
|
|
655
787
|
" ",
|
|
@@ -661,46 +793,46 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
661
793
|
t("detail.notify")
|
|
662
794
|
] })
|
|
663
795
|
] }),
|
|
664
|
-
/* @__PURE__ */ jsx("button", {
|
|
796
|
+
/* @__PURE__ */ jsx("button", { className: `${s.sendBtn} ${isInternal ? s.sendBtnInternal : ""}`, onClick: handleSend, disabled: sending || !replyBody.trim(), "data-action": "send-reply", children: sending ? t("detail.sending") : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
|
|
665
797
|
] })
|
|
666
798
|
]
|
|
667
799
|
}
|
|
668
800
|
)
|
|
669
801
|
] }),
|
|
670
|
-
/* @__PURE__ */ jsxs("div", {
|
|
671
|
-
client && /* @__PURE__ */ jsxs("div", {
|
|
672
|
-
/* @__PURE__ */ jsxs("div", {
|
|
673
|
-
/* @__PURE__ */ jsx("div", {
|
|
674
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
675
|
-
/* @__PURE__ */ jsxs("div", {
|
|
802
|
+
/* @__PURE__ */ jsxs("div", { className: s.sidebar, children: [
|
|
803
|
+
client && /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
|
|
804
|
+
/* @__PURE__ */ jsxs("div", { className: s.clientCard, children: [
|
|
805
|
+
/* @__PURE__ */ jsx("div", { className: s.clientAvatar, children: initials }),
|
|
806
|
+
/* @__PURE__ */ jsxs("div", { className: s.clientInfo, children: [
|
|
807
|
+
/* @__PURE__ */ jsxs("div", { className: s.clientName, children: [
|
|
676
808
|
client.firstName,
|
|
677
809
|
" ",
|
|
678
810
|
client.lastName
|
|
679
811
|
] }),
|
|
680
|
-
/* @__PURE__ */ jsx("div", {
|
|
681
|
-
/* @__PURE__ */ jsx("a", { href: `mailto:${client.email}`,
|
|
812
|
+
/* @__PURE__ */ jsx("div", { className: s.clientCompany, children: client.company }),
|
|
813
|
+
/* @__PURE__ */ jsx("a", { href: `mailto:${client.email}`, className: s.clientEmail, children: client.email })
|
|
682
814
|
] })
|
|
683
815
|
] }),
|
|
684
|
-
/* @__PURE__ */ jsxs("div", {
|
|
685
|
-
/* @__PURE__ */ jsx(Link, { href: `/admin/collections/support-clients/${client.id}`,
|
|
686
|
-
/* @__PURE__ */ jsx("button", {
|
|
816
|
+
/* @__PURE__ */ jsxs("div", { className: s.clientActions, children: [
|
|
817
|
+
/* @__PURE__ */ jsx(Link, { href: `/admin/collections/support-clients/${client.id}`, className: s.smallBtn, children: t("client.clientSheet") }),
|
|
818
|
+
/* @__PURE__ */ jsx("button", { className: s.smallBtn, onClick: () => window.open(`/api/admin/impersonate?clientId=${client.id}`, "_blank"), children: t("client.clientPortal") })
|
|
687
819
|
] })
|
|
688
820
|
] }),
|
|
689
|
-
/* @__PURE__ */ jsxs("div", {
|
|
690
|
-
/* @__PURE__ */ jsx("div", {
|
|
691
|
-
/* @__PURE__ */ jsxs("div", {
|
|
692
|
-
/* @__PURE__ */ jsx("span", {
|
|
693
|
-
/* @__PURE__ */ jsxs("select", {
|
|
821
|
+
/* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
|
|
822
|
+
/* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: t("detail.details") }),
|
|
823
|
+
/* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
|
|
824
|
+
/* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.priority") }),
|
|
825
|
+
/* @__PURE__ */ jsxs("select", { className: s.sideSelect, value: ticket.priority || "normal", onChange: (e) => handleFieldPatch("priority", e.target.value), "aria-label": t("detail.priority"), children: [
|
|
694
826
|
/* @__PURE__ */ jsx("option", { value: "low", children: t("ticket.priority.low") }),
|
|
695
827
|
/* @__PURE__ */ jsx("option", { value: "normal", children: t("ticket.priority.normal") }),
|
|
696
828
|
/* @__PURE__ */ jsx("option", { value: "high", children: t("ticket.priority.high") }),
|
|
697
829
|
/* @__PURE__ */ jsx("option", { value: "urgent", children: t("ticket.priority.urgent") })
|
|
698
830
|
] })
|
|
699
831
|
] }),
|
|
700
|
-
/* @__PURE__ */ jsxs("div", {
|
|
701
|
-
/* @__PURE__ */ jsx("span", {
|
|
702
|
-
/* @__PURE__ */ jsxs("select", {
|
|
703
|
-
/* @__PURE__ */ jsx("option", { value: "", children: "
|
|
832
|
+
/* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
|
|
833
|
+
/* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.category") }),
|
|
834
|
+
/* @__PURE__ */ jsxs("select", { className: s.sideSelect, value: ticket.category || "", onChange: (e) => handleFieldPatch("category", e.target.value), "aria-label": t("detail.category"), children: [
|
|
835
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
|
|
704
836
|
/* @__PURE__ */ jsx("option", { value: "bug", children: t("ticket.category.bug") }),
|
|
705
837
|
/* @__PURE__ */ jsx("option", { value: "content", children: t("ticket.category.content") }),
|
|
706
838
|
/* @__PURE__ */ jsx("option", { value: "feature", children: t("ticket.category.feature") }),
|
|
@@ -708,26 +840,26 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
708
840
|
/* @__PURE__ */ jsx("option", { value: "hosting", children: t("ticket.category.hosting") })
|
|
709
841
|
] })
|
|
710
842
|
] }),
|
|
711
|
-
/* @__PURE__ */ jsxs("div", {
|
|
712
|
-
/* @__PURE__ */ jsx("span", {
|
|
713
|
-
/* @__PURE__ */ jsx("span", {
|
|
843
|
+
/* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
|
|
844
|
+
/* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.source") }),
|
|
845
|
+
/* @__PURE__ */ jsx("span", { className: s.sideValue, children: ticket.source || t("ticket.source.portal") })
|
|
714
846
|
] }),
|
|
715
|
-
/* @__PURE__ */ jsxs("div", {
|
|
716
|
-
/* @__PURE__ */ jsx("span", {
|
|
717
|
-
/* @__PURE__ */ jsx("span", {
|
|
847
|
+
/* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
|
|
848
|
+
/* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.assigned") }),
|
|
849
|
+
/* @__PURE__ */ jsx("span", { className: s.sideValue, children: typeof ticket.assignedTo === "object" && ticket.assignedTo ? ticket.assignedTo.firstName || "Admin" : "\u2014" })
|
|
718
850
|
] })
|
|
719
851
|
] }),
|
|
720
|
-
/* @__PURE__ */ jsxs("div", {
|
|
721
|
-
/* @__PURE__ */ jsx("div", {
|
|
722
|
-
/* @__PURE__ */ jsxs("div", {
|
|
723
|
-
tags.map((tag) => /* @__PURE__ */ jsxs("span", {
|
|
852
|
+
/* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
|
|
853
|
+
/* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: t("detail.tags") }),
|
|
854
|
+
/* @__PURE__ */ jsxs("div", { className: s.tagsWrap, children: [
|
|
855
|
+
tags.map((tag) => /* @__PURE__ */ jsxs("span", { className: s.tagChip, children: [
|
|
724
856
|
tag,
|
|
725
|
-
/* @__PURE__ */ jsx("button", {
|
|
857
|
+
/* @__PURE__ */ jsx("button", { className: s.tagRemove, "aria-label": `Retirer le tag ${tag}`, onClick: () => handleRemoveTag(tag), children: "\xD7" })
|
|
726
858
|
] }, tag)),
|
|
727
859
|
addingTag ? /* @__PURE__ */ jsx(
|
|
728
860
|
"input",
|
|
729
861
|
{
|
|
730
|
-
|
|
862
|
+
className: s.tagInput,
|
|
731
863
|
value: newTagValue,
|
|
732
864
|
onChange: (e) => setNewTagValue(e.target.value),
|
|
733
865
|
onKeyDown: (e) => {
|
|
@@ -741,35 +873,149 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
741
873
|
placeholder: "Tag...",
|
|
742
874
|
autoFocus: true
|
|
743
875
|
}
|
|
744
|
-
) : /* @__PURE__ */ jsx("button", {
|
|
876
|
+
) : /* @__PURE__ */ jsx("button", { className: s.tagAddBtn, onClick: () => setAddingTag(true), "aria-label": "Ajouter un tag", children: "+ Tag" })
|
|
745
877
|
] })
|
|
746
878
|
] }),
|
|
747
|
-
|
|
748
|
-
/* @__PURE__ */
|
|
879
|
+
/* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
|
|
880
|
+
/* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: "Facturation" }),
|
|
881
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
|
|
882
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
|
|
883
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Type" }),
|
|
884
|
+
/* @__PURE__ */ jsxs(
|
|
885
|
+
"select",
|
|
886
|
+
{
|
|
887
|
+
value: ticket?.billingType || "hourly",
|
|
888
|
+
onChange: async (e) => {
|
|
889
|
+
try {
|
|
890
|
+
await fetch(`/api/tickets/${ticketId}`, {
|
|
891
|
+
method: "PATCH",
|
|
892
|
+
credentials: "include",
|
|
893
|
+
headers: { "Content-Type": "application/json" },
|
|
894
|
+
body: JSON.stringify({ billingType: e.target.value })
|
|
895
|
+
});
|
|
896
|
+
fetchAll();
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", background: "#fff" },
|
|
901
|
+
children: [
|
|
902
|
+
/* @__PURE__ */ jsx("option", { value: "hourly", children: "Au temps" }),
|
|
903
|
+
/* @__PURE__ */ jsx("option", { value: "flat", children: "Forfait" })
|
|
904
|
+
]
|
|
905
|
+
}
|
|
906
|
+
)
|
|
907
|
+
] }),
|
|
908
|
+
ticket?.billingType === "flat" && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
|
|
909
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Montant forfait" }),
|
|
910
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
|
|
911
|
+
/* @__PURE__ */ jsx(
|
|
912
|
+
"input",
|
|
913
|
+
{
|
|
914
|
+
type: "number",
|
|
915
|
+
defaultValue: ticket?.flatRateAmount ?? "",
|
|
916
|
+
placeholder: "0",
|
|
917
|
+
onBlur: async (e) => {
|
|
918
|
+
const val = e.target.value ? Number(e.target.value) : null;
|
|
919
|
+
try {
|
|
920
|
+
await fetch(`/api/tickets/${ticketId}`, {
|
|
921
|
+
method: "PATCH",
|
|
922
|
+
credentials: "include",
|
|
923
|
+
headers: { "Content-Type": "application/json" },
|
|
924
|
+
body: JSON.stringify({ flatRateAmount: val })
|
|
925
|
+
});
|
|
926
|
+
fetchAll();
|
|
927
|
+
} catch {
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", width: 80, textAlign: "right" }
|
|
931
|
+
}
|
|
932
|
+
),
|
|
933
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "#9ca3af" }, children: "\u20AC" })
|
|
934
|
+
] })
|
|
935
|
+
] }),
|
|
936
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
|
|
937
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Montant factur\xE9" }),
|
|
938
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
|
|
939
|
+
/* @__PURE__ */ jsx(
|
|
940
|
+
"input",
|
|
941
|
+
{
|
|
942
|
+
type: "number",
|
|
943
|
+
defaultValue: ticket?.billedAmount ?? "",
|
|
944
|
+
placeholder: "0",
|
|
945
|
+
onBlur: async (e) => {
|
|
946
|
+
const val = e.target.value ? Number(e.target.value) : null;
|
|
947
|
+
try {
|
|
948
|
+
await fetch(`/api/tickets/${ticketId}`, {
|
|
949
|
+
method: "PATCH",
|
|
950
|
+
credentials: "include",
|
|
951
|
+
headers: { "Content-Type": "application/json" },
|
|
952
|
+
body: JSON.stringify({ billedAmount: val })
|
|
953
|
+
});
|
|
954
|
+
fetchAll();
|
|
955
|
+
} catch {
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", width: 80, textAlign: "right" }
|
|
959
|
+
}
|
|
960
|
+
),
|
|
961
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "#9ca3af" }, children: "\u20AC" })
|
|
962
|
+
] })
|
|
963
|
+
] }),
|
|
964
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
|
|
965
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Paiement" }),
|
|
966
|
+
/* @__PURE__ */ jsxs(
|
|
967
|
+
"select",
|
|
968
|
+
{
|
|
969
|
+
value: ticket?.paymentStatus || "unpaid",
|
|
970
|
+
onChange: async (e) => {
|
|
971
|
+
try {
|
|
972
|
+
await fetch(`/api/tickets/${ticketId}`, {
|
|
973
|
+
method: "PATCH",
|
|
974
|
+
credentials: "include",
|
|
975
|
+
headers: { "Content-Type": "application/json" },
|
|
976
|
+
body: JSON.stringify({ paymentStatus: e.target.value })
|
|
977
|
+
});
|
|
978
|
+
fetchAll();
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", background: "#fff" },
|
|
983
|
+
children: [
|
|
984
|
+
/* @__PURE__ */ jsx("option", { value: "unpaid", children: "Non pay\xE9" }),
|
|
985
|
+
/* @__PURE__ */ jsx("option", { value: "partial", children: "Partiel" }),
|
|
986
|
+
/* @__PURE__ */ jsx("option", { value: "paid", children: "Pay\xE9" })
|
|
987
|
+
]
|
|
988
|
+
}
|
|
989
|
+
)
|
|
990
|
+
] })
|
|
991
|
+
] })
|
|
992
|
+
] }),
|
|
993
|
+
features.timeTracking && /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
|
|
994
|
+
/* @__PURE__ */ jsxs("div", { className: s.sideSectionTitle, children: [
|
|
749
995
|
t("detail.time"),
|
|
750
996
|
" ",
|
|
751
997
|
/* @__PURE__ */ jsx("span", { style: { fontWeight: 700, fontSize: 13, color: "#d97706" }, children: totalMin > 0 ? `${Math.floor(totalMin / 60)}h${String(totalMin % 60).padStart(2, "0")} ${t("detail.total")}` : "0min" })
|
|
752
998
|
] }),
|
|
753
|
-
/* @__PURE__ */ jsxs("div", {
|
|
754
|
-
/* @__PURE__ */ jsxs("span", {
|
|
999
|
+
/* @__PURE__ */ jsxs("div", { className: s.timer, children: [
|
|
1000
|
+
/* @__PURE__ */ jsxs("span", { className: `${s.timerDisplay} ${timerRunning ? s.timerActive : ""}`, children: [
|
|
755
1001
|
String(Math.floor(timerSeconds / 60)).padStart(2, "0"),
|
|
756
1002
|
":",
|
|
757
1003
|
String(timerSeconds % 60).padStart(2, "0")
|
|
758
1004
|
] }),
|
|
759
|
-
!timerRunning ? /* @__PURE__ */ jsx("button", { style: {
|
|
760
|
-
timerSeconds >= 60 && !timerRunning && /* @__PURE__ */ jsxs("button", {
|
|
1005
|
+
!timerRunning ? /* @__PURE__ */ jsx("button", { className: s.timerBtn, onClick: () => setTimerRunning(true), style: { color: "#dc2626", borderColor: "#dc2626" }, "aria-label": "D\xE9marrer le timer", children: timerSeconds > 0 ? "\u25B6" : "\u25B6 Go" }) : /* @__PURE__ */ jsx("button", { className: s.timerBtn, onClick: () => setTimerRunning(false), "aria-label": "Mettre en pause le timer", children: "\u23F8" }),
|
|
1006
|
+
timerSeconds >= 60 && !timerRunning && /* @__PURE__ */ jsxs("button", { className: s.timerBtn, onClick: () => {
|
|
761
1007
|
handleTimerSave();
|
|
762
1008
|
localStorage.removeItem(`timer-sec-${ticketId}`);
|
|
763
1009
|
localStorage.removeItem(`timer-run-${ticketId}`);
|
|
764
|
-
}, children: [
|
|
765
|
-
"
|
|
1010
|
+
}, style: { color: "#16a34a", borderColor: "#16a34a" }, "aria-label": "Sauvegarder le temps", children: [
|
|
1011
|
+
"\u{1F4BE} ",
|
|
766
1012
|
Math.round(timerSeconds / 60),
|
|
767
1013
|
"m"
|
|
768
1014
|
] })
|
|
769
1015
|
] }),
|
|
770
1016
|
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" }, children: [
|
|
771
1017
|
/* @__PURE__ */ jsx("input", { type: "number", min: "1", placeholder: "min", style: { width: 60, padding: "4px 8px", borderRadius: 6, border: "1px solid var(--theme-elevation-200)", fontSize: 12, color: "var(--theme-text)", background: "var(--theme-elevation-0)" }, id: "manual-time-input" }),
|
|
772
|
-
/* @__PURE__ */ jsx("button", {
|
|
1018
|
+
/* @__PURE__ */ jsx("button", { className: s.timerBtn, onClick: async () => {
|
|
773
1019
|
const input = document.getElementById("manual-time-input");
|
|
774
1020
|
const mins = Number(input?.value);
|
|
775
1021
|
if (!mins || mins < 1 || !ticketId) return;
|
|
@@ -777,10 +1023,36 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
777
1023
|
await fetch("/api/time-entries", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ ticket: Number(ticketId), duration: mins, date: (/* @__PURE__ */ new Date()).toISOString(), description: "Saisie manuelle" }) });
|
|
778
1024
|
if (input) input.value = "";
|
|
779
1025
|
fetchAll();
|
|
780
|
-
} catch
|
|
781
|
-
console.warn("[support] manual time entry error:", err);
|
|
1026
|
+
} catch {
|
|
782
1027
|
}
|
|
783
|
-
}, children: "+ Ajouter" })
|
|
1028
|
+
}, style: { fontSize: 11 }, children: "+ Ajouter" })
|
|
1029
|
+
] }),
|
|
1030
|
+
/* @__PURE__ */ jsxs("div", { style: { marginTop: 8, fontSize: 11, color: "var(--theme-elevation-500)" }, children: [
|
|
1031
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "2px 0", alignItems: "center" }, children: [
|
|
1032
|
+
/* @__PURE__ */ jsx("span", { children: "Facturable" }),
|
|
1033
|
+
/* @__PURE__ */ jsx(
|
|
1034
|
+
"button",
|
|
1035
|
+
{
|
|
1036
|
+
onClick: async () => {
|
|
1037
|
+
const newVal = ticket.billable === false ? true : false;
|
|
1038
|
+
try {
|
|
1039
|
+
await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ billable: newVal }) });
|
|
1040
|
+
fetchAll();
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
style: { fontWeight: 600, color: ticket.billable !== false ? "#16a34a" : "#dc2626", background: "none", border: "none", cursor: "pointer", fontSize: 11, textDecoration: "underline" },
|
|
1045
|
+
children: ticket.billable !== false ? "Oui" : "Non"
|
|
1046
|
+
}
|
|
1047
|
+
)
|
|
1048
|
+
] }),
|
|
1049
|
+
totalMin > 0 && /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "2px 0" }, children: [
|
|
1050
|
+
/* @__PURE__ */ jsx("span", { children: "Montant estim\xE9" }),
|
|
1051
|
+
/* @__PURE__ */ jsxs("span", { style: { fontWeight: 700, color: "var(--theme-text)" }, children: [
|
|
1052
|
+
(totalMin / 60 * 60).toFixed(0),
|
|
1053
|
+
"\u20AC"
|
|
1054
|
+
] })
|
|
1055
|
+
] })
|
|
784
1056
|
] }),
|
|
785
1057
|
timeEntries.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginTop: 8, fontSize: 11 }, children: timeEntries.slice(0, 6).map((e) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "3px 0", color: "var(--theme-elevation-500)" }, children: [
|
|
786
1058
|
/* @__PURE__ */ jsx("span", { children: new Date(e.date).toLocaleDateString("fr-FR", { day: "numeric", month: "short" }) }),
|
|
@@ -790,52 +1062,62 @@ ${uploadedLinks.join("\n")}` : replyBody.trim();
|
|
|
790
1062
|
] })
|
|
791
1063
|
] }, e.id)) })
|
|
792
1064
|
] }),
|
|
793
|
-
features.activityLog && /* @__PURE__ */ jsxs("div", {
|
|
794
|
-
/* @__PURE__ */ jsx("div", {
|
|
795
|
-
"
|
|
796
|
-
showActivity ? "
|
|
1065
|
+
features.activityLog && /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
|
|
1066
|
+
/* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: /* @__PURE__ */ jsxs("button", { className: s.collapseBtn, onClick: () => setShowActivity(!showActivity), "aria-label": showActivity ? "Masquer le journal" : "Afficher le journal", children: [
|
|
1067
|
+
"Activit\xE9 ",
|
|
1068
|
+
showActivity ? "\u25BE" : "\u25B8"
|
|
797
1069
|
] }) }),
|
|
798
|
-
showActivity && activityLog.slice(0, 8).map((a) => /* @__PURE__ */ jsxs("div", {
|
|
799
|
-
/* @__PURE__ */ jsx("div", {
|
|
800
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
801
|
-
/* @__PURE__ */ jsx("div", {
|
|
802
|
-
/* @__PURE__ */ jsx("div", {
|
|
1070
|
+
showActivity && activityLog.slice(0, 8).map((a) => /* @__PURE__ */ jsxs("div", { className: s.activityItem, children: [
|
|
1071
|
+
/* @__PURE__ */ jsx("div", { className: s.activityDot, style: { backgroundColor: a.actorType === "admin" ? "#2563eb" : a.actorType === "system" ? "#6b7280" : "#16a34a" } }),
|
|
1072
|
+
/* @__PURE__ */ jsxs("div", { className: s.activityContent, children: [
|
|
1073
|
+
/* @__PURE__ */ jsx("div", { className: s.activityText, children: (a.detail || a.action).slice(0, 60) }),
|
|
1074
|
+
/* @__PURE__ */ jsx("div", { className: s.activityTime, children: timeAgo(a.createdAt) })
|
|
803
1075
|
] })
|
|
804
1076
|
] }, a.id))
|
|
805
|
-
] })
|
|
1077
|
+
] }),
|
|
1078
|
+
clientSummary && clientSummary.summary && /* @__PURE__ */ jsxs("div", { className: s.sideSection, style: { background: "linear-gradient(135deg, rgba(37,99,235,0.03) 0%, rgba(139,92,246,0.03) 100%)" }, children: [
|
|
1079
|
+
/* @__PURE__ */ jsxs("div", { className: s.sideSectionTitle, style: { display: "flex", alignItems: "center", gap: 4 }, children: [
|
|
1080
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 13 }, children: "\u{1F9E0}" }),
|
|
1081
|
+
" R\xE9sum\xE9 client"
|
|
1082
|
+
] }),
|
|
1083
|
+
/* @__PURE__ */ jsx("p", { style: { margin: "0 0 8px", fontSize: 11, lineHeight: 1.6, color: "#374151" }, children: clientSummary.summary }),
|
|
1084
|
+
clientSummary.recurringTopics && clientSummary.recurringTopics.length > 0 && /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 6 }, children: clientSummary.recurringTopics.slice(0, 3).map((tp, i) => /* @__PURE__ */ jsx("span", { style: { padding: "2px 8px", borderRadius: 10, background: "rgba(37,99,235,0.08)", color: "#2563eb", fontSize: 10, fontWeight: 600 }, children: tp.topic }, i)) }),
|
|
1085
|
+
clientSummary.keyFacts && clientSummary.keyFacts.length > 0 && /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4 }, children: clientSummary.keyFacts.slice(0, 3).map((f, i) => /* @__PURE__ */ jsx("span", { style: { padding: "2px 8px", borderRadius: 10, background: "rgba(22,163,74,0.08)", color: "#16a34a", fontSize: 10, fontWeight: 600 }, children: f }, i)) })
|
|
1086
|
+
] }),
|
|
1087
|
+
summaryLoading && /* @__PURE__ */ jsx("div", { className: s.sideSection, children: /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#94a3b8", textAlign: "center", padding: 8 }, children: "\u{1F9E0} Chargement r\xE9sum\xE9..." }) })
|
|
806
1088
|
] })
|
|
807
1089
|
] }),
|
|
808
|
-
undoToast && /* @__PURE__ */ jsxs("div", {
|
|
809
|
-
/* @__PURE__ */ jsx("span", { children:
|
|
810
|
-
/* @__PURE__ */ jsx("button", {
|
|
1090
|
+
undoToast && /* @__PURE__ */ jsxs("div", { className: s.undoToast, role: "alert", children: [
|
|
1091
|
+
/* @__PURE__ */ jsx("span", { children: "Message supprim\xE9" }),
|
|
1092
|
+
/* @__PURE__ */ jsx("button", { className: s.undoBtn, onClick: handleUndoDelete, children: "Annuler" })
|
|
811
1093
|
] }),
|
|
812
|
-
splitModal && /* @__PURE__ */ jsx("div", {
|
|
1094
|
+
splitModal && /* @__PURE__ */ jsx("div", { className: s.splitOverlay, onClick: (e) => {
|
|
813
1095
|
if (e.target === e.currentTarget) {
|
|
814
1096
|
setSplitModal(null);
|
|
815
1097
|
setSplitSubject("");
|
|
816
1098
|
}
|
|
817
|
-
}, children: /* @__PURE__ */ jsxs("div", {
|
|
818
|
-
/* @__PURE__ */ jsx("h3", {
|
|
819
|
-
/* @__PURE__ */ jsx("div", {
|
|
1099
|
+
}, children: /* @__PURE__ */ jsxs("div", { className: s.splitModal, role: "dialog", "aria-label": "Extraire dans un nouveau ticket", children: [
|
|
1100
|
+
/* @__PURE__ */ jsx("h3", { className: s.splitTitle, children: "Extraire dans un nouveau ticket" }),
|
|
1101
|
+
/* @__PURE__ */ jsx("div", { className: s.splitPreview, children: splitModal.preview }),
|
|
820
1102
|
/* @__PURE__ */ jsx(
|
|
821
1103
|
"input",
|
|
822
1104
|
{
|
|
823
|
-
|
|
1105
|
+
className: s.splitInput,
|
|
824
1106
|
value: splitSubject,
|
|
825
1107
|
onChange: (e) => setSplitSubject(e.target.value),
|
|
826
1108
|
onKeyDown: (e) => {
|
|
827
1109
|
if (e.key === "Enter") handleSplitConfirm();
|
|
828
1110
|
},
|
|
829
|
-
placeholder:
|
|
1111
|
+
placeholder: "Sujet du nouveau ticket...",
|
|
830
1112
|
autoFocus: true
|
|
831
1113
|
}
|
|
832
1114
|
),
|
|
833
|
-
/* @__PURE__ */ jsxs("div", {
|
|
834
|
-
/* @__PURE__ */ jsx("button", {
|
|
1115
|
+
/* @__PURE__ */ jsxs("div", { className: s.splitActions, children: [
|
|
1116
|
+
/* @__PURE__ */ jsx("button", { className: s.splitCancelBtn, onClick: () => {
|
|
835
1117
|
setSplitModal(null);
|
|
836
1118
|
setSplitSubject("");
|
|
837
|
-
}, children:
|
|
838
|
-
/* @__PURE__ */ jsx("button", {
|
|
1119
|
+
}, children: "Annuler" }),
|
|
1120
|
+
/* @__PURE__ */ jsx("button", { className: s.splitConfirmBtn, onClick: handleSplitConfirm, disabled: !splitSubject.trim(), children: "Cr\xE9er le ticket" })
|
|
839
1121
|
] })
|
|
840
1122
|
] }) })
|
|
841
1123
|
] });
|