@consilioweb/payload-support 0.6.0 → 0.6.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.
Files changed (84) hide show
  1. package/dist/{styles/theme.css → components/TicketConversation.css} +13 -326
  2. package/dist/components/TicketConversation.js +3004 -0
  3. package/dist/index.cjs +11 -11
  4. package/dist/index.js +11 -11
  5. package/dist/views.cjs +6184 -0
  6. package/dist/{views/TicketingSettingsView/TicketingSettings.module.scss → views.css} +66 -138
  7. package/dist/views.d.cts +30 -0
  8. package/dist/views.d.ts +30 -0
  9. package/dist/views.js +6165 -0
  10. package/package.json +14 -7
  11. package/src/plugin.ts +11 -11
  12. package/src/views.ts +17 -0
  13. package/dist/components/TicketConversation/components/AISummaryPanel.js +0 -76
  14. package/dist/components/TicketConversation/components/ActionPanels.js +0 -118
  15. package/dist/components/TicketConversation/components/ActivityLog.js +0 -23
  16. package/dist/components/TicketConversation/components/ClientBar.js +0 -42
  17. package/dist/components/TicketConversation/components/ClientHistory.js +0 -137
  18. package/dist/components/TicketConversation/components/CodeBlock.js +0 -152
  19. package/dist/components/TicketConversation/components/CodeBlockInserter.js +0 -155
  20. package/dist/components/TicketConversation/components/QuickActions.js +0 -65
  21. package/dist/components/TicketConversation/components/TicketHeader.js +0 -92
  22. package/dist/components/TicketConversation/components/TimeTrackingPanel.js +0 -133
  23. package/dist/components/TicketConversation/config.js +0 -41
  24. package/dist/components/TicketConversation/constants.js +0 -96
  25. package/dist/components/TicketConversation/context.js +0 -11
  26. package/dist/components/TicketConversation/hooks/useAI.js +0 -176
  27. package/dist/components/TicketConversation/hooks/useMessageActions.js +0 -135
  28. package/dist/components/TicketConversation/hooks/useReply.js +0 -187
  29. package/dist/components/TicketConversation/hooks/useTicketActions.js +0 -230
  30. package/dist/components/TicketConversation/hooks/useTimeTracking.js +0 -122
  31. package/dist/components/TicketConversation/hooks/useTranslation.js +0 -70
  32. package/dist/components/TicketConversation/index.js +0 -1123
  33. package/dist/components/TicketConversation/locales/en.json +0 -878
  34. package/dist/components/TicketConversation/locales/fr.json +0 -878
  35. package/dist/components/TicketConversation/types.js +0 -2
  36. package/dist/components/TicketConversation/utils.js +0 -25
  37. package/dist/styles/BillingView.module.scss +0 -311
  38. package/dist/styles/ChatView.module.scss +0 -438
  39. package/dist/styles/CommandPalette.module.scss +0 -160
  40. package/dist/styles/CrmView.module.scss +0 -554
  41. package/dist/styles/EmailTracking.module.scss +0 -238
  42. package/dist/styles/ImportConversation.module.scss +0 -267
  43. package/dist/styles/Layout.module.scss +0 -55
  44. package/dist/styles/Logs.module.scss +0 -164
  45. package/dist/styles/NewTicket.module.scss +0 -143
  46. package/dist/styles/PendingEmails.module.scss +0 -629
  47. package/dist/styles/SupportDashboard.module.scss +0 -649
  48. package/dist/styles/TicketDetail.module.scss +0 -1050
  49. package/dist/styles/TicketInbox.module.scss +0 -296
  50. package/dist/styles/TicketingSettings.module.scss +0 -358
  51. package/dist/styles/TimeDashboard.module.scss +0 -287
  52. package/dist/styles/_tokens.scss +0 -78
  53. package/dist/views/BillingView/client.js +0 -204
  54. package/dist/views/BillingView/index.js +0 -29
  55. package/dist/views/ChatView/client.js +0 -252
  56. package/dist/views/ChatView/index.js +0 -29
  57. package/dist/views/CrmView/client.js +0 -232
  58. package/dist/views/CrmView/index.js +0 -29
  59. package/dist/views/EmailTrackingView/client.js +0 -154
  60. package/dist/views/EmailTrackingView/index.js +0 -29
  61. package/dist/views/ImportConversationView/client.js +0 -204
  62. package/dist/views/ImportConversationView/index.js +0 -29
  63. package/dist/views/LogsView/client.js +0 -148
  64. package/dist/views/LogsView/index.js +0 -27
  65. package/dist/views/NewTicketView/client.js +0 -224
  66. package/dist/views/NewTicketView/index.js +0 -27
  67. package/dist/views/PendingEmailsView/client.js +0 -172
  68. package/dist/views/PendingEmailsView/index.js +0 -29
  69. package/dist/views/SupportDashboardView/client.js +0 -296
  70. package/dist/views/SupportDashboardView/index.js +0 -29
  71. package/dist/views/TicketDetailView/client.js +0 -844
  72. package/dist/views/TicketDetailView/index.js +0 -29
  73. package/dist/views/TicketInboxView/client.js +0 -294
  74. package/dist/views/TicketInboxView/index.js +0 -27
  75. package/dist/views/TicketingSettingsView/client.js +0 -728
  76. package/dist/views/TicketingSettingsView/index.js +0 -29
  77. package/dist/views/TimeDashboardView/client.js +0 -164
  78. package/dist/views/TimeDashboardView/index.js +0 -29
  79. package/dist/views/shared/AdminViewHeader.js +0 -67
  80. package/dist/views/shared/ErrorBoundary.js +0 -50
  81. package/dist/views/shared/Skeleton.js +0 -72
  82. package/dist/views/shared/adminTokens.js +0 -32
  83. package/dist/views/shared/config.js +0 -41
  84. package/dist/views/shared/index.js +0 -6
@@ -1,844 +0,0 @@
1
- "use client";
2
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
- import React, { useRef, useState, useCallback, useEffect } from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
- import Link from 'next/link';
6
- import { getFeatures } from '../shared/config';
7
- import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation';
8
-
9
- const STATUS = {
10
- open: { label: "Ouvert", bg: "#dbeafe", color: "#1e40af" },
11
- waiting_client: { label: "En attente", bg: "#fef3c7", color: "#92400e" },
12
- resolved: { label: "Resolu", bg: "#dcfce7", color: "#166534" }
13
- };
14
- function timeAgo(d) {
15
- const date = new Date(d), now = /* @__PURE__ */ new Date();
16
- if (date.toDateString() === now.toDateString()) return date.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" });
17
- const y = new Date(now);
18
- y.setDate(y.getDate() - 1);
19
- if (date.toDateString() === y.toDateString()) return `Hier, ${date.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}`;
20
- return date.toLocaleDateString("fr-FR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
21
- }
22
- function dateLabel(d) {
23
- const date = new Date(d), now = /* @__PURE__ */ new Date();
24
- if (date.toDateString() === now.toDateString()) return "Aujourd'hui";
25
- const y = new Date(now);
26
- y.setDate(y.getDate() - 1);
27
- if (date.toDateString() === y.toDateString()) return "Hier";
28
- return date.toLocaleDateString("fr-FR", { day: "numeric", month: "long" });
29
- }
30
- const TicketDetailClient = () => {
31
- const { t } = useTranslation();
32
- const searchParams = useSearchParams();
33
- const ticketId = searchParams.get("id");
34
- const features = getFeatures();
35
- const threadEndRef = useRef(null);
36
- const dropdownRef = useRef(null);
37
- const fileInputRef = useRef(null);
38
- const [ticket, setTicket] = useState(null);
39
- const [messages, setMessages] = useState([]);
40
- const [client, setClient] = useState(null);
41
- const [timeEntries, setTimeEntries] = useState([]);
42
- const [activityLog, setActivityLog] = useState([]);
43
- const [cannedResponses, setCannedResponses] = useState([]);
44
- const [loading, setLoading] = useState(true);
45
- const [replyBody, setReplyBody] = useState("");
46
- const [isInternal, setIsInternal] = useState(false);
47
- const [notifyClient, setNotifyClient] = useState(true);
48
- const [sending, setSending] = useState(false);
49
- const [showMenu, setShowMenu] = useState(false);
50
- const [clientTyping, setClientTyping] = useState(false);
51
- const [aiReplying, setAiReplying] = useState(false);
52
- const [aiRewriting, setAiRewriting] = useState(false);
53
- const [sentiment, setSentiment] = useState(null);
54
- const [statusUpdating, setStatusUpdating] = useState(false);
55
- const [showActivity, setShowActivity] = useState(false);
56
- const [timerRunning, setTimerRunning] = useState(() => {
57
- if (typeof window === "undefined" || !ticketId) return false;
58
- return localStorage.getItem(`timer-run-${ticketId}`) === "1";
59
- });
60
- const [timerSeconds, setTimerSeconds] = useState(() => {
61
- if (typeof window === "undefined" || !ticketId) return 0;
62
- const s2 = Number(localStorage.getItem(`timer-sec-${ticketId}`) || 0);
63
- const t2 = Number(localStorage.getItem(`timer-ts-${ticketId}`) || 0);
64
- const running = localStorage.getItem(`timer-run-${ticketId}`) === "1";
65
- return running && t2 ? s2 + Math.floor((Date.now() - t2) / 1e3) : s2;
66
- });
67
- const timerRef = useRef(null);
68
- const [otherViewers, setOtherViewers] = useState([]);
69
- const [macros, setMacros] = useState([]);
70
- const [applyingMacro, setApplyingMacro] = useState(false);
71
- const [undoToast, setUndoToast] = useState(null);
72
- const [splitModal, setSplitModal] = useState(null);
73
- const [splitSubject, setSplitSubject] = useState("");
74
- const [pendingFiles, setPendingFiles] = useState([]);
75
- const [composerDragOver, setComposerDragOver] = useState(false);
76
- const [tags, setTags] = useState([]);
77
- const [addingTag, setAddingTag] = useState(false);
78
- const [newTagValue, setNewTagValue] = useState("");
79
- const fetchAll = useCallback(async () => {
80
- if (!ticketId) return;
81
- try {
82
- const [mr, tr, ter, ar, cr] = await Promise.all([
83
- fetch(`/api/ticket-messages?where[ticket][equals]=${ticketId}&sort=createdAt&limit=200&depth=1`, { credentials: "include" }),
84
- fetch(`/api/tickets/${ticketId}?depth=1`, { credentials: "include" }),
85
- fetch(`/api/time-entries?where[ticket][equals]=${ticketId}&sort=-date&limit=50&depth=0`, { credentials: "include" }),
86
- fetch(`/api/ticket-activity-log?where[ticket][equals]=${ticketId}&sort=-createdAt&limit=30&depth=0`, { credentials: "include" }),
87
- fetch("/api/canned-responses?sort=sortOrder&limit=50&depth=0", { credentials: "include" })
88
- ]);
89
- if (mr.ok) {
90
- const d = await mr.json();
91
- setMessages(d.docs || []);
92
- }
93
- if (tr.ok) {
94
- const d = await tr.json();
95
- setTicket(d);
96
- if (d.client && typeof d.client === "object") setClient(d.client);
97
- if (Array.isArray(d.tags)) setTags(d.tags.map((t2) => typeof t2 === "object" ? t2.tag || "" : t2).filter(Boolean));
98
- }
99
- if (ter.ok) {
100
- const d = await ter.json();
101
- setTimeEntries(d.docs || []);
102
- }
103
- if (ar.ok) {
104
- const d = await ar.json();
105
- setActivityLog(d.docs || []);
106
- }
107
- if (cr.ok) {
108
- const d = await cr.json();
109
- setCannedResponses(d.docs || []);
110
- }
111
- } catch {
112
- }
113
- setLoading(false);
114
- }, [ticketId]);
115
- useEffect(() => {
116
- fetchAll();
117
- }, [fetchAll]);
118
- useEffect(() => {
119
- if (!ticketId || loading) return;
120
- const iv = setInterval(async () => {
121
- try {
122
- const [mr, tr] = await Promise.all([
123
- fetch(`/api/ticket-messages?where[ticket][equals]=${ticketId}&sort=createdAt&limit=200&depth=1`, { credentials: "include" }),
124
- fetch(`/api/tickets/${ticketId}?depth=0`, { credentials: "include" })
125
- ]);
126
- if (mr.ok) {
127
- const d = await mr.json();
128
- setMessages(d.docs || []);
129
- }
130
- if (tr.ok) {
131
- const d = await tr.json();
132
- setTicket((p) => p ? { ...p, ...d } : d);
133
- }
134
- } catch {
135
- }
136
- }, 1e4);
137
- return () => clearInterval(iv);
138
- }, [ticketId, loading]);
139
- useEffect(() => {
140
- if (!ticketId) return;
141
- fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ lastAdminReadAt: (/* @__PURE__ */ new Date()).toISOString() }) }).catch(() => {
142
- });
143
- }, [ticketId, messages.length]);
144
- useEffect(() => {
145
- if (!ticketId) return;
146
- const iv = setInterval(async () => {
147
- try {
148
- const r = await fetch(`/api/support/typing?ticketId=${ticketId}`, { credentials: "include" });
149
- if (r.ok) {
150
- const d = await r.json();
151
- setClientTyping(d.typing);
152
- }
153
- } catch (err) {
154
- console.warn("[support] typing poll error:", err);
155
- }
156
- }, 2e3);
157
- return () => clearInterval(iv);
158
- }, [ticketId]);
159
- useEffect(() => {
160
- if (!features.ai || messages.length === 0) return;
161
- const last = [...messages].reverse().find((m) => m.authorType === "client" || m.authorType === "email");
162
- if (!last) return;
163
- fetch("/api/support/ai", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ action: "sentiment", text: last.body.slice(0, 500) }) }).then((r) => r.json()).then((d) => {
164
- const raw = (d.sentiment || "").toLowerCase();
165
- const map = {
166
- "frustre": { emoji: ":(", label: "Frustre", color: "#dc2626" },
167
- "mecontent": { emoji: ":(", label: "Mecontent", color: "#ea580c" },
168
- "urgent": { emoji: "!", label: "Urgent", color: "#dc2626" },
169
- "neutre": { emoji: "-", label: "Neutre", color: "#6b7280" },
170
- "satisfait": { emoji: ":)", label: "Satisfait", color: "#16a34a" }
171
- };
172
- const m = Object.keys(map).find((k) => raw.includes(k));
173
- setSentiment(m ? map[m] : { emoji: "-", label: "Neutre", color: "#6b7280" });
174
- }).catch(() => {
175
- });
176
- }, [messages.length, features.ai]);
177
- useEffect(() => {
178
- threadEndRef.current?.scrollIntoView({ behavior: "smooth" });
179
- }, [messages.length]);
180
- useEffect(() => {
181
- if (timerRunning) {
182
- localStorage.setItem(`timer-run-${ticketId}`, "1");
183
- localStorage.setItem(`timer-ts-${ticketId}`, String(Date.now()));
184
- localStorage.setItem(`timer-sec-${ticketId}`, String(timerSeconds));
185
- timerRef.current = setInterval(() => {
186
- setTimerSeconds((p) => {
187
- const next = p + 1;
188
- localStorage.setItem(`timer-sec-${ticketId}`, String(next));
189
- localStorage.setItem(`timer-ts-${ticketId}`, String(Date.now()));
190
- return next;
191
- });
192
- }, 1e3);
193
- } else {
194
- localStorage.setItem(`timer-run-${ticketId}`, "0");
195
- localStorage.setItem(`timer-sec-${ticketId}`, String(timerSeconds));
196
- if (timerRef.current) clearInterval(timerRef.current);
197
- }
198
- return () => {
199
- if (timerRef.current) clearInterval(timerRef.current);
200
- };
201
- }, [timerRunning, ticketId]);
202
- useEffect(() => {
203
- if (!ticketId) return;
204
- const join = () => fetch("/api/support/presence", {
205
- method: "POST",
206
- headers: { "Content-Type": "application/json" },
207
- credentials: "include",
208
- body: JSON.stringify({ ticketId: Number(ticketId), action: "join" })
209
- }).catch(() => {
210
- });
211
- join();
212
- const heartbeat = setInterval(join, 2e4);
213
- return () => {
214
- clearInterval(heartbeat);
215
- fetch("/api/support/presence", {
216
- method: "POST",
217
- headers: { "Content-Type": "application/json" },
218
- credentials: "include",
219
- body: JSON.stringify({ ticketId: Number(ticketId), action: "leave" })
220
- }).catch(() => {
221
- });
222
- };
223
- }, [ticketId]);
224
- useEffect(() => {
225
- if (!ticketId) return;
226
- const poll = setInterval(async () => {
227
- try {
228
- const r = await fetch(`/api/support/presence?ticketId=${ticketId}`, { credentials: "include" });
229
- if (r.ok) {
230
- const d = await r.json();
231
- setOtherViewers(d.viewers || []);
232
- }
233
- } catch {
234
- }
235
- }, 5e3);
236
- return () => clearInterval(poll);
237
- }, [ticketId]);
238
- useEffect(() => {
239
- fetch("/api/macros?where[isActive][equals]=true&depth=0&limit=50", { credentials: "include" }).then((r) => r.ok ? r.json() : null).then((d) => {
240
- if (d?.docs) setMacros(d.docs.map((m) => ({ id: m.id, name: m.name })));
241
- }).catch(() => {
242
- });
243
- }, []);
244
- useEffect(() => {
245
- if (!showMenu) return;
246
- const handler = (e) => {
247
- if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
248
- setShowMenu(false);
249
- }
250
- };
251
- document.addEventListener("mousedown", handler);
252
- return () => document.removeEventListener("mousedown", handler);
253
- }, [showMenu]);
254
- useEffect(() => {
255
- const handler = (e) => {
256
- const mod = e.metaKey || e.ctrlKey;
257
- if (mod && e.key === "Enter") {
258
- e.preventDefault();
259
- const sendBtn = document.querySelector('[data-action="send-reply"]');
260
- if (sendBtn && !sendBtn.disabled) sendBtn.click();
261
- }
262
- if (mod && e.shiftKey && e.key.toLowerCase() === "n") {
263
- e.preventDefault();
264
- setIsInternal((prev) => !prev);
265
- }
266
- if (e.key === "Escape") {
267
- setShowMenu(false);
268
- setSplitModal(null);
269
- }
270
- };
271
- document.addEventListener("keydown", handler);
272
- return () => document.removeEventListener("keydown", handler);
273
- }, []);
274
- const handleSend = async () => {
275
- if (!replyBody.trim() || !ticketId) return;
276
- setSending(true);
277
- try {
278
- const uploadedLinks = [];
279
- for (const file of pendingFiles) {
280
- const formData = new FormData();
281
- formData.append("file", file);
282
- formData.append("_payload", JSON.stringify({ alt: file.name }));
283
- try {
284
- const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
285
- if (ur.ok) {
286
- const ud = await ur.json();
287
- if (ud.doc?.url) uploadedLinks.push(`[${ud.doc.filename || file.name}](${ud.doc.url})`);
288
- }
289
- } catch {
290
- }
291
- }
292
- const finalBody = uploadedLinks.length > 0 ? `${replyBody.trim()}
293
-
294
- ${uploadedLinks.join("\n")}` : replyBody.trim();
295
- const res = await fetch("/api/ticket-messages", {
296
- method: "POST",
297
- headers: { "Content-Type": "application/json" },
298
- credentials: "include",
299
- body: JSON.stringify({ ticket: Number(ticketId), body: finalBody, authorType: "admin", isInternal, skipNotification: isInternal || !notifyClient })
300
- });
301
- if (res.ok) {
302
- setReplyBody("");
303
- setIsInternal(false);
304
- setPendingFiles([]);
305
- fetchAll();
306
- }
307
- } catch (err) {
308
- console.warn("[support] send reply error:", err);
309
- } finally {
310
- setSending(false);
311
- }
312
- };
313
- const handleStatusChange = async (v) => {
314
- if (!ticketId) return;
315
- setStatusUpdating(true);
316
- try {
317
- await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ status: v }) });
318
- fetchAll();
319
- } catch (err) {
320
- console.warn("[support] status change error:", err);
321
- } finally {
322
- setStatusUpdating(false);
323
- }
324
- };
325
- const handleFieldPatch = async (field, value) => {
326
- if (!ticketId) return;
327
- try {
328
- await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ [field]: value }) });
329
- fetchAll();
330
- } catch {
331
- }
332
- };
333
- const handleDeleteMessage = (msgId) => {
334
- if (undoToast) clearTimeout(undoToast.timer);
335
- const timer = setTimeout(() => {
336
- fetch(`/api/ticket-messages/${msgId}`, { method: "DELETE", credentials: "include" }).then(() => fetchAll());
337
- setUndoToast(null);
338
- }, 5e3);
339
- setUndoToast({ msgId, timer });
340
- };
341
- const handleUndoDelete = () => {
342
- if (undoToast) {
343
- clearTimeout(undoToast.timer);
344
- setUndoToast(null);
345
- }
346
- };
347
- const handleSplitConfirm = async () => {
348
- if (!splitModal || !splitSubject.trim()) return;
349
- try {
350
- const r = await fetch("/api/support/split-ticket", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ messageId: splitModal.messageId, subject: splitSubject }) });
351
- const d = await r.json();
352
- if (d.ticketNumber) {
353
- fetchAll();
354
- }
355
- } catch {
356
- }
357
- setSplitModal(null);
358
- setSplitSubject("");
359
- };
360
- const handleAiSuggest = async () => {
361
- if (messages.length === 0) return;
362
- setAiReplying(true);
363
- try {
364
- const r = await fetch("/api/support/ai", {
365
- method: "POST",
366
- headers: { "Content-Type": "application/json" },
367
- credentials: "include",
368
- body: JSON.stringify({ action: "suggest_reply", messages: messages.slice(-10).map((m) => ({ authorType: m.authorType, body: m.body })), clientName: `${client?.firstName || ""} ${client?.lastName || ""}`.trim(), clientCompany: client?.company })
369
- });
370
- if (r.ok) {
371
- const d = await r.json();
372
- if (d.reply) {
373
- setReplyBody(d.reply);
374
- }
375
- }
376
- } catch (err) {
377
- console.warn("[support] AI suggest error:", err);
378
- } finally {
379
- setAiReplying(false);
380
- }
381
- };
382
- const handleAiRewrite = async () => {
383
- if (!replyBody.trim()) return;
384
- setAiRewriting(true);
385
- 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 }) });
387
- if (r.ok) {
388
- const d = await r.json();
389
- if (d.rewritten) {
390
- setReplyBody(d.rewritten);
391
- }
392
- }
393
- } catch (err) {
394
- console.warn("[support] AI rewrite error:", err);
395
- } finally {
396
- setAiRewriting(false);
397
- }
398
- };
399
- const handleTimerSave = async () => {
400
- if (!ticketId || timerSeconds < 60) return;
401
- try {
402
- await fetch("/api/time-entries", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ ticket: Number(ticketId), duration: Math.round(timerSeconds / 60), date: (/* @__PURE__ */ new Date()).toISOString(), description: "Timer" }) });
403
- setTimerSeconds(0);
404
- setTimerRunning(false);
405
- fetchAll();
406
- } catch (err) {
407
- console.warn("[support] timer save error:", err);
408
- }
409
- };
410
- const handleApplyMacro = async (macroId) => {
411
- if (!ticketId || applyingMacro) return;
412
- setApplyingMacro(true);
413
- try {
414
- const r = await fetch("/api/support/apply-macro", {
415
- method: "POST",
416
- headers: { "Content-Type": "application/json" },
417
- credentials: "include",
418
- body: JSON.stringify({ macroId, ticketId: Number(ticketId) })
419
- });
420
- if (r.ok) {
421
- fetchAll();
422
- }
423
- } catch (err) {
424
- console.warn("[support] apply macro error:", err);
425
- } finally {
426
- setApplyingMacro(false);
427
- }
428
- };
429
- const handleFileSelect = (files) => {
430
- if (!files) return;
431
- setPendingFiles((prev) => [...prev, ...Array.from(files)]);
432
- };
433
- const handleFileDrop = (e) => {
434
- e.preventDefault();
435
- setComposerDragOver(false);
436
- handleFileSelect(e.dataTransfer.files);
437
- };
438
- const handleRemoveTag = async (tag) => {
439
- const newTags = tags.filter((t2) => t2 !== tag);
440
- setTags(newTags);
441
- if (ticketId) {
442
- await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ tags: newTags.map((t2) => ({ tag: t2 })) }) }).catch(() => {
443
- });
444
- }
445
- };
446
- const handleAddTag = async () => {
447
- if (!newTagValue.trim()) {
448
- setAddingTag(false);
449
- return;
450
- }
451
- const tag = newTagValue.trim();
452
- if (tags.includes(tag)) {
453
- setAddingTag(false);
454
- setNewTagValue("");
455
- return;
456
- }
457
- const newTags = [...tags, tag];
458
- setTags(newTags);
459
- setAddingTag(false);
460
- setNewTagValue("");
461
- if (ticketId) {
462
- await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ tags: newTags.map((t2) => ({ tag: t2 })) }) }).catch(() => {
463
- });
464
- }
465
- };
466
- if (!ticketId) return /* @__PURE__ */ jsx("div", { style: { padding: 60, textAlign: "center", color: "#94a3b8", fontSize: 14 }, children: t("detail.selectTicket") });
467
- if (loading) return /* @__PURE__ */ jsx("div", { style: { padding: 60, textAlign: "center", color: "#94a3b8", fontSize: 14 }, children: t("common.loading") });
468
- if (!ticket) return /* @__PURE__ */ jsx("div", { style: { padding: 60, textAlign: "center", color: "#94a3b8", fontSize: 14 }, children: t("detail.notFound") });
469
- const st = STATUS[ticket.status || "open"] || STATUS.open;
470
- const totalMin = timeEntries.reduce((a, e) => a + (e.duration || 0), 0);
471
- const initials = client ? `${(client.firstName?.[0] || "").toUpperCase()}${(client.lastName?.[0] || "").toUpperCase()}` : "?";
472
- const S = {
473
- page: { padding: "16px 20px", maxWidth: 1200, margin: "0 auto" },
474
- topBar: { display: "flex", alignItems: "center", gap: 12, marginBottom: 12, padding: "8px 0", borderBottom: "1px solid var(--theme-elevation-200)" },
475
- backLink: { fontSize: 18, textDecoration: "none", color: "var(--theme-elevation-500)", padding: "4px 8px" },
476
- ticketNumber: { fontWeight: 700, fontSize: 14, color: "var(--theme-elevation-400)" },
477
- ticketSubject: { fontWeight: 600, fontSize: 15, color: "var(--theme-text)", flex: 1 },
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 })
510
- ] }),
511
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
512
- /* @__PURE__ */ jsxs("select", { style: { ...S.statusChip, background: st.bg, color: st.color }, value: ticket.status || "open", onChange: (e) => handleStatusChange(e.target.value), disabled: statusUpdating, children: [
513
- /* @__PURE__ */ jsx("option", { value: "open", children: t("detail.statusOpen") }),
514
- /* @__PURE__ */ jsx("option", { value: "waiting_client", children: t("detail.statusWaiting") }),
515
- /* @__PURE__ */ jsx("option", { value: "resolved", children: t("detail.statusResolved") })
516
- ] }),
517
- sentiment && features.ai && /* @__PURE__ */ jsxs("span", { style: { ...S.badge, background: `${sentiment.color}12`, color: sentiment.color }, children: [
518
- sentiment.emoji,
519
- " ",
520
- sentiment.label
521
- ] }),
522
- /* @__PURE__ */ jsxs("div", { ref: dropdownRef, style: { position: "relative" }, children: [
523
- /* @__PURE__ */ jsx("button", { onClick: () => setShowMenu(!showMenu), style: { background: "none", border: "none", cursor: "pointer", fontSize: 18, color: "var(--theme-elevation-500)" }, children: "\xB7\xB7\xB7" }),
524
- showMenu && /* @__PURE__ */ jsxs("div", { style: { position: "absolute", right: 0, top: "100%", background: "var(--theme-elevation-0)", border: "1px solid var(--theme-elevation-200)", borderRadius: 8, boxShadow: "0 4px 12px rgba(0,0,0,0.1)", zIndex: 50, minWidth: 160 }, children: [
525
- /* @__PURE__ */ jsx("button", { style: { display: "block", width: "100%", padding: "8px 14px", border: "none", background: "none", cursor: "pointer", textAlign: "left", fontSize: 13, color: "var(--theme-text)" }, onClick: () => {
526
- navigator.clipboard.writeText(window.location.href);
527
- setShowMenu(false);
528
- }, children: t("detail.copyLink") }),
529
- /* @__PURE__ */ jsx(Link, { href: `/admin/collections/tickets/${ticketId}`, style: { display: "block", padding: "8px 14px", fontSize: 13, color: "var(--theme-text)", textDecoration: "none" }, onClick: () => setShowMenu(false), children: t("detail.payloadView") }),
530
- /* @__PURE__ */ jsx("a", { href: `/support/tickets/${ticketId}`, target: "_blank", rel: "noopener noreferrer", style: { display: "block", padding: "8px 14px", fontSize: 13, color: "var(--theme-text)", textDecoration: "none" }, onClick: () => setShowMenu(false), children: t("detail.clientView") })
531
- ] })
532
- ] })
533
- ] })
534
- ] }),
535
- otherViewers.length > 0 && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "8px 14px", marginBottom: 12, borderRadius: 8, background: "#fef3c7", border: "1px solid #fde68a", fontSize: 13, fontWeight: 500, color: "#92400e" }, children: [
536
- otherViewers.map((v) => v.name).join(", "),
537
- " ",
538
- otherViewers.length === 1 ? "est aussi en train de voir ce ticket" : "sont aussi en train de voir ce ticket"
539
- ] }),
540
- /* @__PURE__ */ jsxs("div", { style: S.layout, children: [
541
- /* @__PURE__ */ jsxs("div", { children: [
542
- /* @__PURE__ */ jsxs("div", { style: S.thread, children: [
543
- messages.map((msg, idx) => {
544
- const prev = idx > 0 ? messages[idx - 1] : null;
545
- const showDate = msg.createdAt && (!prev?.createdAt || new Date(msg.createdAt).toDateString() !== new Date(prev.createdAt).toDateString());
546
- const isAdmin = msg.authorType === "admin";
547
- const isPendingDelete = undoToast?.msgId === msg.id;
548
- return /* @__PURE__ */ jsxs(React.Fragment, { children: [
549
- showDate && /* @__PURE__ */ jsx("div", { style: S.dateSeparator, children: /* @__PURE__ */ jsx("span", { children: dateLabel(msg.createdAt) }) }),
550
- /* @__PURE__ */ jsxs("div", { style: { ...S.message, ...msg.isInternal ? { borderColor: "#fbbf24", background: "#fefce8" } : {}, ...isPendingDelete ? { opacity: 0.3, pointerEvents: "none" } : {} }, children: [
551
- /* @__PURE__ */ jsx("div", { style: { ...S.avatar, backgroundColor: isAdmin ? "#2563eb" : msg.authorType === "email" ? "#ea580c" : "#7c3aed" }, children: isAdmin ? "CW" : initials }),
552
- /* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
553
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 4 }, children: [
554
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, fontSize: 13 }, children: isAdmin ? "Support" : msg.authorType === "email" ? "Email" : client?.firstName || "Client" }),
555
- /* @__PURE__ */ jsx("span", { style: { fontSize: 11, color: "var(--theme-elevation-400)" }, children: timeAgo(msg.createdAt) }),
556
- msg.isInternal && /* @__PURE__ */ jsx("span", { style: { ...S.badge, background: "#fef3c7", color: "#92400e" }, children: "Interne" }),
557
- msg.isSolution && /* @__PURE__ */ jsx("span", { style: { ...S.badge, background: "#dcfce7", color: "#166534" }, children: "Solution" })
558
- ] }),
559
- msg.bodyHtml ? /* @__PURE__ */ jsx("div", { style: S.messageBody, dangerouslySetInnerHTML: { __html: msg.bodyHtml } }) : /* @__PURE__ */ jsx("div", { style: S.messageBody, children: msg.body }),
560
- Array.isArray(msg.attachments) && msg.attachments.length > 0 && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 8 }, children: msg.attachments.map((att, i) => {
561
- const file = typeof att.file === "object" ? att.file : null;
562
- 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: "", style: { maxWidth: 200, maxHeight: 120, borderRadius: 6 } }) }, i) : /* @__PURE__ */ jsxs("a", { href: file.url || "#", target: "_blank", rel: "noopener noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: [
564
- "PJ ",
565
- file.filename || "Fichier"
566
- ] }, i);
567
- }) })
568
- ] }),
569
- /* @__PURE__ */ jsxs("div", { style: { position: "absolute", top: 8, right: 8, display: "flex", gap: 4, opacity: 0.6 }, children: [
570
- /* @__PURE__ */ jsx("button", { style: { ...S.toolbarBtn, color: "#dc2626", fontSize: 11 }, onClick: () => handleDeleteMessage(msg.id), children: t("common.delete") }),
571
- features.splitTicket && !msg.isInternal && /* @__PURE__ */ jsx("button", { style: { ...S.toolbarBtn, fontSize: 11 }, onClick: () => {
572
- setSplitModal({ messageId: msg.id, preview: msg.body.slice(0, 200) });
573
- setSplitSubject(`Split: ${ticket.subject}`);
574
- }, children: t("actions.extractMessage") }),
575
- isAdmin && !msg.isInternal && /* @__PURE__ */ jsx("button", { style: { ...S.toolbarBtn, fontSize: 11 }, onClick: () => fetch("/api/support/resend-notification", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ messageId: msg.id }) }), children: t("actions.resendEmail") })
576
- ] })
577
- ] })
578
- ] }, msg.id);
579
- }),
580
- /* @__PURE__ */ jsx("div", { ref: threadEndRef })
581
- ] }),
582
- clientTyping && /* @__PURE__ */ jsxs("div", { style: { padding: "8px 14px", fontSize: 13, color: "var(--theme-elevation-400)" }, children: [
583
- client?.firstName || "Client",
584
- " est en train d'ecrire..."
585
- ] }),
586
- /* @__PURE__ */ jsxs(
587
- "div",
588
- {
589
- style: { ...S.composer, ...isInternal ? S.composerInternal : {}, ...composerDragOver ? { borderColor: "#2563eb", background: "#eff6ff" } : {} },
590
- onDragOver: (e) => {
591
- e.preventDefault();
592
- setComposerDragOver(true);
593
- },
594
- onDragLeave: () => setComposerDragOver(false),
595
- onDrop: handleFileDrop,
596
- children: [
597
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, marginBottom: 8, flexWrap: "wrap" }, children: [
598
- features.ai && /* @__PURE__ */ jsxs(Fragment, { children: [
599
- /* @__PURE__ */ jsx("button", { style: S.toolbarBtn, onClick: handleAiSuggest, disabled: aiReplying || messages.length === 0, children: aiReplying ? "..." : t("detail.iaSuggestion") }),
600
- /* @__PURE__ */ jsx("button", { style: S.toolbarBtn, onClick: handleAiRewrite, disabled: aiRewriting || !replyBody.trim(), children: aiRewriting ? "..." : t("detail.rewrite") })
601
- ] }),
602
- /* @__PURE__ */ jsx("button", { style: S.toolbarBtn, onClick: () => fileInputRef.current?.click(), children: t("detail.file") }),
603
- /* @__PURE__ */ jsx("input", { ref: fileInputRef, type: "file", multiple: true, style: { display: "none" }, onChange: (e) => handleFileSelect(e.target.files) }),
604
- macros.length > 0 && /* @__PURE__ */ jsxs(
605
- "select",
606
- {
607
- style: { ...S.sideSelect, marginLeft: 0 },
608
- onChange: (e) => {
609
- const id = Number(e.target.value);
610
- if (id) handleApplyMacro(id);
611
- e.target.value = "";
612
- },
613
- disabled: applyingMacro,
614
- children: [
615
- /* @__PURE__ */ jsx("option", { value: "", children: applyingMacro ? t("detail.applyingMacro") : t("detail.macros") }),
616
- macros.map((m) => /* @__PURE__ */ jsx("option", { value: m.id, children: m.name }, m.id))
617
- ]
618
- }
619
- ),
620
- features.canned && cannedResponses.length > 0 && /* @__PURE__ */ jsxs("select", { style: S.sideSelect, onChange: (e) => {
621
- const cr = cannedResponses.find((c) => String(c.id) === e.target.value);
622
- if (cr) {
623
- let b = cr.body;
624
- if (client) {
625
- b = b.replace(/\{\{client\.firstName\}\}/g, client.firstName).replace(/\{\{client\.company\}\}/g, client.company);
626
- }
627
- setReplyBody(b);
628
- }
629
- e.target.value = "";
630
- }, children: [
631
- /* @__PURE__ */ jsx("option", { value: "", children: t("detail.cannedResponses") }),
632
- cannedResponses.map((cr) => /* @__PURE__ */ jsx("option", { value: String(cr.id), children: cr.title }, cr.id))
633
- ] })
634
- ] }),
635
- /* @__PURE__ */ jsx(
636
- "textarea",
637
- {
638
- style: S.textarea,
639
- value: replyBody,
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: [
645
- /* @__PURE__ */ jsxs("span", { children: [
646
- "PJ ",
647
- f.name
648
- ] }),
649
- /* @__PURE__ */ jsx("button", { style: { background: "none", border: "none", cursor: "pointer", fontSize: 14, color: "var(--theme-elevation-400)" }, onClick: () => setPendingFiles((prev) => prev.filter((_, j) => j !== i)), children: "\xD7" })
650
- ] }, i)) }),
651
- /* @__PURE__ */ jsxs("div", { style: S.composerFooter, children: [
652
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 12, alignItems: "center", fontSize: 12 }, children: [
653
- /* @__PURE__ */ jsxs("label", { children: [
654
- /* @__PURE__ */ jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => setIsInternal(e.target.checked) }),
655
- " ",
656
- t("detail.internalNote")
657
- ] }),
658
- /* @__PURE__ */ jsxs("label", { children: [
659
- /* @__PURE__ */ jsx("input", { type: "checkbox", checked: notifyClient, onChange: (e) => setNotifyClient(e.target.checked), disabled: isInternal }),
660
- " ",
661
- t("detail.notify")
662
- ] })
663
- ] }),
664
- /* @__PURE__ */ jsx("button", { style: { ...S.sendBtn, ...isInternal ? { background: "#d97706" } : {} }, onClick: handleSend, disabled: sending || !replyBody.trim(), "data-action": "send-reply", children: sending ? t("detail.sending") : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
665
- ] })
666
- ]
667
- }
668
- )
669
- ] }),
670
- /* @__PURE__ */ jsxs("div", { style: S.sidebar, children: [
671
- client && /* @__PURE__ */ jsxs("div", { style: S.sideSection, children: [
672
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, alignItems: "center", marginBottom: 8 }, children: [
673
- /* @__PURE__ */ jsx("div", { style: { ...S.avatar, backgroundColor: "#7c3aed", width: 36, height: 36 }, children: initials }),
674
- /* @__PURE__ */ jsxs("div", { children: [
675
- /* @__PURE__ */ jsxs("div", { style: { fontWeight: 600, fontSize: 13 }, children: [
676
- client.firstName,
677
- " ",
678
- client.lastName
679
- ] }),
680
- /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "var(--theme-elevation-500)" }, children: client.company }),
681
- /* @__PURE__ */ jsx("a", { href: `mailto:${client.email}`, style: { fontSize: 11, color: "#2563eb" }, children: client.email })
682
- ] })
683
- ] }),
684
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6 }, children: [
685
- /* @__PURE__ */ jsx(Link, { href: `/admin/collections/support-clients/${client.id}`, style: { padding: "4px 10px", borderRadius: 4, border: "1px solid var(--theme-elevation-200)", fontSize: 11, textDecoration: "none", color: "var(--theme-text)" }, children: t("client.clientSheet") }),
686
- /* @__PURE__ */ jsx("button", { style: { padding: "4px 10px", borderRadius: 4, border: "1px solid var(--theme-elevation-200)", fontSize: 11, background: "none", cursor: "pointer", color: "var(--theme-text)" }, onClick: () => window.open(`/api/admin/impersonate?clientId=${client.id}`, "_blank"), children: t("client.clientPortal") })
687
- ] })
688
- ] }),
689
- /* @__PURE__ */ jsxs("div", { style: S.sideSection, children: [
690
- /* @__PURE__ */ jsx("div", { style: S.sideSectionTitle, children: t("detail.details") }),
691
- /* @__PURE__ */ jsxs("div", { style: S.sideField, children: [
692
- /* @__PURE__ */ jsx("span", { style: S.sideLabel, children: t("detail.priority") }),
693
- /* @__PURE__ */ jsxs("select", { style: S.sideSelect, value: ticket.priority || "normal", onChange: (e) => handleFieldPatch("priority", e.target.value), children: [
694
- /* @__PURE__ */ jsx("option", { value: "low", children: t("ticket.priority.low") }),
695
- /* @__PURE__ */ jsx("option", { value: "normal", children: t("ticket.priority.normal") }),
696
- /* @__PURE__ */ jsx("option", { value: "high", children: t("ticket.priority.high") }),
697
- /* @__PURE__ */ jsx("option", { value: "urgent", children: t("ticket.priority.urgent") })
698
- ] })
699
- ] }),
700
- /* @__PURE__ */ jsxs("div", { style: S.sideField, children: [
701
- /* @__PURE__ */ jsx("span", { style: S.sideLabel, children: t("detail.category") }),
702
- /* @__PURE__ */ jsxs("select", { style: S.sideSelect, value: ticket.category || "", onChange: (e) => handleFieldPatch("category", e.target.value), children: [
703
- /* @__PURE__ */ jsx("option", { value: "", children: "--" }),
704
- /* @__PURE__ */ jsx("option", { value: "bug", children: t("ticket.category.bug") }),
705
- /* @__PURE__ */ jsx("option", { value: "content", children: t("ticket.category.content") }),
706
- /* @__PURE__ */ jsx("option", { value: "feature", children: t("ticket.category.feature") }),
707
- /* @__PURE__ */ jsx("option", { value: "question", children: t("ticket.category.question") }),
708
- /* @__PURE__ */ jsx("option", { value: "hosting", children: t("ticket.category.hosting") })
709
- ] })
710
- ] }),
711
- /* @__PURE__ */ jsxs("div", { style: S.sideField, children: [
712
- /* @__PURE__ */ jsx("span", { style: S.sideLabel, children: t("detail.source") }),
713
- /* @__PURE__ */ jsx("span", { style: S.sideValue, children: ticket.source || t("ticket.source.portal") })
714
- ] }),
715
- /* @__PURE__ */ jsxs("div", { style: S.sideField, children: [
716
- /* @__PURE__ */ jsx("span", { style: S.sideLabel, children: t("detail.assigned") }),
717
- /* @__PURE__ */ jsx("span", { style: S.sideValue, children: typeof ticket.assignedTo === "object" && ticket.assignedTo ? ticket.assignedTo.firstName || "Admin" : "--" })
718
- ] })
719
- ] }),
720
- /* @__PURE__ */ jsxs("div", { style: S.sideSection, children: [
721
- /* @__PURE__ */ jsx("div", { style: S.sideSectionTitle, children: t("detail.tags") }),
722
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
723
- tags.map((tag) => /* @__PURE__ */ jsxs("span", { style: S.tagChip, children: [
724
- tag,
725
- /* @__PURE__ */ jsx("button", { style: S.tagRemove, onClick: () => handleRemoveTag(tag), children: "\xD7" })
726
- ] }, tag)),
727
- addingTag ? /* @__PURE__ */ jsx(
728
- "input",
729
- {
730
- style: S.tagInput,
731
- value: newTagValue,
732
- onChange: (e) => setNewTagValue(e.target.value),
733
- onKeyDown: (e) => {
734
- if (e.key === "Enter") handleAddTag();
735
- if (e.key === "Escape") {
736
- setAddingTag(false);
737
- setNewTagValue("");
738
- }
739
- },
740
- onBlur: handleAddTag,
741
- placeholder: "Tag...",
742
- autoFocus: true
743
- }
744
- ) : /* @__PURE__ */ jsx("button", { style: S.tagAddBtn, onClick: () => setAddingTag(true), children: "+ Tag" })
745
- ] })
746
- ] }),
747
- features.timeTracking && /* @__PURE__ */ jsxs("div", { style: S.sideSection, children: [
748
- /* @__PURE__ */ jsxs("div", { style: S.sideSectionTitle, children: [
749
- t("detail.time"),
750
- " ",
751
- /* @__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
- ] }),
753
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
754
- /* @__PURE__ */ jsxs("span", { style: { fontFamily: "monospace", fontSize: 16, fontWeight: 700, color: timerRunning ? "#dc2626" : "var(--theme-text)" }, children: [
755
- String(Math.floor(timerSeconds / 60)).padStart(2, "0"),
756
- ":",
757
- String(timerSeconds % 60).padStart(2, "0")
758
- ] }),
759
- !timerRunning ? /* @__PURE__ */ jsx("button", { style: { ...S.toolbarBtn, color: "#dc2626", borderColor: "#dc2626" }, onClick: () => setTimerRunning(true), children: timerSeconds > 0 ? "Play" : "Go" }) : /* @__PURE__ */ jsx("button", { style: S.toolbarBtn, onClick: () => setTimerRunning(false), children: "Pause" }),
760
- timerSeconds >= 60 && !timerRunning && /* @__PURE__ */ jsxs("button", { style: { ...S.toolbarBtn, color: "#16a34a", borderColor: "#16a34a" }, onClick: () => {
761
- handleTimerSave();
762
- localStorage.removeItem(`timer-sec-${ticketId}`);
763
- localStorage.removeItem(`timer-run-${ticketId}`);
764
- }, children: [
765
- "Save ",
766
- Math.round(timerSeconds / 60),
767
- "m"
768
- ] })
769
- ] }),
770
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" }, children: [
771
- /* @__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", { style: { ...S.toolbarBtn, fontSize: 11 }, onClick: async () => {
773
- const input = document.getElementById("manual-time-input");
774
- const mins = Number(input?.value);
775
- if (!mins || mins < 1 || !ticketId) return;
776
- try {
777
- 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
- if (input) input.value = "";
779
- fetchAll();
780
- } catch (err) {
781
- console.warn("[support] manual time entry error:", err);
782
- }
783
- }, children: "+ Ajouter" })
784
- ] }),
785
- 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
- /* @__PURE__ */ jsx("span", { children: new Date(e.date).toLocaleDateString("fr-FR", { day: "numeric", month: "short" }) }),
787
- /* @__PURE__ */ jsxs("span", { title: e.description, style: { fontWeight: 600, cursor: e.description ? "help" : "default" }, children: [
788
- e.duration,
789
- "min"
790
- ] })
791
- ] }, e.id)) })
792
- ] }),
793
- features.activityLog && /* @__PURE__ */ jsxs("div", { style: S.sideSection, children: [
794
- /* @__PURE__ */ jsx("div", { style: S.sideSectionTitle, children: /* @__PURE__ */ jsxs("button", { onClick: () => setShowActivity(!showActivity), style: { background: "none", border: "none", cursor: "pointer", fontSize: 12, fontWeight: 700, textTransform: "uppercase", color: "var(--theme-elevation-500)" }, children: [
795
- "Activite ",
796
- showActivity ? "&#9662;" : "&#9656;"
797
- ] }) }),
798
- showActivity && activityLog.slice(0, 8).map((a) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "flex-start", padding: "4px 0" }, children: [
799
- /* @__PURE__ */ jsx("div", { style: { width: 6, height: 6, borderRadius: "50%", marginTop: 4, flexShrink: 0, backgroundColor: a.actorType === "admin" ? "#2563eb" : a.actorType === "system" ? "#6b7280" : "#16a34a" } }),
800
- /* @__PURE__ */ jsxs("div", { children: [
801
- /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "var(--theme-text)" }, children: (a.detail || a.action).slice(0, 60) }),
802
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "var(--theme-elevation-400)" }, children: timeAgo(a.createdAt) })
803
- ] })
804
- ] }, a.id))
805
- ] })
806
- ] })
807
- ] }),
808
- undoToast && /* @__PURE__ */ jsxs("div", { style: { position: "fixed", bottom: 24, left: "50%", transform: "translateX(-50%)", padding: "10px 20px", borderRadius: 8, background: "#1e293b", color: "#fff", fontSize: 13, display: "flex", gap: 12, alignItems: "center", zIndex: 100, boxShadow: "0 4px 12px rgba(0,0,0,0.2)" }, role: "alert", children: [
809
- /* @__PURE__ */ jsx("span", { children: t("detail.messageDeleted") }),
810
- /* @__PURE__ */ jsx("button", { onClick: handleUndoDelete, style: { background: "none", border: "1px solid rgba(255,255,255,0.3)", borderRadius: 4, padding: "4px 10px", color: "#fff", cursor: "pointer", fontSize: 12, fontWeight: 600 }, children: t("detail.undo") })
811
- ] }),
812
- splitModal && /* @__PURE__ */ jsx("div", { style: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 100 }, onClick: (e) => {
813
- if (e.target === e.currentTarget) {
814
- setSplitModal(null);
815
- setSplitSubject("");
816
- }
817
- }, children: /* @__PURE__ */ jsxs("div", { style: { background: "var(--theme-elevation-0)", borderRadius: 12, padding: 24, maxWidth: 480, width: "100%", boxShadow: "0 8px 30px rgba(0,0,0,0.15)" }, role: "dialog", children: [
818
- /* @__PURE__ */ jsx("h3", { style: { margin: "0 0 12px", fontSize: 16, fontWeight: 700 }, children: t("detail.extractTitle") }),
819
- /* @__PURE__ */ jsx("div", { style: { padding: 10, background: "var(--theme-elevation-50)", borderRadius: 6, fontSize: 13, color: "var(--theme-elevation-500)", marginBottom: 12 }, children: splitModal.preview }),
820
- /* @__PURE__ */ jsx(
821
- "input",
822
- {
823
- style: { width: "100%", padding: "8px 12px", borderRadius: 6, border: "1px solid var(--theme-elevation-200)", fontSize: 13, marginBottom: 12, color: "var(--theme-text)", background: "var(--theme-elevation-0)" },
824
- value: splitSubject,
825
- onChange: (e) => setSplitSubject(e.target.value),
826
- onKeyDown: (e) => {
827
- if (e.key === "Enter") handleSplitConfirm();
828
- },
829
- placeholder: t("detail.extractSubjectPlaceholder"),
830
- autoFocus: true
831
- }
832
- ),
833
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" }, children: [
834
- /* @__PURE__ */ jsx("button", { style: { padding: "6px 14px", borderRadius: 6, border: "1px solid var(--theme-elevation-200)", background: "none", cursor: "pointer", fontSize: 13, color: "var(--theme-text)" }, onClick: () => {
835
- setSplitModal(null);
836
- setSplitSubject("");
837
- }, children: t("common.cancel") }),
838
- /* @__PURE__ */ jsx("button", { style: { ...S.sendBtn }, onClick: handleSplitConfirm, disabled: !splitSubject.trim(), children: t("ticket.createTicket") })
839
- ] })
840
- ] }) })
841
- ] });
842
- };
843
-
844
- export { TicketDetailClient };