@consilioweb/payload-support 0.6.1 → 0.6.3

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 (134) hide show
  1. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +77 -0
  2. package/dist/components/TicketConversation/components/AISummaryPanel.js +76 -0
  3. package/dist/components/TicketConversation/components/ActionPanels.cjs +121 -0
  4. package/dist/components/TicketConversation/components/ActionPanels.js +118 -0
  5. package/dist/components/TicketConversation/components/ActivityLog.cjs +24 -0
  6. package/dist/components/TicketConversation/components/ActivityLog.js +23 -0
  7. package/dist/components/TicketConversation/components/ClientBar.cjs +43 -0
  8. package/dist/components/TicketConversation/components/ClientBar.js +42 -0
  9. package/dist/components/TicketConversation/components/ClientHistory.cjs +138 -0
  10. package/dist/components/TicketConversation/components/ClientHistory.js +137 -0
  11. package/dist/components/TicketConversation/components/CodeBlock.cjs +154 -0
  12. package/dist/components/TicketConversation/components/CodeBlock.js +152 -0
  13. package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +156 -0
  14. package/dist/components/TicketConversation/components/CodeBlockInserter.js +155 -0
  15. package/dist/components/TicketConversation/components/QuickActions.cjs +66 -0
  16. package/dist/components/TicketConversation/components/QuickActions.js +65 -0
  17. package/dist/components/TicketConversation/components/TicketHeader.cjs +93 -0
  18. package/dist/components/TicketConversation/components/TicketHeader.js +92 -0
  19. package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +134 -0
  20. package/dist/components/TicketConversation/components/TimeTrackingPanel.js +133 -0
  21. package/dist/components/TicketConversation/config.cjs +44 -0
  22. package/dist/components/TicketConversation/config.js +41 -0
  23. package/dist/components/TicketConversation/constants.cjs +100 -0
  24. package/dist/components/TicketConversation/constants.js +96 -0
  25. package/dist/components/TicketConversation/context.cjs +13 -0
  26. package/dist/components/TicketConversation/context.js +11 -0
  27. package/dist/components/TicketConversation/hooks/useAI.cjs +177 -0
  28. package/dist/components/TicketConversation/hooks/useAI.js +176 -0
  29. package/dist/components/TicketConversation/hooks/useMessageActions.cjs +136 -0
  30. package/dist/components/TicketConversation/hooks/useMessageActions.js +135 -0
  31. package/dist/components/TicketConversation/hooks/useReply.cjs +188 -0
  32. package/dist/components/TicketConversation/hooks/useReply.js +187 -0
  33. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +231 -0
  34. package/dist/components/TicketConversation/hooks/useTicketActions.js +230 -0
  35. package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +123 -0
  36. package/dist/components/TicketConversation/hooks/useTimeTracking.js +122 -0
  37. package/dist/components/TicketConversation/hooks/useTranslation.cjs +78 -0
  38. package/dist/components/TicketConversation/hooks/useTranslation.js +70 -0
  39. package/dist/components/TicketConversation/index.cjs +1128 -0
  40. package/dist/components/TicketConversation/index.js +1123 -0
  41. package/dist/components/TicketConversation/locales/en.json +878 -0
  42. package/dist/components/TicketConversation/locales/fr.json +878 -0
  43. package/dist/components/TicketConversation/types.cjs +2 -0
  44. package/dist/components/TicketConversation/types.js +2 -0
  45. package/dist/components/TicketConversation/utils.cjs +27 -0
  46. package/dist/components/TicketConversation/utils.js +25 -0
  47. package/dist/styles/BillingView.module.scss +311 -0
  48. package/dist/styles/ChatView.module.scss +438 -0
  49. package/dist/styles/CommandPalette.module.scss +160 -0
  50. package/dist/styles/CrmView.module.scss +554 -0
  51. package/dist/styles/EmailTracking.module.scss +238 -0
  52. package/dist/styles/ImportConversation.module.scss +267 -0
  53. package/dist/styles/Layout.module.scss +55 -0
  54. package/dist/styles/Logs.module.scss +164 -0
  55. package/dist/styles/NewTicket.module.scss +143 -0
  56. package/dist/styles/PendingEmails.module.scss +629 -0
  57. package/dist/styles/SupportDashboard.module.scss +649 -0
  58. package/dist/styles/TicketDetail.module.scss +1050 -0
  59. package/dist/styles/TicketInbox.module.scss +296 -0
  60. package/dist/styles/TicketingSettings.module.scss +358 -0
  61. package/dist/styles/TimeDashboard.module.scss +287 -0
  62. package/dist/styles/_tokens.scss +78 -0
  63. package/dist/{components/TicketConversation.css → styles/theme.css} +326 -13
  64. package/dist/views/BillingView/client.cjs +209 -0
  65. package/dist/views/BillingView/client.js +204 -0
  66. package/dist/views/BillingView/index.cjs +34 -0
  67. package/dist/views/BillingView/index.js +29 -0
  68. package/dist/views/ChatView/client.cjs +253 -0
  69. package/dist/views/ChatView/client.js +252 -0
  70. package/dist/views/ChatView/index.cjs +34 -0
  71. package/dist/views/ChatView/index.js +29 -0
  72. package/dist/views/CrmView/client.cjs +233 -0
  73. package/dist/views/CrmView/client.js +232 -0
  74. package/dist/views/CrmView/index.cjs +34 -0
  75. package/dist/views/CrmView/index.js +29 -0
  76. package/dist/views/EmailTrackingView/client.cjs +159 -0
  77. package/dist/views/EmailTrackingView/client.js +154 -0
  78. package/dist/views/EmailTrackingView/index.cjs +34 -0
  79. package/dist/views/EmailTrackingView/index.js +29 -0
  80. package/dist/views/ImportConversationView/client.cjs +205 -0
  81. package/dist/views/ImportConversationView/client.js +204 -0
  82. package/dist/views/ImportConversationView/index.cjs +34 -0
  83. package/dist/views/ImportConversationView/index.js +29 -0
  84. package/dist/views/LogsView/client.cjs +149 -0
  85. package/dist/views/LogsView/client.js +148 -0
  86. package/dist/views/LogsView/index.cjs +32 -0
  87. package/dist/views/LogsView/index.js +27 -0
  88. package/dist/views/NewTicketView/client.cjs +229 -0
  89. package/dist/views/NewTicketView/client.js +224 -0
  90. package/dist/views/NewTicketView/index.cjs +32 -0
  91. package/dist/views/NewTicketView/index.js +27 -0
  92. package/dist/views/PendingEmailsView/client.cjs +173 -0
  93. package/dist/views/PendingEmailsView/client.js +172 -0
  94. package/dist/views/PendingEmailsView/index.cjs +34 -0
  95. package/dist/views/PendingEmailsView/index.js +29 -0
  96. package/dist/views/SupportDashboardView/client.cjs +301 -0
  97. package/dist/views/SupportDashboardView/client.js +296 -0
  98. package/dist/views/SupportDashboardView/index.cjs +34 -0
  99. package/dist/views/SupportDashboardView/index.js +29 -0
  100. package/dist/views/TicketDetailView/client.cjs +850 -0
  101. package/dist/views/TicketDetailView/client.js +844 -0
  102. package/dist/views/TicketDetailView/index.cjs +34 -0
  103. package/dist/views/TicketDetailView/index.js +29 -0
  104. package/dist/views/TicketInboxView/client.cjs +299 -0
  105. package/dist/views/TicketInboxView/client.js +294 -0
  106. package/dist/views/TicketInboxView/index.cjs +32 -0
  107. package/dist/views/TicketInboxView/index.js +27 -0
  108. package/dist/{views.css → views/TicketingSettingsView/TicketingSettings.module.scss} +138 -66
  109. package/dist/views/TicketingSettingsView/client.cjs +733 -0
  110. package/dist/views/TicketingSettingsView/client.js +728 -0
  111. package/dist/views/TicketingSettingsView/index.cjs +34 -0
  112. package/dist/views/TicketingSettingsView/index.js +29 -0
  113. package/dist/views/TimeDashboardView/client.cjs +169 -0
  114. package/dist/views/TimeDashboardView/client.js +164 -0
  115. package/dist/views/TimeDashboardView/index.cjs +34 -0
  116. package/dist/views/TimeDashboardView/index.js +29 -0
  117. package/dist/views/shared/AdminViewHeader.cjs +68 -0
  118. package/dist/views/shared/AdminViewHeader.js +67 -0
  119. package/dist/views/shared/ErrorBoundary.cjs +55 -0
  120. package/dist/views/shared/ErrorBoundary.js +50 -0
  121. package/dist/views/shared/Skeleton.cjs +77 -0
  122. package/dist/views/shared/Skeleton.js +72 -0
  123. package/dist/views/shared/adminTokens.cjs +34 -0
  124. package/dist/views/shared/adminTokens.js +31 -0
  125. package/dist/views/shared/config.cjs +44 -0
  126. package/dist/views/shared/config.js +40 -0
  127. package/dist/views/shared/index.cjs +58 -0
  128. package/dist/views/shared/index.js +5 -0
  129. package/dist/views.cjs +77 -6175
  130. package/dist/views.d.cts +13 -30
  131. package/dist/views.d.ts +13 -30
  132. package/dist/views.js +13 -6165
  133. package/package.json +2 -2
  134. package/dist/components/TicketConversation.js +0 -3004
@@ -0,0 +1,844 @@
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 };