@consilioweb/payload-support 0.6.0 → 0.6.2

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