@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,3004 @@
1
+ "use client";
2
+ import React6, { useState, useRef, useCallback, useEffect } from 'react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/components/TicketConversation/constants.ts
6
+ var statusLabels = {
7
+ open: { label: "Ouvert", bg: "#dbeafe", color: "#1e40af" },
8
+ waiting_client: { label: "Attente", bg: "#fef3c7", color: "#92400e" },
9
+ resolved: { label: "R\xE9solu", bg: "#dcfce7", color: "#166534" }
10
+ };
11
+ var projectStatusLabels = {
12
+ active: { label: "Actif", bg: "#dcfce7", color: "#166534" },
13
+ paused: { label: "En pause", bg: "#fef3c7", color: "#92400e" },
14
+ completed: { label: "Termin\xE9", bg: "#e5e7eb", color: "#374151" }
15
+ };
16
+ var C = {
17
+ blue: "#2563eb",
18
+ amber: "#d97706",
19
+ orange: "#ea580c",
20
+ white: "#fff",
21
+ textPrimary: "var(--theme-text)",
22
+ textSecondary: "#6b7280",
23
+ textMuted: "#9ca3af",
24
+ border: "#e2e8f0",
25
+ statusOpen: "#2563eb",
26
+ statusWaiting: "#d97706",
27
+ statusResolved: "#16a34a",
28
+ adminBg: "#f8fafc",
29
+ clientBg: "#fafafa",
30
+ clientBorder: "#e2e8f0",
31
+ emailBg: "#fffbeb",
32
+ internalBg: "#fefce8",
33
+ internalBorder: "#d97706"
34
+ };
35
+ var s = {
36
+ section: { borderTop: `1px solid ${C.border}`, paddingTop: "18px", marginTop: "20px" },
37
+ sectionTitle: { fontSize: "14px", fontWeight: 600, marginBottom: "12px", display: "flex", alignItems: "center", gap: "8px" },
38
+ btn: (bg, disabled) => {
39
+ const hex = bg.replace("#", "");
40
+ const r = parseInt(hex.substring(0, 2), 16) || 0;
41
+ const g = parseInt(hex.substring(2, 4), 16) || 0;
42
+ const b = parseInt(hex.substring(4, 6), 16) || 0;
43
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
44
+ const textColor = luminance > 0.6 ? "#374151" : C.white;
45
+ return {
46
+ padding: "7px 14px",
47
+ borderRadius: "6px",
48
+ border: `1px solid ${C.border}`,
49
+ backgroundColor: bg,
50
+ color: textColor,
51
+ fontWeight: 600,
52
+ fontSize: "13px",
53
+ cursor: disabled ? "not-allowed" : "pointer",
54
+ opacity: disabled ? 0.5 : 1,
55
+ whiteSpace: "nowrap"
56
+ };
57
+ },
58
+ ghostBtn: (color, disabled) => ({
59
+ padding: "7px 14px",
60
+ borderRadius: "6px",
61
+ border: `1px solid ${C.border}`,
62
+ backgroundColor: "transparent",
63
+ color,
64
+ fontWeight: 600,
65
+ fontSize: "13px",
66
+ cursor: disabled ? "not-allowed" : "pointer",
67
+ opacity: disabled ? 0.5 : 1,
68
+ whiteSpace: "nowrap"
69
+ }),
70
+ outlineBtn: (color, disabled) => ({
71
+ padding: "7px 14px",
72
+ borderRadius: "6px",
73
+ border: `1px solid ${color}`,
74
+ backgroundColor: "transparent",
75
+ color,
76
+ fontWeight: 600,
77
+ fontSize: "13px",
78
+ cursor: disabled ? "not-allowed" : "pointer",
79
+ opacity: disabled ? 0.5 : 1,
80
+ whiteSpace: "nowrap"
81
+ }),
82
+ input: { padding: "8px 12px", borderRadius: "6px", border: `1px solid ${C.border}`, fontSize: "14px", color: "#374151", backgroundColor: C.white },
83
+ badge: (bg, color) => ({
84
+ display: "inline-block",
85
+ padding: "2px 8px",
86
+ borderRadius: "4px",
87
+ fontSize: "11px",
88
+ fontWeight: 600,
89
+ backgroundColor: bg,
90
+ color
91
+ })
92
+ };
93
+
94
+ // src/components/TicketConversation/utils.ts
95
+ function getDateLabel(dateStr) {
96
+ const date = new Date(dateStr);
97
+ const today = /* @__PURE__ */ new Date();
98
+ const yesterday = new Date(today);
99
+ yesterday.setDate(yesterday.getDate() - 1);
100
+ if (date.toDateString() === today.toDateString()) return "Aujourd'hui";
101
+ if (date.toDateString() === yesterday.toDateString()) return "Hier";
102
+ return date.toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" });
103
+ }
104
+ function formatMessageDate(dateStr) {
105
+ const date = new Date(dateStr);
106
+ const today = /* @__PURE__ */ new Date();
107
+ const yesterday = new Date(today);
108
+ yesterday.setDate(yesterday.getDate() - 1);
109
+ const time = date.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" });
110
+ if (date.toDateString() === today.toDateString()) return time;
111
+ if (date.toDateString() === yesterday.toDateString()) return `Hier, ${time}`;
112
+ if (date.getFullYear() === today.getFullYear()) {
113
+ return `${date.toLocaleDateString("fr-FR", { day: "numeric", month: "short" })}, ${time}`;
114
+ }
115
+ return `${date.toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" })}, ${time}`;
116
+ }
117
+ var LANG_CONFIG = {
118
+ javascript: { label: "JavaScript", accent: "#f7df1e" },
119
+ js: { label: "JavaScript", accent: "#f7df1e" },
120
+ typescript: { label: "TypeScript", accent: "#3178c6" },
121
+ ts: { label: "TypeScript", accent: "#3178c6" },
122
+ tsx: { label: "TSX", accent: "#3178c6" },
123
+ jsx: { label: "JSX", accent: "#f7df1e" },
124
+ php: { label: "PHP", accent: "#777bb4" },
125
+ python: { label: "Python", accent: "#3776ab" },
126
+ py: { label: "Python", accent: "#3776ab" },
127
+ html: { label: "HTML", accent: "#e34f26" },
128
+ css: { label: "CSS", accent: "#1572b6" },
129
+ scss: { label: "SCSS", accent: "#cc6699" },
130
+ json: { label: "JSON", accent: "#292929" },
131
+ sql: { label: "SQL", accent: "#e48e00" },
132
+ bash: { label: "Bash", accent: "#4eaa25" },
133
+ sh: { label: "Shell", accent: "#4eaa25" },
134
+ yaml: { label: "YAML", accent: "#cb171e" },
135
+ yml: { label: "YAML", accent: "#cb171e" },
136
+ xml: { label: "XML", accent: "#f16529" },
137
+ markdown: { label: "Markdown", accent: "#083fa1" },
138
+ md: { label: "Markdown", accent: "#083fa1" },
139
+ diff: { label: "Diff", accent: "#41b883" },
140
+ go: { label: "Go", accent: "#00add8" },
141
+ rust: { label: "Rust", accent: "#dea584" },
142
+ java: { label: "Java", accent: "#ed8b00" },
143
+ c: { label: "C", accent: "#555555" },
144
+ cpp: { label: "C++", accent: "#00599c" },
145
+ ruby: { label: "Ruby", accent: "#cc342d" },
146
+ swift: { label: "Swift", accent: "#f05138" },
147
+ nginx: { label: "Nginx", accent: "#009639" },
148
+ docker: { label: "Docker", accent: "#2496ed" },
149
+ dockerfile: { label: "Dockerfile", accent: "#2496ed" },
150
+ env: { label: ".env", accent: "#ecd53f" }
151
+ };
152
+ var DEFAULT_CONFIG = { label: "Code", accent: "#6b7280" };
153
+ function SingleCodeBlock({ lang, code }) {
154
+ const [copied, setCopied] = useState(false);
155
+ const config = LANG_CONFIG[lang.toLowerCase()] || (lang ? { label: lang.toUpperCase(), accent: "#6b7280" } : DEFAULT_CONFIG);
156
+ const handleCopy = () => {
157
+ navigator.clipboard.writeText(code);
158
+ setCopied(true);
159
+ setTimeout(() => setCopied(false), 1500);
160
+ };
161
+ const lines = code.split("\n");
162
+ return /* @__PURE__ */ jsxs("div", { style: {
163
+ marginTop: 8,
164
+ marginBottom: 8,
165
+ borderRadius: 8,
166
+ overflow: "hidden",
167
+ border: "1px solid #334155",
168
+ backgroundColor: "#0f172a",
169
+ fontSize: 13
170
+ }, children: [
171
+ /* @__PURE__ */ jsxs("div", { style: {
172
+ display: "flex",
173
+ alignItems: "center",
174
+ justifyContent: "space-between",
175
+ padding: "6px 12px",
176
+ backgroundColor: "#1e293b",
177
+ borderBottom: "1px solid #334155"
178
+ }, children: [
179
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
180
+ /* @__PURE__ */ jsx("span", { style: {
181
+ display: "inline-block",
182
+ width: 8,
183
+ height: 8,
184
+ borderRadius: "50%",
185
+ backgroundColor: config.accent,
186
+ flexShrink: 0
187
+ } }),
188
+ /* @__PURE__ */ jsx("span", { style: {
189
+ fontSize: 11,
190
+ fontWeight: 700,
191
+ color: "#e2e8f0",
192
+ letterSpacing: "0.02em"
193
+ }, children: config.label })
194
+ ] }),
195
+ /* @__PURE__ */ jsx(
196
+ "button",
197
+ {
198
+ onClick: handleCopy,
199
+ style: {
200
+ background: "none",
201
+ border: "1px solid #475569",
202
+ borderRadius: 4,
203
+ padding: "2px 10px",
204
+ fontSize: 11,
205
+ color: copied ? "#4ade80" : "#94a3b8",
206
+ cursor: "pointer",
207
+ fontWeight: 600,
208
+ transition: "color 150ms"
209
+ },
210
+ children: copied ? "Copi\xE9 !" : "Copier"
211
+ }
212
+ )
213
+ ] }),
214
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", overflowX: "auto" }, children: [
215
+ /* @__PURE__ */ jsx("div", { style: {
216
+ padding: "12px 0",
217
+ borderRight: "1px solid #334155",
218
+ userSelect: "none",
219
+ flexShrink: 0
220
+ }, children: lines.map((_, i) => /* @__PURE__ */ jsx("div", { style: {
221
+ padding: "0 12px",
222
+ fontSize: 12,
223
+ lineHeight: "20px",
224
+ color: "#475569",
225
+ textAlign: "right",
226
+ fontFamily: '"SF Mono", "Fira Code", "JetBrains Mono", monospace'
227
+ }, children: i + 1 }, i)) }),
228
+ /* @__PURE__ */ jsx("pre", { style: {
229
+ margin: 0,
230
+ padding: 12,
231
+ overflow: "auto",
232
+ flex: 1
233
+ }, children: /* @__PURE__ */ jsx("code", { style: {
234
+ fontSize: 12,
235
+ lineHeight: "20px",
236
+ fontFamily: '"SF Mono", "Fira Code", "JetBrains Mono", monospace',
237
+ color: "#e2e8f0",
238
+ whiteSpace: "pre"
239
+ }, children: code }) })
240
+ ] })
241
+ ] });
242
+ }
243
+ function CodeBlockRenderer({ text }) {
244
+ const parts = text.split(/(```[\s\S]*?```)/g);
245
+ const hasCodeBlock = parts.some((p) => p.startsWith("```"));
246
+ if (!hasCodeBlock) return null;
247
+ return /* @__PURE__ */ jsx(Fragment, { children: parts.map((part, i) => {
248
+ if (part.startsWith("```")) {
249
+ const match = part.match(/^```(\w*)\n?([\s\S]*?)```$/);
250
+ if (match) {
251
+ const lang = match[1] || "";
252
+ const code = match[2].replace(/\n$/, "");
253
+ return /* @__PURE__ */ jsx(SingleCodeBlock, { lang, code }, i);
254
+ }
255
+ }
256
+ return null;
257
+ }) });
258
+ }
259
+ function CodeBlockRendererHtml({ html }) {
260
+ const stripped = html.replace(/<[^>]+>/g, "");
261
+ return /* @__PURE__ */ jsx(CodeBlockRenderer, { text: stripped });
262
+ }
263
+ var LANGUAGES = [
264
+ { value: "javascript", label: "JavaScript" },
265
+ { value: "typescript", label: "TypeScript" },
266
+ { value: "php", label: "PHP" },
267
+ { value: "python", label: "Python" },
268
+ { value: "html", label: "HTML" },
269
+ { value: "css", label: "CSS" },
270
+ { value: "scss", label: "SCSS" },
271
+ { value: "json", label: "JSON" },
272
+ { value: "sql", label: "SQL" },
273
+ { value: "bash", label: "Bash" },
274
+ { value: "yaml", label: "YAML" },
275
+ { value: "xml", label: "XML" },
276
+ { value: "markdown", label: "Markdown" },
277
+ { value: "diff", label: "Diff" },
278
+ { value: "go", label: "Go" },
279
+ { value: "rust", label: "Rust" },
280
+ { value: "java", label: "Java" },
281
+ { value: "ruby", label: "Ruby" },
282
+ { value: "swift", label: "Swift" },
283
+ { value: "docker", label: "Dockerfile" },
284
+ { value: "nginx", label: "Nginx" },
285
+ { value: "env", label: ".env" }
286
+ ];
287
+ function CodeBlockInserter({ onInsert, className, style }) {
288
+ const [open, setOpen] = useState(false);
289
+ const [filter, setFilter] = useState("");
290
+ const dropdownRef = useRef(null);
291
+ const inputRef = useRef(null);
292
+ useEffect(() => {
293
+ if (open && inputRef.current) inputRef.current.focus();
294
+ }, [open]);
295
+ useEffect(() => {
296
+ const handleClickOutside = (e) => {
297
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
298
+ setOpen(false);
299
+ setFilter("");
300
+ }
301
+ };
302
+ if (open) document.addEventListener("mousedown", handleClickOutside);
303
+ return () => document.removeEventListener("mousedown", handleClickOutside);
304
+ }, [open]);
305
+ const filtered = filter ? LANGUAGES.filter((l) => l.label.toLowerCase().includes(filter.toLowerCase()) || l.value.includes(filter.toLowerCase())) : LANGUAGES;
306
+ const handleSelect = (lang) => {
307
+ onInsert(`
308
+ \`\`\`${lang}
309
+
310
+ \`\`\`
311
+ `);
312
+ setOpen(false);
313
+ setFilter("");
314
+ };
315
+ return /* @__PURE__ */ jsxs("div", { ref: dropdownRef, style: { position: "relative", display: "inline-block" }, children: [
316
+ /* @__PURE__ */ jsx(
317
+ "button",
318
+ {
319
+ onClick: () => setOpen(!open),
320
+ className,
321
+ style: {
322
+ fontSize: 11,
323
+ fontWeight: 700,
324
+ padding: "4px 10px",
325
+ width: "auto",
326
+ cursor: "pointer",
327
+ ...style
328
+ },
329
+ type: "button",
330
+ "aria-label": "Ins\xE9rer un bloc de code",
331
+ "data-tooltip": "Bloc de code",
332
+ children: "</> Code"
333
+ }
334
+ ),
335
+ open && /* @__PURE__ */ jsxs("div", { style: {
336
+ position: "absolute",
337
+ bottom: "100%",
338
+ left: 0,
339
+ marginBottom: 4,
340
+ width: 200,
341
+ maxHeight: 280,
342
+ backgroundColor: "#1e293b",
343
+ border: "1px solid #334155",
344
+ borderRadius: 8,
345
+ boxShadow: "0 8px 24px rgba(0,0,0,0.4)",
346
+ zIndex: 1e3,
347
+ overflow: "hidden",
348
+ display: "flex",
349
+ flexDirection: "column"
350
+ }, children: [
351
+ /* @__PURE__ */ jsx("div", { style: { padding: "8px 8px 4px" }, children: /* @__PURE__ */ jsx(
352
+ "input",
353
+ {
354
+ ref: inputRef,
355
+ type: "text",
356
+ value: filter,
357
+ onChange: (e) => setFilter(e.target.value),
358
+ onKeyDown: (e) => {
359
+ if (e.key === "Escape") {
360
+ setOpen(false);
361
+ setFilter("");
362
+ }
363
+ if (e.key === "Enter" && filtered.length > 0) handleSelect(filtered[0].value);
364
+ },
365
+ placeholder: "Rechercher...",
366
+ style: {
367
+ width: "100%",
368
+ padding: "6px 10px",
369
+ fontSize: 12,
370
+ backgroundColor: "#0f172a",
371
+ border: "1px solid #334155",
372
+ borderRadius: 6,
373
+ color: "#e2e8f0",
374
+ outline: "none",
375
+ boxSizing: "border-box"
376
+ }
377
+ }
378
+ ) }),
379
+ /* @__PURE__ */ jsxs("div", { style: { overflowY: "auto", maxHeight: 230, padding: "4px 0" }, children: [
380
+ filtered.map((lang) => /* @__PURE__ */ jsx(
381
+ "button",
382
+ {
383
+ onClick: () => handleSelect(lang.value),
384
+ style: {
385
+ display: "block",
386
+ width: "100%",
387
+ padding: "6px 12px",
388
+ fontSize: 12,
389
+ fontWeight: 500,
390
+ color: "#e2e8f0",
391
+ backgroundColor: "transparent",
392
+ border: "none",
393
+ cursor: "pointer",
394
+ textAlign: "left"
395
+ },
396
+ onMouseEnter: (e) => {
397
+ e.target.style.backgroundColor = "#334155";
398
+ },
399
+ onMouseLeave: (e) => {
400
+ e.target.style.backgroundColor = "transparent";
401
+ },
402
+ type: "button",
403
+ children: lang.label
404
+ },
405
+ lang.value
406
+ )),
407
+ filtered.length === 0 && /* @__PURE__ */ jsx("div", { style: { padding: "12px", fontSize: 12, color: "#64748b", textAlign: "center" }, children: "Aucun langage trouv\xE9" })
408
+ ] })
409
+ ] })
410
+ ] });
411
+ }
412
+ var sourceLabels = {
413
+ email: "Email",
414
+ "live-chat": "Live Chat",
415
+ portal: "Portail",
416
+ admin: "Admin"
417
+ };
418
+ function TicketHeader({
419
+ ticketNumber,
420
+ currentStatus,
421
+ clientSentiment,
422
+ ticketSource,
423
+ chatSession,
424
+ snoozeUntil,
425
+ satisfaction,
426
+ copiedLink,
427
+ onCopyLink
428
+ }) {
429
+ return /* @__PURE__ */ jsxs("div", { style: {
430
+ display: "flex",
431
+ alignItems: "center",
432
+ gap: "10px",
433
+ padding: "10px 14px",
434
+ borderRadius: "8px",
435
+ backgroundColor: "var(--theme-elevation-100)",
436
+ marginBottom: "12px",
437
+ flexWrap: "wrap"
438
+ }, children: [
439
+ ticketNumber && /* @__PURE__ */ jsx("span", { style: { fontFamily: "'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace", fontWeight: 700, fontSize: "14px", color: C.textPrimary }, children: ticketNumber }),
440
+ currentStatus && (() => {
441
+ const st = statusLabels[currentStatus] || statusLabels.open;
442
+ return /* @__PURE__ */ jsx("span", { style: s.badge(st.bg, st.color), children: st.label });
443
+ })(),
444
+ clientSentiment && /* @__PURE__ */ jsxs(
445
+ "span",
446
+ {
447
+ style: { ...s.badge(`${clientSentiment.color}15`, clientSentiment.color), fontSize: "11px" },
448
+ title: `Sentiment : ${clientSentiment.label}`,
449
+ children: [
450
+ clientSentiment.emoji,
451
+ " ",
452
+ clientSentiment.label
453
+ ]
454
+ }
455
+ ),
456
+ ticketSource && ticketSource !== "portal" && /* @__PURE__ */ jsx("span", { style: s.badge("#f1f5f9", "#475569"), children: sourceLabels[ticketSource] || ticketSource }),
457
+ chatSession && ticketSource === "live-chat" && /* @__PURE__ */ jsxs("span", { style: { fontSize: "11px", color: C.textMuted }, children: [
458
+ "Session : ",
459
+ chatSession.slice(0, 16),
460
+ "..."
461
+ ] }),
462
+ snoozeUntil && new Date(snoozeUntil) > /* @__PURE__ */ new Date() && /* @__PURE__ */ jsxs("span", { style: s.badge("#f5f3ff", "#7c3aed"), children: [
463
+ "Snooz\xE9 ",
464
+ new Date(snoozeUntil).toLocaleDateString("fr-FR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" })
465
+ ] }),
466
+ satisfaction && /* @__PURE__ */ jsxs("span", { style: s.badge(
467
+ satisfaction.rating >= 4 ? "#dcfce7" : satisfaction.rating >= 3 ? "#fef9c3" : "#fee2e2",
468
+ satisfaction.rating >= 4 ? "#166534" : satisfaction.rating >= 3 ? "#854d0e" : "#991b1b"
469
+ ), children: [
470
+ Array.from({ length: 5 }, (_, i) => i < satisfaction.rating ? "\u2605" : "\u2606").join(""),
471
+ " ",
472
+ satisfaction.rating,
473
+ "/5"
474
+ ] }),
475
+ /* @__PURE__ */ jsxs("span", { style: { marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: "4px" }, children: [
476
+ /* @__PURE__ */ jsx(
477
+ "button",
478
+ {
479
+ onClick: () => onCopyLink("admin"),
480
+ title: "Copier le lien admin",
481
+ style: { background: "none", border: "none", cursor: "pointer", fontSize: "13px", opacity: copiedLink === "admin" ? 1 : 0.5, padding: "2px 4px" },
482
+ children: copiedLink === "admin" ? "\u2705" : "\u{1F4CB}"
483
+ }
484
+ ),
485
+ /* @__PURE__ */ jsx(
486
+ "button",
487
+ {
488
+ onClick: () => onCopyLink("client"),
489
+ title: "Copier le lien client",
490
+ style: { background: "none", border: "none", cursor: "pointer", fontSize: "13px", opacity: copiedLink === "client" ? 1 : 0.5, padding: "2px 4px" },
491
+ children: copiedLink === "client" ? "\u2705" : "\u{1F517}"
492
+ }
493
+ ),
494
+ copiedLink && /* @__PURE__ */ jsx("span", { style: { fontSize: "11px", color: "#16a34a", fontWeight: 600 }, children: "Copi\xE9" })
495
+ ] })
496
+ ] });
497
+ }
498
+ function ClientBar({ client }) {
499
+ return /* @__PURE__ */ jsxs("div", { style: {
500
+ display: "flex",
501
+ alignItems: "center",
502
+ gap: "12px",
503
+ padding: "10px 16px",
504
+ borderRadius: "8px",
505
+ backgroundColor: "var(--theme-elevation-100)",
506
+ marginBottom: "16px",
507
+ flexWrap: "wrap"
508
+ }, children: [
509
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "16px" }, children: "\u{1F464}" }),
510
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 700, fontSize: "13px", color: C.textPrimary }, children: client.company }),
511
+ /* @__PURE__ */ jsxs("span", { style: { color: C.textSecondary, fontSize: "13px" }, children: [
512
+ client.firstName,
513
+ " ",
514
+ client.lastName
515
+ ] }),
516
+ /* @__PURE__ */ jsx("span", { style: { color: C.textMuted, fontSize: "12px" }, children: "|" }),
517
+ /* @__PURE__ */ jsx("a", { href: `mailto:${client.email}`, style: { color: "#2563eb", fontSize: "12px", fontWeight: 600, textDecoration: "none" }, children: client.email }),
518
+ client.phone && /* @__PURE__ */ jsx("span", { style: { color: C.textSecondary, fontSize: "12px" }, children: client.phone }),
519
+ /* @__PURE__ */ jsxs("span", { style: { marginLeft: "auto", display: "inline-flex", gap: "8px", alignItems: "center" }, children: [
520
+ /* @__PURE__ */ jsx("a", { href: `/admin/collections/support-clients/${client.id}`, style: { ...s.ghostBtn("#475569"), fontSize: "11px", padding: "4px 10px", textDecoration: "none" }, children: "Fiche client" }),
521
+ /* @__PURE__ */ jsx(
522
+ "button",
523
+ {
524
+ type: "button",
525
+ onClick: () => window.open(`/api/admin/impersonate?clientId=${client.id}`, "_blank"),
526
+ style: { ...s.ghostBtn("#7c3aed"), fontSize: "11px", padding: "4px 10px" },
527
+ title: "Se connecter au portail support en tant que ce client",
528
+ children: "Voir en tant que client"
529
+ }
530
+ )
531
+ ] })
532
+ ] });
533
+ }
534
+ function QuickActions({
535
+ statusTransitions,
536
+ statusUpdating,
537
+ onStatusChange,
538
+ snoozeUntil,
539
+ snoozeSaving,
540
+ onCancelSnooze,
541
+ showMerge: _showMerge,
542
+ showExtMsg: _showExtMsg,
543
+ showSnooze: _showSnooze,
544
+ onToggleMerge,
545
+ onToggleExtMsg,
546
+ onToggleSnooze,
547
+ onNextTicket,
548
+ showNextTicket,
549
+ nextTicketId,
550
+ nextTicketInfo,
551
+ onCloseNextTicket
552
+ }) {
553
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
554
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap", marginBottom: "16px", alignItems: "center" }, children: [
555
+ statusTransitions.map((a) => /* @__PURE__ */ jsx(
556
+ "button",
557
+ {
558
+ onClick: () => onStatusChange(a.status),
559
+ disabled: statusUpdating,
560
+ style: s.outlineBtn(a.color, statusUpdating),
561
+ children: a.label
562
+ },
563
+ a.status
564
+ )),
565
+ snoozeUntil && new Date(snoozeUntil) > /* @__PURE__ */ new Date() && /* @__PURE__ */ jsx("button", { onClick: onCancelSnooze, disabled: snoozeSaving, style: { ...s.ghostBtn("#7c3aed", snoozeSaving), fontSize: "12px", padding: "5px 10px" }, children: "Annuler snooze" }),
566
+ /* @__PURE__ */ jsx("span", { style: { borderLeft: `1px solid ${C.border}`, height: "20px", margin: "0 4px" } }),
567
+ /* @__PURE__ */ jsx("button", { onClick: onToggleMerge, style: s.ghostBtn("#be185d"), children: "Fusionner" }),
568
+ /* @__PURE__ */ jsx("button", { onClick: onToggleExtMsg, style: s.ghostBtn("#4f46e5"), children: "+ Message re\xE7u" }),
569
+ /* @__PURE__ */ jsx("button", { onClick: onToggleSnooze, style: s.ghostBtn("#7c3aed"), children: "Snooze" }),
570
+ /* @__PURE__ */ jsx("button", { onClick: onNextTicket, style: s.ghostBtn("#16a34a"), children: "Ticket suivant" })
571
+ ] }),
572
+ showNextTicket && /* @__PURE__ */ jsxs("div", { style: { padding: "10px 14px", borderRadius: "8px", backgroundColor: "#f0fdf4", border: `1px solid ${C.border}`, marginBottom: "14px", display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
573
+ nextTicketId ? /* @__PURE__ */ jsxs(Fragment, { children: [
574
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: "13px", color: "#166534", fontWeight: 600 }, children: [
575
+ "Ticket suivant : ",
576
+ /* @__PURE__ */ jsx("strong", { children: nextTicketInfo })
577
+ ] }),
578
+ /* @__PURE__ */ jsx(
579
+ "button",
580
+ {
581
+ onClick: () => {
582
+ window.location.href = `/admin/support/ticket?id=${nextTicketId}`;
583
+ },
584
+ style: { ...s.btn(C.statusResolved), color: C.white, fontSize: "12px", padding: "5px 14px" },
585
+ children: "Ouvrir"
586
+ }
587
+ )
588
+ ] }) : /* @__PURE__ */ jsx("span", { style: { fontSize: "13px", color: "#166534", fontWeight: 700 }, children: nextTicketInfo }),
589
+ /* @__PURE__ */ jsx("button", { onClick: onCloseNextTicket, style: { border: "none", background: "none", color: C.textMuted, cursor: "pointer", fontSize: "16px", fontWeight: 700, marginLeft: "8px" }, children: "\xD7" })
590
+ ] })
591
+ ] });
592
+ }
593
+ function AISummaryPanel({
594
+ showAiSummary,
595
+ setShowAiSummary,
596
+ aiSummary,
597
+ aiGenerating,
598
+ aiSaving,
599
+ aiSaved,
600
+ handleAiGenerate,
601
+ handleAiSave
602
+ }) {
603
+ return /* @__PURE__ */ jsxs("div", { style: { marginBottom: "14px" }, children: [
604
+ /* @__PURE__ */ jsx(
605
+ "button",
606
+ {
607
+ onClick: () => {
608
+ setShowAiSummary(!showAiSummary);
609
+ if (!showAiSummary && !aiSummary) handleAiGenerate();
610
+ },
611
+ style: {
612
+ ...s.ghostBtn("#7c3aed", false),
613
+ fontSize: "12px",
614
+ display: "inline-flex",
615
+ alignItems: "center",
616
+ gap: "6px"
617
+ },
618
+ children: showAiSummary ? "Masquer la synth\xE8se IA" : "Synth\xE8se IA"
619
+ }
620
+ ),
621
+ showAiSummary && /* @__PURE__ */ jsxs("div", { style: {
622
+ marginTop: "10px",
623
+ padding: "14px 18px",
624
+ borderRadius: "8px",
625
+ backgroundColor: "#faf5ff",
626
+ border: "1px solid #e9d5ff"
627
+ }, children: [
628
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "10px" }, children: [
629
+ /* @__PURE__ */ jsx("h4", { style: { fontSize: "13px", fontWeight: 600, color: "#7c3aed", margin: 0 }, children: "Synth\xE8se IA" }),
630
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "6px" }, children: [
631
+ /* @__PURE__ */ jsx(
632
+ "button",
633
+ {
634
+ onClick: handleAiGenerate,
635
+ disabled: aiGenerating,
636
+ style: { ...s.outlineBtn("#7c3aed", aiGenerating), fontSize: "11px", padding: "4px 10px" },
637
+ children: aiGenerating ? "G\xE9n\xE9ration..." : "R\xE9g\xE9n\xE9rer"
638
+ }
639
+ ),
640
+ aiSummary && !aiGenerating && /* @__PURE__ */ jsx(
641
+ "button",
642
+ {
643
+ onClick: handleAiSave,
644
+ disabled: aiSaving || aiSaved,
645
+ style: { ...s.btn(aiSaved ? "#16a34a" : "#2563eb", aiSaving), fontSize: "11px", padding: "4px 10px" },
646
+ children: aiSaved ? "Sauvegard\xE9" : aiSaving ? "Sauvegarde..." : "Sauvegarder (note interne)"
647
+ }
648
+ )
649
+ ] })
650
+ ] }),
651
+ aiGenerating ? /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", gap: "10px", color: "#7c3aed", fontSize: "13px" }, children: "Analyse de la conversation en cours..." }) : aiSummary ? /* @__PURE__ */ jsx(
652
+ "div",
653
+ {
654
+ style: { fontSize: "13px", lineHeight: "1.7", color: "#1e1b4b", whiteSpace: "pre-wrap" },
655
+ dangerouslySetInnerHTML: {
656
+ __html: aiSummary.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>").replace(/\n/g, "<br/>")
657
+ }
658
+ }
659
+ ) : /* @__PURE__ */ jsx("p", { style: { color: "#999", fontStyle: "italic", fontSize: "13px", margin: 0 }, children: `Cliquez sur "R\xE9g\xE9n\xE9rer" pour lancer l'analyse` })
660
+ ] })
661
+ ] });
662
+ }
663
+ function MergePanel({
664
+ mergeTarget,
665
+ setMergeTarget,
666
+ mergeTargetInfo,
667
+ setMergeTargetInfo,
668
+ mergeError,
669
+ setMergeError,
670
+ merging,
671
+ handleMergeLookup,
672
+ handleMerge
673
+ }) {
674
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "14px 18px", borderRadius: "8px", backgroundColor: "#fdf2f8", border: "1px solid #fbcfe8", marginBottom: "14px" }, children: [
675
+ /* @__PURE__ */ jsx("h4", { style: { fontSize: "13px", fontWeight: 600, marginBottom: "10px", color: "#831843" }, children: "Fusionner ce ticket dans un autre" }),
676
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "center", marginBottom: "8px" }, children: [
677
+ /* @__PURE__ */ jsx(
678
+ "input",
679
+ {
680
+ type: "text",
681
+ value: mergeTarget,
682
+ onChange: (e) => {
683
+ setMergeTarget(e.target.value);
684
+ setMergeTargetInfo(null);
685
+ setMergeError("");
686
+ },
687
+ placeholder: "TK-0001",
688
+ style: { ...s.input, width: "130px" }
689
+ }
690
+ ),
691
+ /* @__PURE__ */ jsx("button", { onClick: handleMergeLookup, style: { ...s.outlineBtn("#ec4899"), fontSize: "12px", padding: "6px 14px" }, children: "Rechercher" }),
692
+ mergeError && /* @__PURE__ */ jsx("span", { style: { fontSize: "12px", color: "#be185d", fontWeight: 600 }, children: mergeError })
693
+ ] }),
694
+ mergeTargetInfo && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "center" }, children: [
695
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: "13px", fontWeight: 600 }, children: [
696
+ mergeTargetInfo.ticketNumber,
697
+ " \u2014 ",
698
+ mergeTargetInfo.subject
699
+ ] }),
700
+ /* @__PURE__ */ jsx("button", { onClick: handleMerge, disabled: merging, style: { ...s.btn("#ec4899", merging), color: "#fff", fontSize: "12px", padding: "6px 14px" }, children: merging ? "Fusion..." : "Confirmer la fusion" })
701
+ ] })
702
+ ] });
703
+ }
704
+ function ExtMessagePanel({
705
+ extMsgBody,
706
+ setExtMsgBody,
707
+ extMsgAuthor,
708
+ setExtMsgAuthor,
709
+ extMsgDate,
710
+ setExtMsgDate,
711
+ extMsgFiles,
712
+ setExtMsgFiles,
713
+ sendingExtMsg,
714
+ handleSendExtMsg,
715
+ handleExtFileChange
716
+ }) {
717
+ const extFileInputRef = useRef(null);
718
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "14px 18px", borderRadius: "8px", backgroundColor: "#eef2ff", border: "1px solid #c7d2fe", marginBottom: "14px" }, children: [
719
+ /* @__PURE__ */ jsx("h4", { style: { fontSize: "13px", fontWeight: 600, marginBottom: "10px", color: "#312e81" }, children: "Ajouter un message re\xE7u (email, SMS, WhatsApp...)" }),
720
+ /* @__PURE__ */ jsx(
721
+ "textarea",
722
+ {
723
+ value: extMsgBody,
724
+ onChange: (e) => setExtMsgBody(e.target.value),
725
+ placeholder: "Coller le contenu du message re\xE7u...",
726
+ rows: 3,
727
+ style: { ...s.input, width: "100%", resize: "vertical", marginBottom: "10px" }
728
+ }
729
+ ),
730
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap", marginBottom: "10px" }, children: [
731
+ /* @__PURE__ */ jsxs("select", { value: extMsgAuthor, onChange: (e) => setExtMsgAuthor(e.target.value), style: { ...s.input, fontSize: "12px" }, children: [
732
+ /* @__PURE__ */ jsx("option", { value: "client", children: "Envoy\xE9 par le client" }),
733
+ /* @__PURE__ */ jsx("option", { value: "admin", children: "Envoy\xE9 par le support" })
734
+ ] }),
735
+ /* @__PURE__ */ jsx(
736
+ "input",
737
+ {
738
+ type: "datetime-local",
739
+ value: extMsgDate,
740
+ onChange: (e) => setExtMsgDate(e.target.value),
741
+ style: { ...s.input, fontSize: "12px" }
742
+ }
743
+ ),
744
+ /* @__PURE__ */ jsx("input", { ref: extFileInputRef, type: "file", multiple: true, onChange: handleExtFileChange, style: { display: "none" }, accept: "image/*,.pdf,.doc,.docx,.txt,.zip" }),
745
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => extFileInputRef.current?.click(), style: { ...s.ghostBtn("#6b7280"), fontSize: "12px", padding: "6px 12px" }, children: "+ PJ" })
746
+ ] }),
747
+ extMsgFiles.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginBottom: "10px", display: "flex", gap: "6px", flexWrap: "wrap" }, children: extMsgFiles.map((file, i) => /* @__PURE__ */ jsxs("span", { style: { ...s.badge("#f1f5f9", "#374151"), display: "inline-flex", alignItems: "center", gap: "4px" }, children: [
748
+ file.name,
749
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setExtMsgFiles((prev) => prev.filter((_, idx) => idx !== i)), style: { border: "none", background: "none", color: "#ef4444", fontWeight: 700, cursor: "pointer", fontSize: "14px", lineHeight: 1 }, children: "\xD7" })
750
+ ] }, i)) }),
751
+ /* @__PURE__ */ jsx("button", { onClick: handleSendExtMsg, disabled: sendingExtMsg || !extMsgBody.trim(), style: s.btn("#818cf8", sendingExtMsg || !extMsgBody.trim()), children: sendingExtMsg ? "Ajout..." : "Ajouter (sans notification)" })
752
+ ] });
753
+ }
754
+ function SnoozePanel({ snoozeSaving, handleSnooze }) {
755
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "14px 18px", borderRadius: "8px", backgroundColor: "#faf5ff", border: "1px solid #e9d5ff", marginBottom: "14px" }, children: [
756
+ /* @__PURE__ */ jsx("h4", { style: { fontSize: "13px", fontWeight: 600, marginBottom: "10px", color: "#5b21b6" }, children: "Snooze \u2014 masquer temporairement" }),
757
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap", alignItems: "center" }, children: [
758
+ /* @__PURE__ */ jsx("button", { onClick: () => handleSnooze(1), disabled: snoozeSaving, style: { ...s.outlineBtn("#8b5cf6", snoozeSaving), fontSize: "12px" }, children: "1 jour" }),
759
+ /* @__PURE__ */ jsx("button", { onClick: () => handleSnooze(3), disabled: snoozeSaving, style: { ...s.outlineBtn("#8b5cf6", snoozeSaving), fontSize: "12px" }, children: "3 jours" }),
760
+ /* @__PURE__ */ jsx("button", { onClick: () => handleSnooze(7), disabled: snoozeSaving, style: { ...s.outlineBtn("#8b5cf6", snoozeSaving), fontSize: "12px" }, children: "1 semaine" }),
761
+ /* @__PURE__ */ jsx(
762
+ "input",
763
+ {
764
+ type: "datetime-local",
765
+ onChange: (e) => {
766
+ if (e.target.value) handleSnooze(null, e.target.value);
767
+ },
768
+ style: { ...s.input, fontSize: "12px" }
769
+ }
770
+ )
771
+ ] })
772
+ ] });
773
+ }
774
+ function ActivityLog({ activityLog }) {
775
+ const [showActivity, setShowActivity] = useState(false);
776
+ return /* @__PURE__ */ jsxs("div", { style: s.section, children: [
777
+ /* @__PURE__ */ jsxs("h4", { style: { ...s.sectionTitle, cursor: "pointer" }, onClick: () => setShowActivity(!showActivity), children: [
778
+ "Historique ",
779
+ /* @__PURE__ */ jsx("span", { style: s.badge("#f1f5f9", "#475569"), children: activityLog.length }),
780
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "12px", color: C.textMuted, transition: "transform 0.2s", display: "inline-block", transform: showActivity ? "rotate(90deg)" : "none" }, children: "\u25B6" })
781
+ ] }),
782
+ showActivity && (activityLog.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: "12px", color: C.textMuted, fontStyle: "italic" }, children: "Aucune activit\xE9 enregistr\xE9e." }) : /* @__PURE__ */ jsx("div", { style: { fontSize: "12px", display: "flex", flexDirection: "column", gap: "2px", borderLeft: "2px solid #bfdbfe", paddingLeft: "14px", marginLeft: "4px" }, children: activityLog.map((entry) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "10px", alignItems: "center", padding: "5px 0", borderBottom: "1px solid #f8fafc" }, children: [
783
+ /* @__PURE__ */ jsx("span", { style: { color: C.textMuted, fontSize: "11px", whiteSpace: "nowrap", fontWeight: 500 }, children: new Date(entry.createdAt).toLocaleString("fr-FR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }) }),
784
+ /* @__PURE__ */ jsx("span", { style: s.badge(entry.actorType === "admin" ? "#eff6ff" : "#dcfce7", entry.actorType === "admin" ? "#1e40af" : "#166534"), children: entry.actorType || "system" }),
785
+ /* @__PURE__ */ jsx("span", { style: { color: "#374151", fontWeight: 500 }, children: (entry.detail || entry.action).replace(/\[object Object\]/g, "(utilisateur)") }),
786
+ /* @__PURE__ */ jsx("span", { style: { color: C.textMuted, fontSize: "11px" }, children: entry.actorEmail })
787
+ ] }, entry.id)) }))
788
+ ] });
789
+ }
790
+ function ClientHistory({
791
+ client: _client,
792
+ clientTickets,
793
+ clientProjects,
794
+ clientNotes,
795
+ onNotesChange,
796
+ onNotesSave,
797
+ savingNotes,
798
+ notesSaved
799
+ }) {
800
+ const [showClientHistory, setShowClientHistory] = useState(false);
801
+ return /* @__PURE__ */ jsxs("div", { style: s.section, children: [
802
+ /* @__PURE__ */ jsxs(
803
+ "button",
804
+ {
805
+ type: "button",
806
+ onClick: () => setShowClientHistory(!showClientHistory),
807
+ style: {
808
+ background: "none",
809
+ border: `1px dashed ${C.border}`,
810
+ borderRadius: "6px",
811
+ padding: "8px 14px",
812
+ cursor: "pointer",
813
+ color: C.textSecondary,
814
+ fontSize: "13px",
815
+ fontWeight: 600,
816
+ width: "100%",
817
+ textAlign: "left",
818
+ display: "flex",
819
+ alignItems: "center",
820
+ justifyContent: "space-between"
821
+ },
822
+ children: [
823
+ /* @__PURE__ */ jsxs("span", { children: [
824
+ "Historique client (",
825
+ clientTickets.length,
826
+ " ticket",
827
+ clientTickets.length !== 1 ? "s" : "",
828
+ ", ",
829
+ clientProjects.length,
830
+ " projet",
831
+ clientProjects.length !== 1 ? "s" : "",
832
+ ")"
833
+ ] }),
834
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "12px", transition: "transform 0.2s", display: "inline-block", transform: showClientHistory ? "rotate(90deg)" : "none" }, children: "\u25B6" })
835
+ ]
836
+ }
837
+ ),
838
+ showClientHistory && /* @__PURE__ */ jsxs("div", { style: { marginTop: "10px", padding: "14px 18px", borderRadius: "8px", backgroundColor: C.white, border: `1px solid ${C.border}` }, children: [
839
+ /* @__PURE__ */ jsx("h5", { style: { fontSize: "12px", fontWeight: 600, color: C.textSecondary, marginBottom: "8px" }, children: "Derniers tickets" }),
840
+ clientTickets.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: "12px", color: C.textMuted, fontStyle: "italic", marginBottom: "14px" }, children: "Aucun autre ticket" }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: "4px", marginBottom: "14px" }, children: clientTickets.map((t) => {
841
+ const st = statusLabels[t.status] || statusLabels.open;
842
+ return /* @__PURE__ */ jsxs(
843
+ "a",
844
+ {
845
+ href: `/admin/support/ticket?id=${t.id}`,
846
+ style: {
847
+ display: "flex",
848
+ alignItems: "center",
849
+ gap: "8px",
850
+ padding: "6px 10px",
851
+ borderRadius: "6px",
852
+ border: `1px solid ${C.border}`,
853
+ textDecoration: "none",
854
+ fontSize: "12px",
855
+ color: "#374151",
856
+ backgroundColor: "#fafafa"
857
+ },
858
+ children: [
859
+ /* @__PURE__ */ jsx("span", { style: { fontFamily: "'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace", fontWeight: 600, color: C.textMuted, fontSize: "11px", whiteSpace: "nowrap" }, children: t.ticketNumber }),
860
+ /* @__PURE__ */ jsx("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontWeight: 600 }, children: t.subject }),
861
+ /* @__PURE__ */ jsx("span", { style: s.badge(st.bg, st.color), children: st.label }),
862
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "10px", color: C.textMuted, whiteSpace: "nowrap" }, children: new Date(t.createdAt).toLocaleDateString("fr-FR", { day: "numeric", month: "short" }) })
863
+ ]
864
+ },
865
+ t.id
866
+ );
867
+ }) }),
868
+ /* @__PURE__ */ jsx("h5", { style: { fontSize: "12px", fontWeight: 600, color: C.textSecondary, marginBottom: "8px" }, children: "Projets" }),
869
+ clientProjects.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: "12px", color: C.textMuted, fontStyle: "italic", marginBottom: "14px" }, children: "Aucun projet" }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap", marginBottom: "14px" }, children: clientProjects.map((p) => {
870
+ const ps = projectStatusLabels[p.status] || projectStatusLabels.active;
871
+ return /* @__PURE__ */ jsxs(
872
+ "a",
873
+ {
874
+ href: `/admin/collections/projects/${p.id}`,
875
+ style: {
876
+ display: "inline-flex",
877
+ alignItems: "center",
878
+ gap: "6px",
879
+ padding: "5px 10px",
880
+ borderRadius: "6px",
881
+ border: `1px solid ${C.border}`,
882
+ textDecoration: "none",
883
+ fontSize: "12px",
884
+ fontWeight: 600,
885
+ color: "#374151",
886
+ backgroundColor: "#fafafa"
887
+ },
888
+ children: [
889
+ p.name,
890
+ /* @__PURE__ */ jsx("span", { style: s.badge(ps.bg, ps.color), children: ps.label })
891
+ ]
892
+ },
893
+ p.id
894
+ );
895
+ }) }),
896
+ /* @__PURE__ */ jsx("h5", { style: { fontSize: "12px", fontWeight: 600, color: C.textSecondary, marginBottom: "8px" }, children: "Notes internes" }),
897
+ /* @__PURE__ */ jsx(
898
+ "textarea",
899
+ {
900
+ value: clientNotes,
901
+ onChange: (e) => onNotesChange(e.target.value),
902
+ rows: 3,
903
+ style: { ...s.input, width: "100%", resize: "vertical", fontSize: "12px", marginBottom: "8px" },
904
+ placeholder: "Notes sur ce client..."
905
+ }
906
+ ),
907
+ /* @__PURE__ */ jsx(
908
+ "button",
909
+ {
910
+ type: "button",
911
+ onClick: onNotesSave,
912
+ disabled: savingNotes,
913
+ style: { ...s.btn(notesSaved ? "#16a34a" : C.blue, savingNotes), fontSize: "11px", padding: "5px 12px" },
914
+ children: notesSaved ? "Sauvegarde OK" : savingNotes ? "Sauvegarde..." : "Sauvegarder les notes"
915
+ }
916
+ )
917
+ ] })
918
+ ] });
919
+ }
920
+ function TimeTrackingPanel({
921
+ timeEntries,
922
+ totalMinutes,
923
+ timerRunning,
924
+ timerSeconds,
925
+ setTimerSeconds,
926
+ timerDescription,
927
+ setTimerDescription,
928
+ handleTimerStart,
929
+ handleTimerStop,
930
+ handleTimerSave,
931
+ handleTimerDiscard,
932
+ duration,
933
+ setDuration,
934
+ timeDescription,
935
+ setTimeDescription,
936
+ handleAddTime,
937
+ addingTime,
938
+ timeSuccess
939
+ }) {
940
+ const totalH = Math.floor(totalMinutes / 60);
941
+ const totalM = totalMinutes % 60;
942
+ return /* @__PURE__ */ jsxs("div", { style: s.section, children: [
943
+ /* @__PURE__ */ jsxs("h4", { style: s.sectionTitle, children: [
944
+ "Temps",
945
+ totalMinutes > 0 && /* @__PURE__ */ jsxs("span", { style: s.badge("#fef3c7", "#92400e"), children: [
946
+ totalH,
947
+ "h",
948
+ String(totalM).padStart(2, "0"),
949
+ " total"
950
+ ] })
951
+ ] }),
952
+ /* @__PURE__ */ jsxs("div", { style: {
953
+ display: "flex",
954
+ alignItems: "center",
955
+ gap: "10px",
956
+ flexWrap: "wrap",
957
+ padding: "12px 16px",
958
+ borderRadius: "10px",
959
+ marginBottom: "14px",
960
+ backgroundColor: timerRunning ? "#fef2f2" : "var(--theme-elevation-100)",
961
+ border: timerRunning ? "1px solid #fca5a5" : "1px solid var(--theme-elevation-300)"
962
+ }, children: [
963
+ /* @__PURE__ */ jsxs("div", { style: {
964
+ fontFamily: "monospace",
965
+ fontSize: "24px",
966
+ fontWeight: 700,
967
+ color: timerRunning ? "#dc2626" : "var(--theme-text)",
968
+ minWidth: "90px"
969
+ }, children: [
970
+ String(Math.floor(timerSeconds / 3600)).padStart(2, "0"),
971
+ ":",
972
+ String(Math.floor(timerSeconds % 3600 / 60)).padStart(2, "0"),
973
+ ":",
974
+ String(timerSeconds % 60).padStart(2, "0")
975
+ ] }),
976
+ !timerRunning && timerSeconds > 0 && /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", gap: "4px" }, children: [-5, -1, 1, 5, 15, 30].map((m) => /* @__PURE__ */ jsx(
977
+ "button",
978
+ {
979
+ onClick: () => setTimerSeconds((p) => Math.max(0, p + m * 60)),
980
+ style: { width: Math.abs(m) >= 15 ? "32px" : "28px", height: "28px", borderRadius: "6px", border: "1px solid #e2e8f0", background: "white", cursor: "pointer", fontSize: Math.abs(m) >= 15 ? "12px" : "14px", fontWeight: 700, color: "#64748b" },
981
+ title: `${m > 0 ? "+" : ""}${m} min`,
982
+ children: m > 0 ? `+${m}` : String(m)
983
+ },
984
+ m
985
+ )) }),
986
+ !timerRunning && timerSeconds === 0 && /* @__PURE__ */ jsx("button", { onClick: () => handleTimerStart(true), style: { ...s.btn("#dc2626", false), fontSize: "12px", padding: "6px 16px", display: "flex", alignItems: "center", gap: "6px" }, children: "\u25B6 D\xE9marrer" }),
987
+ timerRunning && /* @__PURE__ */ jsx("button", { onClick: handleTimerStop, style: { ...s.btn("#374151", false), fontSize: "12px", padding: "6px 16px", display: "flex", alignItems: "center", gap: "6px" }, children: "\u23F8 Pause" }),
988
+ !timerRunning && timerSeconds > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
989
+ /* @__PURE__ */ jsx("button", { onClick: () => handleTimerStart(false), style: { ...s.btn("#dc2626", false), fontSize: "12px", padding: "6px 14px" }, children: "\u25B6 Reprendre" }),
990
+ /* @__PURE__ */ jsx(
991
+ "input",
992
+ {
993
+ type: "text",
994
+ value: timerDescription,
995
+ onChange: (e) => setTimerDescription(e.target.value),
996
+ placeholder: "Description...",
997
+ style: { ...s.input, fontSize: "12px", flex: 1, minWidth: "120px" }
998
+ }
999
+ ),
1000
+ /* @__PURE__ */ jsxs(
1001
+ "button",
1002
+ {
1003
+ onClick: handleTimerSave,
1004
+ disabled: addingTime || timerSeconds < 60,
1005
+ style: { ...s.btn("#16a34a", addingTime || timerSeconds < 60), fontSize: "12px", padding: "6px 14px" },
1006
+ title: timerSeconds < 60 ? "Minimum 1 minute" : `Sauvegarder ${Math.round(timerSeconds / 60)} min`,
1007
+ children: [
1008
+ "\u{1F4BE} ",
1009
+ Math.round(timerSeconds / 60),
1010
+ " min"
1011
+ ]
1012
+ }
1013
+ ),
1014
+ /* @__PURE__ */ jsx("button", { onClick: handleTimerDiscard, style: { background: "none", border: "none", cursor: "pointer", fontSize: "14px", color: "#94a3b8", padding: "4px" }, title: "Annuler", children: "\u2715" })
1015
+ ] }),
1016
+ timerRunning && /* @__PURE__ */ jsx("span", { style: { fontSize: "11px", color: "#dc2626", fontWeight: 600 }, children: "\u25CF Enregistrement en cours \u2014 session maintenue active" })
1017
+ ] }),
1018
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end", flexWrap: "wrap", marginBottom: "14px" }, children: [
1019
+ /* @__PURE__ */ jsxs("div", { children: [
1020
+ /* @__PURE__ */ jsx("label", { style: { display: "block", fontSize: "11px", color: C.textSecondary, marginBottom: "3px", fontWeight: 600 }, children: "Min" }),
1021
+ /* @__PURE__ */ jsx("input", { type: "number", min: "1", value: duration, onChange: (e) => setDuration(e.target.value), placeholder: "30", style: { ...s.input, width: "80px" } })
1022
+ ] }),
1023
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
1024
+ /* @__PURE__ */ jsx("label", { style: { display: "block", fontSize: "11px", color: C.textSecondary, marginBottom: "3px", fontWeight: 600 }, children: "Description" }),
1025
+ /* @__PURE__ */ jsx("input", { type: "text", value: timeDescription, onChange: (e) => setTimeDescription(e.target.value), placeholder: "Travail effectu\xE9...", style: { ...s.input, width: "100%" } })
1026
+ ] }),
1027
+ /* @__PURE__ */ jsx("button", { onClick: handleAddTime, disabled: addingTime || !duration, style: s.btn(C.amber, addingTime || !duration), children: addingTime ? "..." : "+ Temps" }),
1028
+ timeSuccess && /* @__PURE__ */ jsx("span", { style: { fontSize: "12px", color: "#16a34a", fontWeight: 600 }, children: timeSuccess })
1029
+ ] }),
1030
+ timeEntries.length > 0 && /* @__PURE__ */ jsx("div", { style: { fontSize: "12px" }, children: /* @__PURE__ */ jsxs("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
1031
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { style: { borderBottom: `1px solid ${C.border}` }, children: [
1032
+ /* @__PURE__ */ jsx("th", { style: { textAlign: "left", padding: "6px 8px", fontWeight: 600, fontSize: "11px", color: C.textSecondary }, children: "Date" }),
1033
+ /* @__PURE__ */ jsx("th", { style: { textAlign: "left", padding: "6px 8px", fontWeight: 600, fontSize: "11px", color: C.textSecondary }, children: "Dur\xE9e" }),
1034
+ /* @__PURE__ */ jsx("th", { style: { textAlign: "left", padding: "6px 8px", fontWeight: 600, fontSize: "11px", color: C.textSecondary }, children: "Description" })
1035
+ ] }) }),
1036
+ /* @__PURE__ */ jsx("tbody", { children: timeEntries.map((entry) => /* @__PURE__ */ jsxs("tr", { style: { borderBottom: "1px solid #f1f5f9" }, children: [
1037
+ /* @__PURE__ */ jsx("td", { style: { padding: "6px 8px", color: "#374151" }, children: new Date(entry.date).toLocaleDateString("fr-FR", { day: "numeric", month: "short" }) }),
1038
+ /* @__PURE__ */ jsxs("td", { style: { padding: "6px 8px", fontWeight: 600, color: "#374151" }, children: [
1039
+ entry.duration,
1040
+ " min"
1041
+ ] }),
1042
+ /* @__PURE__ */ jsx("td", { style: { padding: "6px 8px", color: C.textSecondary }, children: entry.description || "\u2014" })
1043
+ ] }, entry.id)) })
1044
+ ] }) })
1045
+ ] });
1046
+ }
1047
+ function useTimeTracking(id, fetchAll) {
1048
+ const [duration, setDuration] = useState("");
1049
+ const [timeDescription, setTimeDescription] = useState("");
1050
+ const [addingTime, setAddingTime] = useState(false);
1051
+ const [timeSuccess, setTimeSuccess] = useState("");
1052
+ const [timerRunning, setTimerRunning] = useState(false);
1053
+ const [timerSeconds, setTimerSeconds] = useState(0);
1054
+ const [timerDescription, setTimerDescription] = useState("");
1055
+ const timerIntervalRef = useRef(null);
1056
+ const timerKeepAliveRef = useRef(null);
1057
+ const handleAddTime = async () => {
1058
+ if (!duration || !id) return;
1059
+ setAddingTime(true);
1060
+ setTimeSuccess("");
1061
+ try {
1062
+ const res = await fetch("/api/time-entries", {
1063
+ method: "POST",
1064
+ headers: { "Content-Type": "application/json" },
1065
+ credentials: "include",
1066
+ body: JSON.stringify({
1067
+ ticket: id,
1068
+ duration: parseInt(duration, 10),
1069
+ date: (/* @__PURE__ */ new Date()).toISOString(),
1070
+ description: timeDescription.trim() || void 0
1071
+ })
1072
+ });
1073
+ if (res.ok) {
1074
+ setDuration("");
1075
+ setTimeDescription("");
1076
+ setTimeSuccess(`${duration} min ajout\xE9es`);
1077
+ setTimeout(() => setTimeSuccess(""), 3e3);
1078
+ fetchAll();
1079
+ }
1080
+ } catch {
1081
+ } finally {
1082
+ setAddingTime(false);
1083
+ }
1084
+ };
1085
+ const handleTimerStart = (reset = false) => {
1086
+ setTimerRunning(true);
1087
+ if (reset) setTimerSeconds(0);
1088
+ timerIntervalRef.current = setInterval(() => {
1089
+ setTimerSeconds((prev) => prev + 1);
1090
+ }, 1e3);
1091
+ timerKeepAliveRef.current = setInterval(() => {
1092
+ fetch("/api/users/me", { credentials: "include" }).catch(() => {
1093
+ });
1094
+ }, 5 * 60 * 1e3);
1095
+ };
1096
+ const handleTimerStop = () => {
1097
+ setTimerRunning(false);
1098
+ if (timerIntervalRef.current) {
1099
+ clearInterval(timerIntervalRef.current);
1100
+ timerIntervalRef.current = null;
1101
+ }
1102
+ if (timerKeepAliveRef.current) {
1103
+ clearInterval(timerKeepAliveRef.current);
1104
+ timerKeepAliveRef.current = null;
1105
+ }
1106
+ };
1107
+ const handleTimerSave = async () => {
1108
+ if (!id || timerSeconds < 60) return;
1109
+ const minutes = Math.round(timerSeconds / 60);
1110
+ setAddingTime(true);
1111
+ try {
1112
+ const res = await fetch("/api/time-entries", {
1113
+ method: "POST",
1114
+ headers: { "Content-Type": "application/json" },
1115
+ credentials: "include",
1116
+ body: JSON.stringify({
1117
+ ticket: id,
1118
+ duration: minutes,
1119
+ date: (/* @__PURE__ */ new Date()).toISOString(),
1120
+ description: timerDescription.trim() || "Timer"
1121
+ })
1122
+ });
1123
+ if (res.ok) {
1124
+ setTimeSuccess(`${minutes} min ajout\xE9es (timer)`);
1125
+ setTimeout(() => setTimeSuccess(""), 3e3);
1126
+ setTimerSeconds(0);
1127
+ setTimerDescription("");
1128
+ fetchAll();
1129
+ }
1130
+ } catch {
1131
+ } finally {
1132
+ setAddingTime(false);
1133
+ }
1134
+ };
1135
+ const handleTimerDiscard = () => {
1136
+ setTimerSeconds(0);
1137
+ setTimerDescription("");
1138
+ };
1139
+ useEffect(() => {
1140
+ return () => {
1141
+ if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
1142
+ if (timerKeepAliveRef.current) clearInterval(timerKeepAliveRef.current);
1143
+ };
1144
+ }, []);
1145
+ return {
1146
+ duration,
1147
+ setDuration,
1148
+ timeDescription,
1149
+ setTimeDescription,
1150
+ addingTime,
1151
+ timeSuccess,
1152
+ timerRunning,
1153
+ timerSeconds,
1154
+ setTimerSeconds,
1155
+ timerDescription,
1156
+ setTimerDescription,
1157
+ handleAddTime,
1158
+ handleTimerStart,
1159
+ handleTimerStop,
1160
+ handleTimerSave,
1161
+ handleTimerDiscard
1162
+ };
1163
+ }
1164
+ function useMessageActions(id, client, fetchAll) {
1165
+ const [togglingAuthor, setTogglingAuthor] = useState(null);
1166
+ const [editingMsg, setEditingMsg] = useState(null);
1167
+ const [editBody, setEditBody] = useState("");
1168
+ const [editHtml, setEditHtml] = useState("");
1169
+ const [savingEdit, setSavingEdit] = useState(false);
1170
+ const [deletingMsg, setDeletingMsg] = useState(null);
1171
+ const [resendingMsg, setResendingMsg] = useState(null);
1172
+ const [resendSuccess, setResendSuccess] = useState(null);
1173
+ const handleEditStart = (msg) => {
1174
+ setEditingMsg(msg.id);
1175
+ setEditBody(msg.body);
1176
+ setEditHtml(msg.bodyHtml || msg.body.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br />"));
1177
+ };
1178
+ const handleEditSave = async (msgId) => {
1179
+ if (!editBody.trim()) return;
1180
+ setSavingEdit(true);
1181
+ try {
1182
+ const res = await fetch(`/api/ticket-messages/${msgId}`, {
1183
+ method: "PATCH",
1184
+ headers: { "Content-Type": "application/json" },
1185
+ credentials: "include",
1186
+ body: JSON.stringify({ body: editBody.trim(), ...editHtml ? { bodyHtml: editHtml } : {} })
1187
+ });
1188
+ if (res.ok) {
1189
+ setEditingMsg(null);
1190
+ setEditBody("");
1191
+ fetchAll();
1192
+ }
1193
+ } catch {
1194
+ } finally {
1195
+ setSavingEdit(false);
1196
+ }
1197
+ };
1198
+ const handleEditCancel = () => {
1199
+ setEditingMsg(null);
1200
+ setEditBody("");
1201
+ setEditHtml("");
1202
+ };
1203
+ const handleDelete = async (msgId) => {
1204
+ if (!window.confirm("Supprimer ce message ?")) return;
1205
+ setDeletingMsg(msgId);
1206
+ try {
1207
+ const res = await fetch(`/api/ticket-messages/${msgId}`, {
1208
+ method: "DELETE",
1209
+ credentials: "include"
1210
+ });
1211
+ if (res.ok) fetchAll();
1212
+ } catch {
1213
+ } finally {
1214
+ setDeletingMsg(null);
1215
+ }
1216
+ };
1217
+ const handleResend = async (msgId) => {
1218
+ setResendingMsg(msgId);
1219
+ setResendSuccess(null);
1220
+ try {
1221
+ const res = await fetch("/api/support/resend-notification", {
1222
+ method: "POST",
1223
+ headers: { "Content-Type": "application/json" },
1224
+ credentials: "include",
1225
+ body: JSON.stringify({ messageId: msgId })
1226
+ });
1227
+ if (res.ok) {
1228
+ setResendSuccess(msgId);
1229
+ setTimeout(() => setResendSuccess(null), 3e3);
1230
+ }
1231
+ } catch {
1232
+ } finally {
1233
+ setResendingMsg(null);
1234
+ }
1235
+ };
1236
+ const handleToggleAuthor = async (msgId, currentType) => {
1237
+ const newType = currentType === "admin" ? "client" : "admin";
1238
+ setTogglingAuthor(msgId);
1239
+ try {
1240
+ const patchData = { authorType: newType };
1241
+ if (newType === "client" && client) {
1242
+ patchData.authorClient = client.id;
1243
+ }
1244
+ const res = await fetch(`/api/ticket-messages/${msgId}`, {
1245
+ method: "PATCH",
1246
+ headers: { "Content-Type": "application/json" },
1247
+ credentials: "include",
1248
+ body: JSON.stringify(patchData)
1249
+ });
1250
+ if (res.ok) fetchAll();
1251
+ } catch {
1252
+ } finally {
1253
+ setTogglingAuthor(null);
1254
+ }
1255
+ };
1256
+ const handleSplitMessage = async (msgId, ticketSubject) => {
1257
+ const subject = prompt("Sujet du nouveau ticket :", `Split: ${ticketSubject}`);
1258
+ if (!subject) return;
1259
+ try {
1260
+ const res = await fetch("/api/support/split-ticket", {
1261
+ method: "POST",
1262
+ headers: { "Content-Type": "application/json" },
1263
+ credentials: "include",
1264
+ body: JSON.stringify({ messageId: msgId, subject })
1265
+ });
1266
+ if (res.ok) {
1267
+ const data = await res.json();
1268
+ alert(`Ticket ${data.ticketNumber} cr\xE9\xE9`);
1269
+ fetchAll();
1270
+ }
1271
+ } catch {
1272
+ }
1273
+ };
1274
+ return {
1275
+ togglingAuthor,
1276
+ editingMsg,
1277
+ editBody,
1278
+ editHtml,
1279
+ setEditHtml,
1280
+ savingEdit,
1281
+ handleEditStart,
1282
+ handleEditSave,
1283
+ handleEditCancel,
1284
+ deletingMsg,
1285
+ handleDelete,
1286
+ resendingMsg,
1287
+ resendSuccess,
1288
+ handleResend,
1289
+ handleToggleAuthor,
1290
+ handleSplitMessage,
1291
+ setEditBody
1292
+ };
1293
+ }
1294
+ function useTicketActions(id, fetchAll) {
1295
+ const [statusUpdating, setStatusUpdating] = useState(false);
1296
+ const [showMerge, setShowMerge] = useState(false);
1297
+ const [mergeTarget, setMergeTarget] = useState("");
1298
+ const [mergeTargetInfo, setMergeTargetInfo] = useState(null);
1299
+ const [merging, setMerging] = useState(false);
1300
+ const [mergeError, setMergeError] = useState("");
1301
+ const [showExtMsg, setShowExtMsg] = useState(false);
1302
+ const [extMsgBody, setExtMsgBody] = useState("");
1303
+ const [extMsgAuthor, setExtMsgAuthor] = useState("client");
1304
+ const [extMsgDate, setExtMsgDate] = useState("");
1305
+ const [extMsgFiles, setExtMsgFiles] = useState([]);
1306
+ const [sendingExtMsg, setSendingExtMsg] = useState(false);
1307
+ const extFileInputRef = useRef(null);
1308
+ const [showSnooze, setShowSnooze] = useState(false);
1309
+ const [snoozeUntil, setSnoozeUntil] = useState(null);
1310
+ const [snoozeSaving, setSnoozeSaving] = useState(false);
1311
+ const [showNextTicket, setShowNextTicket] = useState(false);
1312
+ const [nextTicketId, setNextTicketId] = useState(null);
1313
+ const [nextTicketInfo, setNextTicketInfo] = useState("");
1314
+ const handleStatusChange = async (newStatus) => {
1315
+ if (!id) return;
1316
+ setStatusUpdating(true);
1317
+ try {
1318
+ await fetch(`/api/tickets/${id}`, {
1319
+ method: "PATCH",
1320
+ headers: { "Content-Type": "application/json" },
1321
+ credentials: "include",
1322
+ body: JSON.stringify({ status: newStatus })
1323
+ });
1324
+ window.location.reload();
1325
+ } catch {
1326
+ } finally {
1327
+ setStatusUpdating(false);
1328
+ }
1329
+ };
1330
+ const handleMergeLookup = async () => {
1331
+ if (!mergeTarget.trim()) return;
1332
+ setMergeTargetInfo(null);
1333
+ setMergeError("");
1334
+ try {
1335
+ const searchVal = mergeTarget.trim().toUpperCase();
1336
+ const res = await fetch(`/api/tickets?where[ticketNumber][equals]=${encodeURIComponent(searchVal)}&limit=1&depth=0`, { credentials: "include" });
1337
+ if (res.ok) {
1338
+ const d = await res.json();
1339
+ if (d.docs?.length > 0) {
1340
+ const t = d.docs[0];
1341
+ if (String(t.id) === String(id)) {
1342
+ setMergeError("Impossible de fusionner un ticket avec lui-m\xEAme");
1343
+ } else {
1344
+ setMergeTargetInfo({ id: t.id, ticketNumber: t.ticketNumber, subject: t.subject });
1345
+ }
1346
+ } else {
1347
+ setMergeError("Ticket introuvable");
1348
+ }
1349
+ }
1350
+ } catch {
1351
+ setMergeError("Erreur de recherche");
1352
+ }
1353
+ };
1354
+ const handleMerge = async () => {
1355
+ if (!mergeTargetInfo || !id) return;
1356
+ setMerging(true);
1357
+ setMergeError("");
1358
+ try {
1359
+ const res = await fetch("/api/support/merge-tickets", {
1360
+ method: "POST",
1361
+ headers: { "Content-Type": "application/json" },
1362
+ credentials: "include",
1363
+ body: JSON.stringify({ sourceTicketId: id, targetTicketId: mergeTargetInfo.id })
1364
+ });
1365
+ if (res.ok) {
1366
+ window.location.href = `/admin/collections/tickets/${mergeTargetInfo.id}`;
1367
+ } else {
1368
+ const d = await res.json().catch(() => ({}));
1369
+ setMergeError(d.error || "Erreur de fusion");
1370
+ }
1371
+ } catch {
1372
+ setMergeError("Erreur r\xE9seau");
1373
+ } finally {
1374
+ setMerging(false);
1375
+ }
1376
+ };
1377
+ const handleExtFileChange = (e) => {
1378
+ if (e.target.files) {
1379
+ const maxSize = 1 * 1024 * 1024;
1380
+ const newFiles = Array.from(e.target.files);
1381
+ const tooLarge = newFiles.filter((f) => f.size > maxSize);
1382
+ if (tooLarge.length > 0) {
1383
+ alert(`Fichier(s) trop volumineux (max 1 Mo) : ${tooLarge.map((f) => f.name).join(", ")}`);
1384
+ }
1385
+ setExtMsgFiles((prev) => [...prev, ...newFiles.filter((f) => f.size <= maxSize)]);
1386
+ }
1387
+ if (extFileInputRef.current) extFileInputRef.current.value = "";
1388
+ };
1389
+ const handleSendExtMsg = async () => {
1390
+ if (!extMsgBody.trim() || !id) return;
1391
+ setSendingExtMsg(true);
1392
+ try {
1393
+ const uploadedIds = [];
1394
+ for (const file of extMsgFiles) {
1395
+ if (file.size > 1 * 1024 * 1024) continue;
1396
+ const formData = new FormData();
1397
+ formData.append("file", file);
1398
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
1399
+ const uploadRes = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
1400
+ if (uploadRes.ok) {
1401
+ const d = await uploadRes.json();
1402
+ if (d.doc?.id) uploadedIds.push(d.doc.id);
1403
+ }
1404
+ }
1405
+ const messageData = {
1406
+ ticket: id,
1407
+ body: extMsgBody.trim(),
1408
+ authorType: extMsgAuthor,
1409
+ isInternal: false,
1410
+ skipNotification: true
1411
+ };
1412
+ if (extMsgDate) {
1413
+ messageData.createdAt = new Date(extMsgDate).toISOString();
1414
+ }
1415
+ if (uploadedIds.length > 0) {
1416
+ messageData.attachments = uploadedIds.map((mid) => ({ file: mid }));
1417
+ }
1418
+ const res = await fetch("/api/ticket-messages", {
1419
+ method: "POST",
1420
+ headers: { "Content-Type": "application/json" },
1421
+ credentials: "include",
1422
+ body: JSON.stringify(messageData)
1423
+ });
1424
+ if (res.ok) {
1425
+ setExtMsgBody("");
1426
+ setExtMsgDate("");
1427
+ setExtMsgFiles([]);
1428
+ setShowExtMsg(false);
1429
+ fetchAll();
1430
+ }
1431
+ } catch {
1432
+ } finally {
1433
+ setSendingExtMsg(false);
1434
+ }
1435
+ };
1436
+ const handleSnooze = async (days, customDate) => {
1437
+ if (!id) return;
1438
+ setSnoozeSaving(true);
1439
+ try {
1440
+ let newSnooze = null;
1441
+ if (days !== null) {
1442
+ const d = /* @__PURE__ */ new Date();
1443
+ d.setDate(d.getDate() + days);
1444
+ newSnooze = d.toISOString();
1445
+ } else if (customDate) {
1446
+ newSnooze = new Date(customDate).toISOString();
1447
+ }
1448
+ await fetch(`/api/tickets/${id}`, {
1449
+ method: "PATCH",
1450
+ headers: { "Content-Type": "application/json" },
1451
+ credentials: "include",
1452
+ body: JSON.stringify({ snoozeUntil: newSnooze })
1453
+ });
1454
+ setSnoozeUntil(newSnooze);
1455
+ setShowSnooze(false);
1456
+ fetchAll();
1457
+ } catch {
1458
+ } finally {
1459
+ setSnoozeSaving(false);
1460
+ }
1461
+ };
1462
+ const handleNextTicket = async () => {
1463
+ try {
1464
+ const res = await fetch("/api/tickets?where[status][equals]=open&sort=updatedAt&limit=1&depth=0", { credentials: "include" });
1465
+ if (res.ok) {
1466
+ const d = await res.json();
1467
+ if (d.docs?.length > 0 && String(d.docs[0].id) !== String(id)) {
1468
+ setNextTicketId(d.docs[0].id);
1469
+ setNextTicketInfo(`${d.docs[0].ticketNumber} \u2014 ${d.docs[0].subject}`);
1470
+ } else {
1471
+ setNextTicketId(null);
1472
+ setNextTicketInfo("Aucun autre ticket ouvert");
1473
+ }
1474
+ setShowNextTicket(true);
1475
+ }
1476
+ } catch {
1477
+ }
1478
+ };
1479
+ return {
1480
+ statusUpdating,
1481
+ handleStatusChange,
1482
+ showMerge,
1483
+ setShowMerge,
1484
+ mergeTarget,
1485
+ setMergeTarget,
1486
+ mergeTargetInfo,
1487
+ setMergeTargetInfo,
1488
+ mergeError,
1489
+ setMergeError,
1490
+ merging,
1491
+ handleMergeLookup,
1492
+ handleMerge,
1493
+ showExtMsg,
1494
+ setShowExtMsg,
1495
+ extMsgBody,
1496
+ setExtMsgBody,
1497
+ extMsgAuthor,
1498
+ setExtMsgAuthor,
1499
+ extMsgDate,
1500
+ setExtMsgDate,
1501
+ extMsgFiles,
1502
+ setExtMsgFiles,
1503
+ sendingExtMsg,
1504
+ handleExtFileChange,
1505
+ handleSendExtMsg,
1506
+ showSnooze,
1507
+ setShowSnooze,
1508
+ snoozeUntil,
1509
+ setSnoozeUntil,
1510
+ snoozeSaving,
1511
+ handleSnooze,
1512
+ showNextTicket,
1513
+ setShowNextTicket,
1514
+ nextTicketId,
1515
+ nextTicketInfo,
1516
+ handleNextTicket
1517
+ };
1518
+ }
1519
+ function useReply(id, client, cannedResponses, ticketNumber, ticketSubject, fetchAll, handleNextTicket, replyEditorRef) {
1520
+ const fileInputRef = useRef(null);
1521
+ const [replyBody, setReplyBody] = useState("");
1522
+ const [replyHtml, setReplyHtml] = useState("");
1523
+ const [replyFiles, setReplyFiles] = useState([]);
1524
+ const [isInternal, setIsInternal] = useState(false);
1525
+ const [notifyClient, setNotifyClient] = useState(false);
1526
+ const [sendAsClient, setSendAsClient] = useState(false);
1527
+ const [sending, setSending] = useState(false);
1528
+ const [showSchedule, setShowSchedule] = useState(false);
1529
+ const [scheduleDate, setScheduleDate] = useState("");
1530
+ const handleEditorFileUpload = useCallback(async (file) => {
1531
+ if (file.size > 5 * 1024 * 1024) return null;
1532
+ const formData = new FormData();
1533
+ formData.append("file", file);
1534
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
1535
+ try {
1536
+ const res = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
1537
+ if (res.ok) {
1538
+ const data = await res.json();
1539
+ return data.doc?.url || null;
1540
+ }
1541
+ } catch {
1542
+ }
1543
+ return null;
1544
+ }, []);
1545
+ const replaceCannedVariables = useCallback((text) => {
1546
+ let result = text;
1547
+ if (client) {
1548
+ result = result.replace(/\{\{client\.firstName\}\}/g, client.firstName || "Client");
1549
+ result = result.replace(/\{\{client\.lastName\}\}/g, client.lastName || "");
1550
+ result = result.replace(/\{\{client\.company\}\}/g, client.company || "");
1551
+ result = result.replace(/\{\{client\.email\}\}/g, client.email || "");
1552
+ result = result.replace(/\{\{clientName\}\}/g, client.firstName || "Client");
1553
+ }
1554
+ result = result.replace(/\{\{ticket\.number\}\}/g, ticketNumber || "");
1555
+ result = result.replace(/\{\{ticket\.subject\}\}/g, ticketSubject || "");
1556
+ result = result.replace(/\{\{agent\.name\}\}/g, "Support");
1557
+ return result;
1558
+ }, [client, ticketNumber, ticketSubject]);
1559
+ const handleCannedSelect = (e) => {
1560
+ const selected = cannedResponses.find((cr) => String(cr.id) === e.target.value);
1561
+ if (selected) {
1562
+ const body = replaceCannedVariables(selected.body);
1563
+ const html = body.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br />");
1564
+ setReplyBody(body);
1565
+ setReplyHtml(html);
1566
+ replyEditorRef.current?.setContent(html);
1567
+ }
1568
+ e.target.value = "";
1569
+ };
1570
+ const handleReplyFileChange = (e) => {
1571
+ if (e.target.files) {
1572
+ const maxSize = 1 * 1024 * 1024;
1573
+ const newFiles = Array.from(e.target.files);
1574
+ const tooLarge = newFiles.filter((f) => f.size > maxSize);
1575
+ if (tooLarge.length > 0) {
1576
+ alert(`Fichier(s) trop volumineux (max 1 Mo) : ${tooLarge.map((f) => f.name).join(", ")}`);
1577
+ }
1578
+ setReplyFiles((prev) => [...prev, ...newFiles.filter((f) => f.size <= maxSize)]);
1579
+ }
1580
+ if (fileInputRef.current) fileInputRef.current.value = "";
1581
+ };
1582
+ const uploadFiles = async (files) => {
1583
+ const uploadedIds = [];
1584
+ for (const file of files) {
1585
+ if (file.size > 1 * 1024 * 1024) continue;
1586
+ const formData = new FormData();
1587
+ formData.append("file", file);
1588
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
1589
+ const uploadRes = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
1590
+ if (uploadRes.ok) {
1591
+ const d = await uploadRes.json();
1592
+ if (d.doc?.id) uploadedIds.push(d.doc.id);
1593
+ }
1594
+ }
1595
+ return uploadedIds;
1596
+ };
1597
+ const resetReply = () => {
1598
+ setReplyBody("");
1599
+ setReplyHtml("");
1600
+ setReplyFiles([]);
1601
+ setIsInternal(false);
1602
+ setNotifyClient(false);
1603
+ setSendAsClient(false);
1604
+ replyEditorRef.current?.clear();
1605
+ };
1606
+ const handleSendReply = async () => {
1607
+ if (!replyBody.trim() || !id) return;
1608
+ setSending(true);
1609
+ try {
1610
+ const uploadedIds = await uploadFiles(replyFiles);
1611
+ const finalBody = replyBody.trim() || (replyHtml ? "[Contenu enrichi]" : "");
1612
+ const messageData = {
1613
+ ticket: id,
1614
+ body: finalBody,
1615
+ ...replyHtml ? { bodyHtml: replyHtml } : {},
1616
+ authorType: sendAsClient ? "client" : "admin",
1617
+ isInternal,
1618
+ skipNotification: isInternal || !notifyClient,
1619
+ ...sendAsClient && client ? { authorClient: client.id } : {}
1620
+ };
1621
+ if (uploadedIds.length > 0) {
1622
+ messageData.attachments = uploadedIds.map((mid) => ({ file: mid }));
1623
+ }
1624
+ const res = await fetch("/api/ticket-messages", {
1625
+ method: "POST",
1626
+ headers: { "Content-Type": "application/json" },
1627
+ credentials: "include",
1628
+ body: JSON.stringify(messageData)
1629
+ });
1630
+ if (res.ok) {
1631
+ resetReply();
1632
+ fetchAll();
1633
+ if (!isInternal) handleNextTicket();
1634
+ }
1635
+ } catch {
1636
+ } finally {
1637
+ setSending(false);
1638
+ }
1639
+ };
1640
+ const handleScheduleReply = async () => {
1641
+ if (!replyBody.trim() || !id || !scheduleDate) return;
1642
+ setSending(true);
1643
+ try {
1644
+ const uploadedIds = await uploadFiles(replyFiles);
1645
+ const messageData = {
1646
+ ticket: id,
1647
+ body: replyBody.trim(),
1648
+ ...replyHtml ? { bodyHtml: replyHtml } : {},
1649
+ authorType: "admin",
1650
+ isInternal: false,
1651
+ skipNotification: true,
1652
+ scheduledAt: new Date(scheduleDate).toISOString(),
1653
+ scheduledSent: false
1654
+ };
1655
+ if (uploadedIds.length > 0) {
1656
+ messageData.attachments = uploadedIds.map((mid) => ({ file: mid }));
1657
+ }
1658
+ const res = await fetch("/api/ticket-messages", {
1659
+ method: "POST",
1660
+ headers: { "Content-Type": "application/json" },
1661
+ credentials: "include",
1662
+ body: JSON.stringify(messageData)
1663
+ });
1664
+ if (res.ok) {
1665
+ resetReply();
1666
+ setShowSchedule(false);
1667
+ setScheduleDate("");
1668
+ fetchAll();
1669
+ }
1670
+ } catch {
1671
+ } finally {
1672
+ setSending(false);
1673
+ }
1674
+ };
1675
+ return {
1676
+ fileInputRef,
1677
+ replyBody,
1678
+ setReplyBody,
1679
+ replyHtml,
1680
+ setReplyHtml,
1681
+ replyFiles,
1682
+ setReplyFiles,
1683
+ isInternal,
1684
+ setIsInternal,
1685
+ notifyClient,
1686
+ setNotifyClient,
1687
+ sendAsClient,
1688
+ setSendAsClient,
1689
+ sending,
1690
+ showSchedule,
1691
+ setShowSchedule,
1692
+ scheduleDate,
1693
+ setScheduleDate,
1694
+ handleEditorFileUpload,
1695
+ handleCannedSelect,
1696
+ handleReplyFileChange,
1697
+ handleSendReply,
1698
+ handleScheduleReply
1699
+ };
1700
+ }
1701
+ function useAI(messages, client, ticketSubject, replyBody, setReplyBody, setReplyHtml, replyEditorRef) {
1702
+ const [clientSentiment, setClientSentiment] = useState(null);
1703
+ const [aiReplying, setAiReplying] = useState(false);
1704
+ const [aiRewriting, setAiRewriting] = useState(false);
1705
+ const [showAiSummary, setShowAiSummary] = useState(false);
1706
+ const [aiSummary, setAiSummary] = useState("");
1707
+ const [aiGenerating, setAiGenerating] = useState(false);
1708
+ const [aiSaving, setAiSaving] = useState(false);
1709
+ const [aiSaved, setAiSaved] = useState(false);
1710
+ useEffect(() => {
1711
+ if (messages.length === 0) return;
1712
+ const lastClientMsg = [...messages].reverse().find((m) => m.authorType === "client" || m.authorType === "email");
1713
+ if (!lastClientMsg) return;
1714
+ const analyze = async () => {
1715
+ try {
1716
+ const res = await fetch("/api/support/ai", {
1717
+ method: "POST",
1718
+ headers: { "Content-Type": "application/json" },
1719
+ credentials: "include",
1720
+ body: JSON.stringify({ action: "sentiment", text: lastClientMsg.body.slice(0, 500) })
1721
+ });
1722
+ if (res.ok) {
1723
+ const data = await res.json();
1724
+ const raw = (data.sentiment || "").toLowerCase().trim();
1725
+ const sentimentMap = {
1726
+ "frustr\xE9": { emoji: "\u{1F624}", label: "Frustr\xE9", color: "#dc2626" },
1727
+ "frustre": { emoji: "\u{1F624}", label: "Frustr\xE9", color: "#dc2626" },
1728
+ "m\xE9content": { emoji: "\u{1F620}", label: "M\xE9content", color: "#ea580c" },
1729
+ "mecontent": { emoji: "\u{1F620}", label: "M\xE9content", color: "#ea580c" },
1730
+ "urgent": { emoji: "\u{1F525}", label: "Urgent", color: "#dc2626" },
1731
+ "neutre": { emoji: "\u{1F610}", label: "Neutre", color: "#6b7280" },
1732
+ "satisfait": { emoji: "\u{1F60A}", label: "Satisfait", color: "#16a34a" }
1733
+ };
1734
+ const match = Object.keys(sentimentMap).find((k) => raw.includes(k));
1735
+ if (match) setClientSentiment(sentimentMap[match]);
1736
+ else setClientSentiment({ emoji: "\u{1F610}", label: "Neutre", color: "#6b7280" });
1737
+ }
1738
+ } catch {
1739
+ }
1740
+ };
1741
+ analyze();
1742
+ }, [messages.length]);
1743
+ const handleAiSuggestReply = async () => {
1744
+ if (messages.length === 0) return;
1745
+ setAiReplying(true);
1746
+ try {
1747
+ const res = await fetch("/api/support/ai", {
1748
+ method: "POST",
1749
+ headers: { "Content-Type": "application/json" },
1750
+ credentials: "include",
1751
+ body: JSON.stringify({
1752
+ action: "suggest_reply",
1753
+ messages: messages.slice(-10).map((m) => ({ authorType: m.authorType, body: m.body })),
1754
+ clientName: `${client?.firstName || ""} ${client?.lastName || ""}`.trim(),
1755
+ clientCompany: client?.company
1756
+ })
1757
+ });
1758
+ if (res.ok) {
1759
+ const data = await res.json();
1760
+ const suggestion = data.reply || "";
1761
+ if (suggestion) {
1762
+ setReplyBody(suggestion);
1763
+ setReplyHtml(suggestion.replace(/\n/g, "<br/>"));
1764
+ if (replyEditorRef.current?.setContent) {
1765
+ replyEditorRef.current.setContent(suggestion.replace(/\n/g, "<br/>"));
1766
+ }
1767
+ }
1768
+ }
1769
+ } catch (err) {
1770
+ console.error("AI suggest error:", err);
1771
+ }
1772
+ setAiReplying(false);
1773
+ };
1774
+ const handleAiRewrite = async () => {
1775
+ if (!replyBody.trim()) return;
1776
+ setAiRewriting(true);
1777
+ try {
1778
+ const res = await fetch("/api/support/ai", {
1779
+ method: "POST",
1780
+ headers: { "Content-Type": "application/json" },
1781
+ credentials: "include",
1782
+ body: JSON.stringify({ action: "rewrite", text: replyBody })
1783
+ });
1784
+ if (res.ok) {
1785
+ const data = await res.json();
1786
+ const rewritten = data.rewritten || "";
1787
+ if (rewritten) {
1788
+ setReplyBody(rewritten);
1789
+ setReplyHtml(rewritten.replace(/\n/g, "<br/>"));
1790
+ if (replyEditorRef.current?.setContent) {
1791
+ replyEditorRef.current.setContent(rewritten.replace(/\n/g, "<br/>"));
1792
+ }
1793
+ }
1794
+ }
1795
+ } catch (err) {
1796
+ console.error("AI rewrite error:", err);
1797
+ }
1798
+ setAiRewriting(false);
1799
+ };
1800
+ const handleAiGenerate = async () => {
1801
+ if (messages.length === 0) return;
1802
+ setAiGenerating(true);
1803
+ setAiSummary("");
1804
+ setAiSaved(false);
1805
+ try {
1806
+ const res = await fetch("/api/support/ai", {
1807
+ method: "POST",
1808
+ headers: { "Content-Type": "application/json" },
1809
+ credentials: "include",
1810
+ body: JSON.stringify({
1811
+ action: "synthesis",
1812
+ messages: messages.map((m) => ({ authorType: m.authorType, body: m.body, createdAt: m.createdAt })),
1813
+ ticketSubject,
1814
+ clientName: `${client?.firstName || ""} ${client?.lastName || ""}`.trim(),
1815
+ clientCompany: client?.company
1816
+ })
1817
+ });
1818
+ if (res.ok) {
1819
+ const data = await res.json();
1820
+ setAiSummary(data.synthesis || "Aucune r\xE9ponse g\xE9n\xE9r\xE9e");
1821
+ } else {
1822
+ setAiSummary("Erreur lors de la g\xE9n\xE9ration de la synth\xE8se.");
1823
+ }
1824
+ } catch (err) {
1825
+ setAiSummary(`Erreur de connexion : ${err instanceof Error ? err.message : "erreur inconnue"}`);
1826
+ } finally {
1827
+ setAiGenerating(false);
1828
+ }
1829
+ };
1830
+ const handleAiSave = async (id, fetchAll) => {
1831
+ if (!id || !aiSummary) return;
1832
+ setAiSaving(true);
1833
+ try {
1834
+ const res = await fetch("/api/ticket-messages", {
1835
+ method: "POST",
1836
+ headers: { "Content-Type": "application/json" },
1837
+ credentials: "include",
1838
+ body: JSON.stringify({
1839
+ ticket: id,
1840
+ body: `\u{1F4CB} **Synth\xE8se IA (${(/* @__PURE__ */ new Date()).toLocaleDateString("fr-FR")})**
1841
+
1842
+ ${aiSummary}`,
1843
+ authorType: "admin",
1844
+ isInternal: true
1845
+ })
1846
+ });
1847
+ if (res.ok) {
1848
+ setAiSaved(true);
1849
+ fetchAll();
1850
+ }
1851
+ } catch {
1852
+ } finally {
1853
+ setAiSaving(false);
1854
+ }
1855
+ };
1856
+ return {
1857
+ clientSentiment,
1858
+ aiReplying,
1859
+ handleAiSuggestReply,
1860
+ aiRewriting,
1861
+ handleAiRewrite,
1862
+ showAiSummary,
1863
+ setShowAiSummary,
1864
+ aiSummary,
1865
+ aiGenerating,
1866
+ handleAiGenerate,
1867
+ aiSaving,
1868
+ aiSaved,
1869
+ handleAiSave
1870
+ };
1871
+ }
1872
+
1873
+ // src/components/TicketConversation/config.ts
1874
+ var DEFAULT_FEATURES = {
1875
+ timeTracking: true,
1876
+ ai: true,
1877
+ satisfaction: true,
1878
+ chat: true,
1879
+ emailTracking: true,
1880
+ canned: true,
1881
+ merge: true,
1882
+ snooze: true,
1883
+ externalMessages: true,
1884
+ clientHistory: true,
1885
+ activityLog: true,
1886
+ splitTicket: true,
1887
+ scheduledReplies: true,
1888
+ autoClose: true,
1889
+ autoCloseDays: 7,
1890
+ roundRobin: false
1891
+ };
1892
+ var STORAGE_KEY = "ticketing_features";
1893
+ function getFeatures() {
1894
+ if (typeof window === "undefined") return DEFAULT_FEATURES;
1895
+ try {
1896
+ const stored = localStorage.getItem(STORAGE_KEY);
1897
+ if (stored) {
1898
+ const parsed = JSON.parse(stored);
1899
+ return { ...DEFAULT_FEATURES, ...parsed };
1900
+ }
1901
+ } catch {
1902
+ }
1903
+ return DEFAULT_FEATURES;
1904
+ }
1905
+ function useDocumentIdFromUrl() {
1906
+ const [id, setId] = useState(void 0);
1907
+ useEffect(() => {
1908
+ const match = window.location.pathname.match(
1909
+ /\/admin\/collections\/[^/]+\/([^/?#]+)/
1910
+ );
1911
+ if (match && match[1] !== "create") {
1912
+ const raw = match[1];
1913
+ const num = Number(raw);
1914
+ setId(Number.isFinite(num) && String(num) === raw ? num : raw);
1915
+ }
1916
+ }, []);
1917
+ return { id };
1918
+ }
1919
+ function SkeletonText({ lines = 3 }) {
1920
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: "8px", padding: "12px 0" }, children: Array.from({ length: lines }).map((_, i) => /* @__PURE__ */ jsx(
1921
+ "div",
1922
+ {
1923
+ style: {
1924
+ height: "14px",
1925
+ borderRadius: "4px",
1926
+ backgroundColor: "#e2e8f0",
1927
+ width: i === lines - 1 ? "60%" : "100%",
1928
+ animation: "pulse 1.5s ease-in-out infinite"
1929
+ }
1930
+ },
1931
+ i
1932
+ )) });
1933
+ }
1934
+ var layoutStyles = {
1935
+ root: { padding: "12px 0" },
1936
+ twoColumns: {
1937
+ display: "grid",
1938
+ gridTemplateColumns: "1fr 320px",
1939
+ gap: "16px",
1940
+ alignItems: "start"
1941
+ },
1942
+ mainColumn: { minWidth: 0 },
1943
+ sideColumn: {
1944
+ display: "flex",
1945
+ flexDirection: "column",
1946
+ gap: "12px",
1947
+ position: "sticky",
1948
+ top: "80px"
1949
+ }
1950
+ };
1951
+ var TicketConversation = () => {
1952
+ const { id } = useDocumentIdFromUrl();
1953
+ const [features] = useState(() => getFeatures());
1954
+ const [messages, setMessages] = useState([]);
1955
+ const [timeEntries, setTimeEntries] = useState([]);
1956
+ const [client, setClient] = useState(null);
1957
+ const [cannedResponses, setCannedResponses] = useState([]);
1958
+ const [activityLog, setActivityLog] = useState([]);
1959
+ const [satisfaction, setSatisfaction] = useState(null);
1960
+ const [loading, setLoading] = useState(true);
1961
+ const [messagesCollapsed, setMessagesCollapsed] = useState(true);
1962
+ const [searchQuery, setSearchQuery] = useState("");
1963
+ const [copiedLink, setCopiedLink] = useState(null);
1964
+ const prevMessageCountRef = useRef(0);
1965
+ const [clientTyping, setClientTyping] = useState(false);
1966
+ const [clientTypingName, setClientTypingName] = useState("");
1967
+ const typingLastSent = useRef(0);
1968
+ const [currentStatus, setCurrentStatus] = useState("");
1969
+ const [ticketNumber, setTicketNumber] = useState("");
1970
+ const [ticketSubject, setTicketSubject] = useState("");
1971
+ const [ticketSource, setTicketSource] = useState("");
1972
+ const [chatSession, setChatSession] = useState("");
1973
+ const [clientTickets, setClientTickets] = useState([]);
1974
+ const [clientProjects, setClientProjects] = useState([]);
1975
+ const [clientNotes, setClientNotes] = useState("");
1976
+ const [savingNotes, setSavingNotes] = useState(false);
1977
+ const [notesSaved, setNotesSaved] = useState(false);
1978
+ const [lastClientReadAt, setLastClientReadAt] = useState(null);
1979
+ const fetchAll = useCallback(async () => {
1980
+ if (!id) return;
1981
+ try {
1982
+ const [msgRes, timeRes, ticketRes, cannedRes, activityRes, csatRes] = await Promise.all([
1983
+ fetch(`/api/ticket-messages?where[ticket][equals]=${id}&sort=createdAt&limit=200&depth=1`, { credentials: "include" }),
1984
+ fetch(`/api/time-entries?where[ticket][equals]=${id}&sort=-date&limit=50&depth=0`, { credentials: "include" }),
1985
+ fetch(`/api/tickets/${id}?depth=1`, { credentials: "include" }),
1986
+ fetch(`/api/canned-responses?sort=sortOrder&limit=50&depth=0`, { credentials: "include" }),
1987
+ fetch(`/api/ticket-activity-log?where[ticket][equals]=${id}&sort=-createdAt&limit=50&depth=0`, { credentials: "include" }),
1988
+ fetch(`/api/satisfaction-surveys?where[ticket][equals]=${id}&limit=1&depth=0`, { credentials: "include" })
1989
+ ]);
1990
+ if (msgRes.ok) {
1991
+ const d = await msgRes.json();
1992
+ setMessages(d.docs || []);
1993
+ }
1994
+ if (timeRes.ok) {
1995
+ const d = await timeRes.json();
1996
+ setTimeEntries(d.docs || []);
1997
+ }
1998
+ let resolvedChatSession = "";
1999
+ if (ticketRes.ok) {
2000
+ const d = await ticketRes.json();
2001
+ if (d.client && typeof d.client === "object") {
2002
+ setClient(d.client);
2003
+ }
2004
+ setSnoozeUntil(d.snoozeUntil || null);
2005
+ setLastClientReadAt(d.lastClientReadAt || null);
2006
+ setCurrentStatus(d.status || "");
2007
+ setTicketNumber(d.ticketNumber || "");
2008
+ setTicketSubject(d.subject || "");
2009
+ setTicketSource(d.source || "");
2010
+ setChatSession(d.chatSession || "");
2011
+ resolvedChatSession = d.chatSession || "";
2012
+ const clientId = typeof d.client === "object" ? d.client?.id : d.client;
2013
+ if (clientId) {
2014
+ const [clientTicketsRes, projectsRes, clientDetailRes] = await Promise.all([
2015
+ fetch(`/api/tickets?where[client][equals]=${clientId}&where[id][not_equals]=${id}&sort=-createdAt&limit=5&depth=0`, { credentials: "include" }),
2016
+ fetch(`/api/projects?where[client][contains]=${clientId}&depth=0`, { credentials: "include" }),
2017
+ fetch(`/api/support-clients/${clientId}?depth=0`, { credentials: "include" })
2018
+ ]);
2019
+ if (clientTicketsRes.ok) {
2020
+ const ctData = await clientTicketsRes.json();
2021
+ setClientTickets((ctData.docs || []).map((t) => ({
2022
+ id: t.id,
2023
+ ticketNumber: t.ticketNumber,
2024
+ subject: t.subject,
2025
+ status: t.status,
2026
+ createdAt: t.createdAt
2027
+ })));
2028
+ }
2029
+ if (projectsRes.ok) {
2030
+ const pData = await projectsRes.json();
2031
+ setClientProjects((pData.docs || []).map((p) => ({
2032
+ id: p.id,
2033
+ name: p.name,
2034
+ status: p.status
2035
+ })));
2036
+ }
2037
+ if (clientDetailRes.ok) {
2038
+ const cdData = await clientDetailRes.json();
2039
+ setClientNotes(cdData.notes || "");
2040
+ }
2041
+ }
2042
+ }
2043
+ if (cannedRes.ok) {
2044
+ const d = await cannedRes.json();
2045
+ setCannedResponses(d.docs || []);
2046
+ }
2047
+ if (activityRes.ok) {
2048
+ const d = await activityRes.json();
2049
+ setActivityLog(d.docs || []);
2050
+ }
2051
+ if (csatRes.ok) {
2052
+ const d = await csatRes.json();
2053
+ setSatisfaction(d.docs?.[0] || null);
2054
+ }
2055
+ if (resolvedChatSession) {
2056
+ try {
2057
+ const chatRes = await fetch(`/api/support/admin-chat?session=${encodeURIComponent(resolvedChatSession)}`, { credentials: "include" });
2058
+ if (chatRes.ok) {
2059
+ const chatData = await chatRes.json();
2060
+ const chatMsgs = (chatData.messages || []).filter((cm) => cm.senderType !== "system").map((cm) => ({
2061
+ id: `chat-${cm.id}`,
2062
+ body: cm.message,
2063
+ authorType: cm.senderType === "agent" ? "admin" : "client",
2064
+ isInternal: false,
2065
+ createdAt: cm.createdAt,
2066
+ fromChat: true
2067
+ }));
2068
+ setMessages((prev) => {
2069
+ const ticketMsgs = prev;
2070
+ const merged = [...ticketMsgs];
2071
+ for (const chatMsg of chatMsgs) {
2072
+ const isDuplicate = ticketMsgs.some((tm) => {
2073
+ if (tm.body !== chatMsg.body) return false;
2074
+ const timeDiff = Math.abs(new Date(tm.createdAt).getTime() - new Date(chatMsg.createdAt).getTime());
2075
+ return timeDiff < 5e3;
2076
+ });
2077
+ if (!isDuplicate) {
2078
+ merged.push(chatMsg);
2079
+ }
2080
+ }
2081
+ return merged.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
2082
+ });
2083
+ }
2084
+ } catch (err) {
2085
+ console.warn("[TicketConversation] Chat fetch error:", err);
2086
+ }
2087
+ }
2088
+ } catch (err) {
2089
+ console.warn("[TicketConversation] Fetch error:", err);
2090
+ } finally {
2091
+ setLoading(false);
2092
+ }
2093
+ }, [id]);
2094
+ useEffect(() => {
2095
+ fetchAll();
2096
+ }, [fetchAll]);
2097
+ useEffect(() => {
2098
+ if (!id) return;
2099
+ fetch(`/api/tickets/${id}`, {
2100
+ method: "PATCH",
2101
+ headers: { "Content-Type": "application/json" },
2102
+ credentials: "include",
2103
+ body: JSON.stringify({ lastAdminReadAt: (/* @__PURE__ */ new Date()).toISOString() })
2104
+ }).catch(() => {
2105
+ });
2106
+ }, [id, messages.length]);
2107
+ const typingFailCount = useRef(0);
2108
+ useEffect(() => {
2109
+ if (!id) return;
2110
+ typingFailCount.current = 0;
2111
+ const poll = async () => {
2112
+ if (typingFailCount.current >= 3) return;
2113
+ try {
2114
+ const res = await fetch(`/api/support/typing?ticketId=${id}`, { credentials: "include" });
2115
+ if (res.ok) {
2116
+ typingFailCount.current = 0;
2117
+ const data = await res.json();
2118
+ setClientTyping(data.typing);
2119
+ setClientTypingName(data.name || "");
2120
+ } else {
2121
+ typingFailCount.current++;
2122
+ }
2123
+ } catch {
2124
+ typingFailCount.current++;
2125
+ }
2126
+ };
2127
+ poll();
2128
+ const interval = setInterval(poll, 3e3);
2129
+ return () => clearInterval(interval);
2130
+ }, [id]);
2131
+ const sendAdminTyping = useCallback(() => {
2132
+ if (!id) return;
2133
+ const now = Date.now();
2134
+ if (now - typingLastSent.current < 3e3) return;
2135
+ typingLastSent.current = now;
2136
+ fetch("/api/support/typing", {
2137
+ method: "POST",
2138
+ headers: { "Content-Type": "application/json" },
2139
+ credentials: "include",
2140
+ body: JSON.stringify({ ticketId: id })
2141
+ }).catch(() => {
2142
+ });
2143
+ }, [id]);
2144
+ const tt = useTimeTracking(id, fetchAll);
2145
+ const { duration, setDuration, timeDescription, setTimeDescription, addingTime, timeSuccess, timerRunning, timerSeconds, setTimerSeconds, timerDescription, setTimerDescription, handleAddTime, handleTimerStart, handleTimerStop, handleTimerSave, handleTimerDiscard } = tt;
2146
+ const ma = useMessageActions(id, client, fetchAll);
2147
+ const { togglingAuthor, editingMsg, editBody, editHtml, setEditHtml, savingEdit, handleEditStart, handleEditSave, handleEditCancel, deletingMsg, handleDelete, resendingMsg, resendSuccess, handleResend, handleToggleAuthor, handleSplitMessage, setEditBody } = ma;
2148
+ const ta = useTicketActions(id, fetchAll);
2149
+ const { statusUpdating, handleStatusChange, showMerge, setShowMerge, mergeTarget, setMergeTarget, mergeTargetInfo, setMergeTargetInfo, mergeError, setMergeError, merging, handleMergeLookup, handleMerge, showExtMsg, setShowExtMsg, extMsgBody, setExtMsgBody, extMsgAuthor, setExtMsgAuthor, extMsgDate, setExtMsgDate, extMsgFiles, setExtMsgFiles, sendingExtMsg, handleExtFileChange, handleSendExtMsg, showSnooze, setShowSnooze, snoozeUntil, setSnoozeUntil, snoozeSaving, handleSnooze, showNextTicket, setShowNextTicket, nextTicketId, nextTicketInfo, handleNextTicket } = ta;
2150
+ const replyEditorRef = useRef(null);
2151
+ const rp = useReply(id, client, cannedResponses, ticketNumber, ticketSubject, fetchAll, handleNextTicket, replyEditorRef);
2152
+ const { fileInputRef, replyBody, setReplyBody, replyHtml, setReplyHtml, replyFiles, setReplyFiles, isInternal, setIsInternal, notifyClient, setNotifyClient, sendAsClient, setSendAsClient, sending, showSchedule, setShowSchedule, scheduleDate, setScheduleDate, handleCannedSelect, handleReplyFileChange, handleSendReply, handleScheduleReply } = rp;
2153
+ const ai = useAI(messages, client, ticketSubject, replyBody, setReplyBody, setReplyHtml, replyEditorRef);
2154
+ const { clientSentiment, aiReplying, handleAiSuggestReply, aiRewriting, handleAiRewrite, showAiSummary, setShowAiSummary, aiSummary, aiGenerating, handleAiGenerate, aiSaving, aiSaved, handleAiSave: aiSaveRaw } = ai;
2155
+ const handleAiSave = () => aiSaveRaw(id, fetchAll);
2156
+ const [pollExpired, setPollExpired] = useState(false);
2157
+ useEffect(() => {
2158
+ if (!id || loading || pollExpired) return;
2159
+ const poll = async () => {
2160
+ try {
2161
+ const [msgRes, ticketRes, activityRes] = await Promise.all([
2162
+ fetch(`/api/ticket-messages?where[ticket][equals]=${id}&sort=createdAt&limit=200&depth=1`, { credentials: "include" }),
2163
+ fetch(`/api/tickets/${id}?depth=0`, { credentials: "include" }),
2164
+ fetch(`/api/ticket-activity-log?where[ticket][equals]=${id}&sort=-createdAt&limit=50&depth=0`, { credentials: "include" })
2165
+ ]);
2166
+ if (msgRes.status === 401 || msgRes.status === 403) {
2167
+ setPollExpired(true);
2168
+ return;
2169
+ }
2170
+ if (msgRes.ok) {
2171
+ const d = await msgRes.json();
2172
+ setMessages(d.docs || []);
2173
+ }
2174
+ if (ticketRes.ok) {
2175
+ const d = await ticketRes.json();
2176
+ setCurrentStatus(d.status || "");
2177
+ setSnoozeUntil(d.snoozeUntil || null);
2178
+ setLastClientReadAt(d.lastClientReadAt || null);
2179
+ }
2180
+ if (activityRes.ok) {
2181
+ const d = await activityRes.json();
2182
+ setActivityLog(d.docs || []);
2183
+ }
2184
+ } catch {
2185
+ }
2186
+ };
2187
+ const interval = setInterval(poll, 15e3);
2188
+ return () => clearInterval(interval);
2189
+ }, [id, loading, pollExpired]);
2190
+ useEffect(() => {
2191
+ const handleKeyDown = (e) => {
2192
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && !e.shiftKey) {
2193
+ e.preventDefault();
2194
+ const sendBtn = document.querySelector('[data-action="send-reply"]');
2195
+ if (sendBtn && !sendBtn.disabled) sendBtn.click();
2196
+ }
2197
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "N" || e.key === "n")) {
2198
+ e.preventDefault();
2199
+ setIsInternal((prev) => !prev);
2200
+ }
2201
+ };
2202
+ window.addEventListener("keydown", handleKeyDown);
2203
+ return () => window.removeEventListener("keydown", handleKeyDown);
2204
+ }, []);
2205
+ const playNotificationSound = useCallback(() => {
2206
+ try {
2207
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
2208
+ const osc = ctx.createOscillator();
2209
+ const gain = ctx.createGain();
2210
+ osc.connect(gain);
2211
+ gain.connect(ctx.destination);
2212
+ osc.frequency.value = 800;
2213
+ osc.type = "sine";
2214
+ gain.gain.value = 0.1;
2215
+ osc.start();
2216
+ gain.gain.exponentialRampToValueAtTime(1e-3, ctx.currentTime + 0.3);
2217
+ osc.stop(ctx.currentTime + 0.3);
2218
+ } catch {
2219
+ }
2220
+ }, []);
2221
+ useEffect(() => {
2222
+ const currentCount = messages.length;
2223
+ if (prevMessageCountRef.current > 0 && currentCount > prevMessageCountRef.current) {
2224
+ const lastMsg = messages[messages.length - 1];
2225
+ if (lastMsg && lastMsg.authorType !== "admin") {
2226
+ playNotificationSound();
2227
+ }
2228
+ }
2229
+ prevMessageCountRef.current = currentCount;
2230
+ }, [messages, playNotificationSound]);
2231
+ const handleCopyLink = (type) => {
2232
+ const url = type === "admin" ? `${window.location.origin}/admin/collections/tickets/${id}` : `${window.location.origin}/support/tickets/${id}`;
2233
+ navigator.clipboard.writeText(url);
2234
+ setCopiedLink(type);
2235
+ setTimeout(() => setCopiedLink(null), 2e3);
2236
+ };
2237
+ if (!id) {
2238
+ return /* @__PURE__ */ jsx("div", { style: { padding: "16px", color: "#666", fontStyle: "italic" }, children: "Enregistrez le ticket pour voir le tableau de bord." });
2239
+ }
2240
+ const totalMinutes = timeEntries.reduce((sum, e) => sum + (e.duration || 0), 0);
2241
+ const statusTransitions = (() => {
2242
+ switch (currentStatus) {
2243
+ case "open":
2244
+ return [
2245
+ { status: "waiting_client", label: "Attente client", color: C.statusWaiting },
2246
+ { status: "resolved", label: "R\xE9solu", color: C.statusResolved }
2247
+ ];
2248
+ case "waiting_client":
2249
+ return [
2250
+ { status: "open", label: "Ouvrir", color: C.statusOpen },
2251
+ { status: "resolved", label: "R\xE9solu", color: C.statusResolved }
2252
+ ];
2253
+ case "resolved":
2254
+ return [
2255
+ { status: "open", label: "Rouvrir", color: C.statusOpen }
2256
+ ];
2257
+ default:
2258
+ return [
2259
+ { status: "open", label: "Ouvrir", color: C.statusOpen },
2260
+ { status: "waiting_client", label: "Attente client", color: C.statusWaiting },
2261
+ { status: "resolved", label: "R\xE9solu", color: C.statusResolved }
2262
+ ];
2263
+ }
2264
+ })();
2265
+ return /* @__PURE__ */ jsxs("div", { style: layoutStyles.root, children: [
2266
+ /* @__PURE__ */ jsx(
2267
+ TicketHeader,
2268
+ {
2269
+ ticketNumber,
2270
+ currentStatus,
2271
+ clientSentiment,
2272
+ ticketSource,
2273
+ chatSession,
2274
+ snoozeUntil,
2275
+ satisfaction,
2276
+ copiedLink,
2277
+ onCopyLink: handleCopyLink
2278
+ }
2279
+ ),
2280
+ client && /* @__PURE__ */ jsx(ClientBar, { client }),
2281
+ /* @__PURE__ */ jsxs("div", { style: layoutStyles.twoColumns, children: [
2282
+ /* @__PURE__ */ jsxs("div", { style: layoutStyles.mainColumn, children: [
2283
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "4px", display: "flex", alignItems: "center", gap: "10px" }, children: [
2284
+ /* @__PURE__ */ jsxs("h3", { style: { fontSize: "14px", fontWeight: 600, margin: 0, display: "flex", alignItems: "center", gap: "8px" }, children: [
2285
+ "Conversation ",
2286
+ /* @__PURE__ */ jsx("span", { style: s.badge("#f1f5f9", "#475569"), children: messages.length })
2287
+ ] }),
2288
+ /* @__PURE__ */ jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
2289
+ "input",
2290
+ {
2291
+ type: "text",
2292
+ value: searchQuery,
2293
+ onChange: (e) => setSearchQuery(e.target.value),
2294
+ placeholder: "Rechercher...",
2295
+ style: { ...s.input, width: "100%", fontSize: "12px", padding: "6px 10px" }
2296
+ }
2297
+ ) })
2298
+ ] }),
2299
+ loading ? /* @__PURE__ */ jsx(SkeletonText, { lines: 4 }) : messages.length === 0 ? /* @__PURE__ */ jsx("p", { style: { color: "#999", fontStyle: "italic", padding: "12px 0" }, children: "Aucun message." }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: "10px", marginBottom: "16px", paddingRight: "4px" }, children: (() => {
2300
+ const filtered = messages.filter((msg) => !searchQuery.trim() || msg.body.toLowerCase().includes(searchQuery.toLowerCase()));
2301
+ const isSearching = searchQuery.trim().length > 0;
2302
+ const VISIBLE_COUNT = 3;
2303
+ const showCollapse = !isSearching && messagesCollapsed && filtered.length > VISIBLE_COUNT;
2304
+ const visibleMessages = showCollapse ? filtered.slice(-VISIBLE_COUNT) : filtered;
2305
+ const hiddenCount = filtered.length - VISIBLE_COUNT;
2306
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2307
+ showCollapse && hiddenCount > 0 && /* @__PURE__ */ jsxs(
2308
+ "button",
2309
+ {
2310
+ onClick: () => setMessagesCollapsed(false),
2311
+ style: {
2312
+ background: "none",
2313
+ border: `1px dashed ${C.border}`,
2314
+ borderRadius: "6px",
2315
+ padding: "8px",
2316
+ cursor: "pointer",
2317
+ color: C.textMuted,
2318
+ fontSize: "12px",
2319
+ fontWeight: 600,
2320
+ textAlign: "center"
2321
+ },
2322
+ children: [
2323
+ "Voir les ",
2324
+ hiddenCount,
2325
+ " message",
2326
+ hiddenCount > 1 ? "s" : "",
2327
+ " pr\xE9c\xE9dent",
2328
+ hiddenCount > 1 ? "s" : ""
2329
+ ]
2330
+ }
2331
+ ),
2332
+ !messagesCollapsed && filtered.length > 1 && !isSearching && /* @__PURE__ */ jsx(
2333
+ "button",
2334
+ {
2335
+ onClick: () => setMessagesCollapsed(true),
2336
+ style: {
2337
+ background: "none",
2338
+ border: `1px dashed ${C.border}`,
2339
+ borderRadius: "6px",
2340
+ padding: "8px",
2341
+ cursor: "pointer",
2342
+ color: C.textMuted,
2343
+ fontSize: "12px",
2344
+ fontWeight: 600,
2345
+ textAlign: "center"
2346
+ },
2347
+ children: "Masquer les anciens messages"
2348
+ }
2349
+ ),
2350
+ visibleMessages.map((msg, msgIdx) => {
2351
+ const borderColor = msg.isInternal ? C.internalBorder : msg.fromChat ? "#bae6fd" : msg.authorType === "admin" ? "#bfdbfe" : msg.authorType === "email" ? "#fed7aa" : C.clientBorder;
2352
+ const bgColor = msg.isInternal ? C.internalBg : msg.fromChat ? "#f0f9ff" : msg.authorType === "admin" ? C.adminBg : msg.authorType === "email" ? C.emailBg : C.clientBg;
2353
+ const prevVisMsg = msgIdx > 0 ? visibleMessages[msgIdx - 1] : null;
2354
+ const showDateSep = msg.createdAt && (!prevVisMsg?.createdAt || new Date(msg.createdAt).toDateString() !== new Date(prevVisMsg.createdAt).toDateString());
2355
+ return /* @__PURE__ */ jsxs(React6.Fragment, { children: [
2356
+ showDateSep && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "12px", padding: "4px 0" }, children: [
2357
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, borderTop: `1px solid ${C.border}` } }),
2358
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "11px", fontWeight: 600, color: C.textMuted, whiteSpace: "nowrap" }, children: getDateLabel(msg.createdAt) }),
2359
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, borderTop: `1px solid ${C.border}` } })
2360
+ ] }),
2361
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "flex-start" }, children: [
2362
+ /* @__PURE__ */ jsx("div", { style: {
2363
+ flexShrink: 0,
2364
+ width: "28px",
2365
+ height: "28px",
2366
+ borderRadius: "50%",
2367
+ display: "flex",
2368
+ alignItems: "center",
2369
+ justifyContent: "center",
2370
+ fontSize: "10px",
2371
+ fontWeight: 700,
2372
+ color: "#fff",
2373
+ marginTop: "2px",
2374
+ backgroundColor: msg.authorType === "admin" ? C.blue : msg.authorType === "email" ? C.orange : "#94a3b8"
2375
+ }, children: msg.authorType === "admin" ? "CW" : client ? `${(client.firstName?.[0] || "").toUpperCase()}${(client.lastName?.[0] || "").toUpperCase()}` || "?" : "?" }),
2376
+ /* @__PURE__ */ jsxs(
2377
+ "div",
2378
+ {
2379
+ style: {
2380
+ flex: 1,
2381
+ padding: "10px 14px",
2382
+ borderRadius: "8px",
2383
+ border: msg.isInternal ? `1px dashed ${C.internalBorder}` : `1px solid ${borderColor}`,
2384
+ backgroundColor: bgColor
2385
+ },
2386
+ children: [
2387
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: "4px", fontSize: "12px", alignItems: "center" }, children: [
2388
+ /* @__PURE__ */ jsxs("span", { style: { fontWeight: 600, display: "inline-flex", alignItems: "center", gap: "6px" }, children: [
2389
+ msg.authorType === "email" ? /* @__PURE__ */ jsx("span", { style: s.badge(C.emailBg, C.orange), children: "Email" }) : /* @__PURE__ */ jsxs(
2390
+ "select",
2391
+ {
2392
+ value: msg.authorType,
2393
+ onChange: (e) => handleToggleAuthor(msg.id, e.target.value === "admin" ? "client" : "admin"),
2394
+ disabled: togglingAuthor === msg.id,
2395
+ style: {
2396
+ fontSize: "12px",
2397
+ fontWeight: 600,
2398
+ padding: "2px 6px",
2399
+ borderRadius: "4px",
2400
+ border: `1px solid ${C.border}`,
2401
+ cursor: "pointer",
2402
+ backgroundColor: msg.authorType === "admin" ? "#eff6ff" : "#f9fafb",
2403
+ color: "#374151",
2404
+ opacity: togglingAuthor === msg.id ? 0.5 : 1
2405
+ },
2406
+ children: [
2407
+ /* @__PURE__ */ jsx("option", { value: "admin", children: "Support" }),
2408
+ /* @__PURE__ */ jsx("option", { value: "client", children: "Client" })
2409
+ ]
2410
+ }
2411
+ ),
2412
+ msg.fromChat && /* @__PURE__ */ jsx("span", { style: s.badge("#e0f2fe", "#0284c7"), children: "Chat" }),
2413
+ msg.isInternal && /* @__PURE__ */ jsx("span", { style: s.badge("#fef3c7", "#92400e"), children: "Interne" }),
2414
+ msg.scheduledAt && (() => {
2415
+ const sched = msg;
2416
+ const scheduledDate = new Date(sched.scheduledAt).toLocaleString("fr-FR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
2417
+ const createdDate = new Date(msg.createdAt).toLocaleString("fr-FR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
2418
+ return sched.scheduledSent ? /* @__PURE__ */ jsxs("span", { style: s.badge("#f0fdf4", "#16a34a"), children: [
2419
+ "\u2713",
2420
+ " Programm\xE9 le ",
2421
+ createdDate,
2422
+ " \u2014 r\xE9dig\xE9 le ",
2423
+ scheduledDate !== createdDate ? scheduledDate : createdDate
2424
+ ] }) : /* @__PURE__ */ jsxs("span", { style: s.badge("#f3e8ff", "#7c3aed"), children: [
2425
+ "\u23F0",
2426
+ " R\xE9dig\xE9 le ",
2427
+ createdDate,
2428
+ " \u2014 envoi programm\xE9 le ",
2429
+ scheduledDate
2430
+ ] });
2431
+ })()
2432
+ ] }),
2433
+ /* @__PURE__ */ jsxs("span", { style: { color: C.textMuted, fontWeight: 500, fontSize: "11px", display: "inline-flex", alignItems: "center", gap: "6px" }, children: [
2434
+ formatMessageDate(msg.createdAt),
2435
+ msg.editedAt && /* @__PURE__ */ jsx("span", { style: { fontSize: "10px", color: "#6b7280", fontStyle: "italic" }, children: "(modifi\xE9)" }),
2436
+ !msg.isInternal && !msg.deletedAt && /* @__PURE__ */ jsxs(
2437
+ "button",
2438
+ {
2439
+ onClick: () => handleSplitMessage(msg.id, ticketSubject),
2440
+ style: { background: "none", border: "none", cursor: "pointer", fontSize: "10px", color: "#6b7280", padding: 0 },
2441
+ title: "Extraire en nouveau ticket",
2442
+ children: [
2443
+ "\u2197",
2444
+ " Extraire"
2445
+ ]
2446
+ }
2447
+ ),
2448
+ msg.authorType === "admin" && !msg.isInternal && (() => {
2449
+ const msgExt = msg;
2450
+ const isRead = lastClientReadAt && msg.createdAt && new Date(msg.createdAt) < new Date(lastClientReadAt);
2451
+ const sentAt = msgExt.emailSentAt;
2452
+ const openedAt = msgExt.emailOpenedAt;
2453
+ const sentTo = msgExt.emailSentTo;
2454
+ if (openedAt) {
2455
+ const openDate = new Date(openedAt).toLocaleString("fr-FR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", timeZone: "Europe/Paris" });
2456
+ return /* @__PURE__ */ jsxs("span", { title: `Envoy\xE9 \xE0 ${sentTo || "?"} \u2014 Ouvert le ${openDate}`, style: { fontSize: "10px", color: "#16a34a", fontWeight: 600, cursor: "help" }, children: [
2457
+ "\u2709 Ouvert ",
2458
+ openDate
2459
+ ] });
2460
+ }
2461
+ if (sentAt) {
2462
+ const sentDate = new Date(sentAt).toLocaleString("fr-FR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", timeZone: "Europe/Paris" });
2463
+ return /* @__PURE__ */ jsxs("span", { title: `Envoy\xE9 \xE0 ${sentTo || "?"} le ${sentDate}`, style: { fontSize: "10px", color: "#2563eb", fontWeight: 600, cursor: "help" }, children: [
2464
+ "\u2709 Envoy\xE9 \xE0 ",
2465
+ sentTo,
2466
+ " \u2014 ",
2467
+ sentDate
2468
+ ] });
2469
+ }
2470
+ return /* @__PURE__ */ jsx("span", { style: { fontSize: "10px", color: isRead ? "#16a34a" : "#94a3b8", fontWeight: 600 }, children: isRead ? "\u2713\u2713 Lu" : "\u2713 Envoy\xE9" });
2471
+ })()
2472
+ ] })
2473
+ ] }),
2474
+ editingMsg === msg.id ? /* @__PURE__ */ jsxs("div", { style: { marginTop: "6px" }, children: [
2475
+ /* @__PURE__ */ jsx(
2476
+ "textarea",
2477
+ {
2478
+ value: editBody,
2479
+ onChange: (e) => {
2480
+ setEditBody(e.target.value);
2481
+ setEditHtml(e.target.value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br />"));
2482
+ },
2483
+ rows: 4,
2484
+ style: { ...s.input, width: "100%", resize: "vertical", fontSize: "13px" }
2485
+ }
2486
+ ),
2487
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", marginTop: "8px" }, children: [
2488
+ /* @__PURE__ */ jsx("button", { onClick: () => handleEditSave(msg.id), disabled: savingEdit || !editBody.trim() && !editHtml, style: { ...s.btn(C.blue, savingEdit), fontSize: "11px", padding: "5px 12px" }, children: savingEdit ? "..." : "Enregistrer" }),
2489
+ /* @__PURE__ */ jsx("button", { onClick: handleEditCancel, style: { ...s.ghostBtn("#6b7280"), fontSize: "11px", padding: "5px 12px" }, children: "Annuler" })
2490
+ ] })
2491
+ ] }) : msg.deletedAt ? /* @__PURE__ */ jsx("div", { style: { fontSize: "13px", color: "#94a3b8", fontStyle: "italic" }, children: "Ce message a \xE9t\xE9 supprim\xE9." }) : msg.bodyHtml ? /* @__PURE__ */ jsxs(Fragment, { children: [
2492
+ /* @__PURE__ */ jsx(
2493
+ "div",
2494
+ {
2495
+ className: "rte-display",
2496
+ style: { fontSize: "13px", color: "#374151", lineHeight: 1.5 },
2497
+ dangerouslySetInnerHTML: { __html: msg.bodyHtml }
2498
+ }
2499
+ ),
2500
+ /* @__PURE__ */ jsx(CodeBlockRendererHtml, { html: msg.bodyHtml })
2501
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2502
+ /* @__PURE__ */ jsx("div", { style: { whiteSpace: "pre-wrap", fontSize: "13px", color: "#374151", lineHeight: 1.5 }, children: searchQuery.trim() ? msg.body.split(new RegExp(`(${searchQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi")).map(
2503
+ (part, i) => part.toLowerCase() === searchQuery.toLowerCase() ? /* @__PURE__ */ jsx("mark", { style: { backgroundColor: "#fde68a", borderRadius: "2px", padding: "0 2px", fontWeight: 600 }, children: part }, i) : part
2504
+ ) : msg.body }),
2505
+ /* @__PURE__ */ jsx(CodeBlockRenderer, { text: msg.body })
2506
+ ] }),
2507
+ Array.isArray(msg.attachments) && msg.attachments.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginTop: "8px", display: "flex", gap: "8px", flexWrap: "wrap" }, children: msg.attachments.map((att, i) => {
2508
+ const file = typeof att.file === "object" ? att.file : null;
2509
+ if (!file) return null;
2510
+ const mime = (file.mimeType || file.filename || "").toLowerCase();
2511
+ const isImage = mime.includes("image/") || /\.(png|jpg|jpeg|webp|gif|svg)$/i.test(file.filename || "");
2512
+ const isGif = mime.includes("image/gif") || /\.gif$/i.test(file.filename || "");
2513
+ const isVideo = mime.includes("video/") || /\.(mp4|webm|mov|avi)$/i.test(file.filename || "");
2514
+ if (isImage) {
2515
+ return /* @__PURE__ */ jsx("a", { href: file.url || "#", target: "_blank", rel: "noopener noreferrer", style: { display: "block" }, children: /* @__PURE__ */ jsx(
2516
+ "img",
2517
+ {
2518
+ src: file.url || "",
2519
+ alt: file.filename || "Image",
2520
+ style: {
2521
+ maxWidth: isGif ? 300 : 240,
2522
+ maxHeight: 200,
2523
+ borderRadius: "6px",
2524
+ border: `1px solid ${C.border}`,
2525
+ objectFit: "cover",
2526
+ cursor: "pointer"
2527
+ }
2528
+ }
2529
+ ) }, i);
2530
+ }
2531
+ if (isVideo) {
2532
+ return /* @__PURE__ */ jsx(
2533
+ "video",
2534
+ {
2535
+ src: file.url || "",
2536
+ controls: true,
2537
+ preload: "metadata",
2538
+ style: {
2539
+ maxWidth: 360,
2540
+ maxHeight: 240,
2541
+ borderRadius: "6px",
2542
+ border: `1px solid ${C.border}`,
2543
+ backgroundColor: "#000"
2544
+ }
2545
+ },
2546
+ i
2547
+ );
2548
+ }
2549
+ return /* @__PURE__ */ jsxs(
2550
+ "a",
2551
+ {
2552
+ href: file.url || "#",
2553
+ target: "_blank",
2554
+ rel: "noopener noreferrer",
2555
+ style: {
2556
+ display: "inline-flex",
2557
+ alignItems: "center",
2558
+ gap: "4px",
2559
+ padding: "4px 10px",
2560
+ borderRadius: "4px",
2561
+ border: `1px solid ${C.border}`,
2562
+ fontSize: "11px",
2563
+ fontWeight: 600,
2564
+ color: "#374151",
2565
+ textDecoration: "none",
2566
+ backgroundColor: C.white
2567
+ },
2568
+ children: [
2569
+ "\u{1F4CE}",
2570
+ " ",
2571
+ file.filename || "Fichier"
2572
+ ]
2573
+ },
2574
+ i
2575
+ );
2576
+ }) }),
2577
+ editingMsg !== msg.id && !msg.fromChat && /* @__PURE__ */ jsxs("div", { style: { marginTop: "6px", display: "flex", gap: "12px", alignItems: "center" }, children: [
2578
+ /* @__PURE__ */ jsx(
2579
+ "button",
2580
+ {
2581
+ type: "button",
2582
+ onClick: () => handleEditStart(msg),
2583
+ style: { border: "none", background: "none", cursor: "pointer", fontSize: "11px", color: C.textSecondary, padding: 0, fontWeight: 600, textDecoration: "underline" },
2584
+ children: "Modifier"
2585
+ }
2586
+ ),
2587
+ /* @__PURE__ */ jsx(
2588
+ "button",
2589
+ {
2590
+ type: "button",
2591
+ onClick: () => handleDelete(msg.id),
2592
+ disabled: deletingMsg === msg.id,
2593
+ style: { border: "none", background: "none", cursor: "pointer", fontSize: "11px", color: "#ef4444", padding: 0, fontWeight: 600, textDecoration: "underline", opacity: deletingMsg === msg.id ? 0.3 : 1 },
2594
+ children: "Supprimer"
2595
+ }
2596
+ ),
2597
+ msg.authorType === "admin" && !msg.isInternal && /* @__PURE__ */ jsx(
2598
+ "button",
2599
+ {
2600
+ type: "button",
2601
+ onClick: () => handleResend(msg.id),
2602
+ disabled: resendingMsg === msg.id,
2603
+ style: { border: "none", background: "none", cursor: "pointer", fontSize: "11px", color: "#2563eb", padding: 0, fontWeight: 600, textDecoration: "underline", opacity: resendingMsg === msg.id ? 0.3 : 1 },
2604
+ children: resendingMsg === msg.id ? "Envoi..." : resendSuccess === msg.id ? "Envoy\xE9 !" : "Renvoyer email"
2605
+ }
2606
+ )
2607
+ ] })
2608
+ ]
2609
+ }
2610
+ )
2611
+ ] })
2612
+ ] }, msg.id);
2613
+ })
2614
+ ] });
2615
+ })() }),
2616
+ clientTyping && /* @__PURE__ */ jsxs("div", { style: {
2617
+ display: "flex",
2618
+ alignItems: "center",
2619
+ gap: 8,
2620
+ padding: "8px 12px",
2621
+ fontSize: 12,
2622
+ color: "#7c3aed",
2623
+ fontWeight: 500
2624
+ }, children: [
2625
+ /* @__PURE__ */ jsxs("span", { style: { display: "flex", gap: 2 }, children: [
2626
+ /* @__PURE__ */ jsx("span", { style: { width: 5, height: 5, borderRadius: "50%", backgroundColor: "#7c3aed", animation: "bounce 1s infinite", animationDelay: "0ms" } }),
2627
+ /* @__PURE__ */ jsx("span", { style: { width: 5, height: 5, borderRadius: "50%", backgroundColor: "#7c3aed", animation: "bounce 1s infinite", animationDelay: "150ms" } }),
2628
+ /* @__PURE__ */ jsx("span", { style: { width: 5, height: 5, borderRadius: "50%", backgroundColor: "#7c3aed", animation: "bounce 1s infinite", animationDelay: "300ms" } })
2629
+ ] }),
2630
+ clientTypingName || "Client",
2631
+ " est en train d'\xE9crire..."
2632
+ ] }),
2633
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "16px" }, children: [
2634
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "6px", alignItems: "center", overflowX: "auto", paddingBottom: "8px", marginBottom: "6px", flexWrap: "wrap" }, children: [
2635
+ [
2636
+ "Bien re\xE7u, je regarde \xE7a !",
2637
+ "C'est corrig\xE9 !",
2638
+ "Pouvez-vous pr\xE9ciser ?",
2639
+ "Je reviens vers vous rapidement",
2640
+ "Pouvez-vous m'envoyer une capture d'\xE9cran ?"
2641
+ ].map((text) => /* @__PURE__ */ jsx(
2642
+ "button",
2643
+ {
2644
+ type: "button",
2645
+ onClick: () => {
2646
+ setReplyBody(text);
2647
+ const html = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2648
+ setReplyHtml(html);
2649
+ if (replyEditorRef.current?.setContent) {
2650
+ replyEditorRef.current.setContent(html);
2651
+ }
2652
+ },
2653
+ onMouseEnter: (e) => {
2654
+ e.currentTarget.style.backgroundColor = "#f1f5f9";
2655
+ },
2656
+ onMouseLeave: (e) => {
2657
+ e.currentTarget.style.backgroundColor = "white";
2658
+ },
2659
+ style: {
2660
+ padding: "3px 10px",
2661
+ borderRadius: "14px",
2662
+ border: `1px solid ${C.border}`,
2663
+ backgroundColor: "white",
2664
+ fontSize: "11px",
2665
+ fontWeight: 500,
2666
+ color: "#475569",
2667
+ cursor: "pointer",
2668
+ whiteSpace: "nowrap"
2669
+ },
2670
+ children: text
2671
+ },
2672
+ text
2673
+ )),
2674
+ features.ai && /* @__PURE__ */ jsx(
2675
+ "button",
2676
+ {
2677
+ onClick: handleAiSuggestReply,
2678
+ disabled: aiReplying || messages.length === 0,
2679
+ style: { ...s.outlineBtn("#7c3aed", aiReplying || messages.length === 0), fontSize: "11px", padding: "3px 10px", borderRadius: "14px" },
2680
+ children: aiReplying ? "G\xE9n\xE9ration..." : "Suggestion IA"
2681
+ }
2682
+ ),
2683
+ features.ai && /* @__PURE__ */ jsx(
2684
+ "button",
2685
+ {
2686
+ onClick: handleAiRewrite,
2687
+ disabled: aiRewriting || !replyBody.trim(),
2688
+ style: { ...s.outlineBtn("#0891b2", aiRewriting || !replyBody.trim()), fontSize: "11px", padding: "3px 10px", borderRadius: "14px" },
2689
+ children: aiRewriting ? "Reformulation..." : "Reformuler"
2690
+ }
2691
+ ),
2692
+ /* @__PURE__ */ jsx(
2693
+ CodeBlockInserter,
2694
+ {
2695
+ style: { ...s.outlineBtn("#059669", false), fontSize: "11px", padding: "3px 10px", borderRadius: "14px" },
2696
+ onInsert: (block) => {
2697
+ const nb = replyBody ? replyBody + block : block;
2698
+ setReplyBody(nb);
2699
+ setReplyHtml(nb.replace(/\n/g, "<br/>"));
2700
+ replyEditorRef.current?.setContent(nb.replace(/\n/g, "<br/>"));
2701
+ }
2702
+ }
2703
+ ),
2704
+ features.canned && cannedResponses.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
2705
+ /* @__PURE__ */ jsxs("select", { onChange: handleCannedSelect, style: { ...s.input, fontSize: "11px", padding: "3px 8px", fontWeight: 600 }, children: [
2706
+ /* @__PURE__ */ jsx("option", { value: "", children: "R\xE9ponse rapide..." }),
2707
+ cannedResponses.map((cr) => /* @__PURE__ */ jsx("option", { value: String(cr.id), children: cr.title }, cr.id))
2708
+ ] }),
2709
+ /* @__PURE__ */ jsx(
2710
+ "span",
2711
+ {
2712
+ title: "Variables disponibles : {{client.firstName}}, {{client.lastName}}, {{client.company}}, {{client.email}}, {{ticket.number}}, {{ticket.subject}}, {{agent.name}}",
2713
+ style: { cursor: "help", fontSize: "13px", color: C.textMuted },
2714
+ children: "\u24D8"
2715
+ }
2716
+ )
2717
+ ] })
2718
+ ] }),
2719
+ /* @__PURE__ */ jsx("div", { style: { border: `1px solid ${C.border}`, borderRadius: "8px", overflow: "hidden" }, children: /* @__PURE__ */ jsx(
2720
+ "textarea",
2721
+ {
2722
+ value: replyBody,
2723
+ onChange: (e) => {
2724
+ const text = e.target.value;
2725
+ setReplyBody(text);
2726
+ setReplyHtml(text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br />"));
2727
+ sendAdminTyping();
2728
+ },
2729
+ placeholder: "\xC9crire une r\xE9ponse au client...",
2730
+ style: {
2731
+ width: "100%",
2732
+ minHeight: "120px",
2733
+ padding: "12px",
2734
+ border: "none",
2735
+ outline: "none",
2736
+ fontSize: "14px",
2737
+ lineHeight: 1.5,
2738
+ resize: "vertical",
2739
+ fontFamily: "inherit",
2740
+ color: "#374151",
2741
+ backgroundColor: "transparent",
2742
+ boxSizing: "border-box"
2743
+ }
2744
+ }
2745
+ ) }),
2746
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: "8px", display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" }, children: [
2747
+ /* @__PURE__ */ jsx(
2748
+ "input",
2749
+ {
2750
+ ref: fileInputRef,
2751
+ type: "file",
2752
+ multiple: true,
2753
+ onChange: handleReplyFileChange,
2754
+ style: { display: "none" },
2755
+ accept: "image/*,.pdf,.doc,.docx,.txt,.zip"
2756
+ }
2757
+ ),
2758
+ /* @__PURE__ */ jsx(
2759
+ "button",
2760
+ {
2761
+ type: "button",
2762
+ onClick: () => fileInputRef.current?.click(),
2763
+ style: { ...s.ghostBtn("#6b7280"), fontSize: "12px", padding: "5px 10px" },
2764
+ children: "+ Pi\xE8ce jointe"
2765
+ }
2766
+ ),
2767
+ replyFiles.length > 0 && /* @__PURE__ */ jsx(Fragment, { children: replyFiles.map((file, i) => /* @__PURE__ */ jsxs("span", { style: { ...s.badge("#f1f5f9", "#374151"), display: "inline-flex", alignItems: "center", gap: "4px" }, children: [
2768
+ "\u{1F4CE}",
2769
+ " ",
2770
+ file.name,
2771
+ /* @__PURE__ */ jsx(
2772
+ "button",
2773
+ {
2774
+ type: "button",
2775
+ onClick: () => setReplyFiles((prev) => prev.filter((_, idx) => idx !== i)),
2776
+ style: { border: "none", background: "none", color: "#ef4444", fontWeight: 700, cursor: "pointer", fontSize: "14px", lineHeight: 1 },
2777
+ children: "\xD7"
2778
+ }
2779
+ )
2780
+ ] }, i)) })
2781
+ ] }),
2782
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "10px", flexWrap: "wrap", marginTop: "8px" }, children: [
2783
+ /* @__PURE__ */ jsxs(
2784
+ "select",
2785
+ {
2786
+ value: sendAsClient ? "client" : "admin",
2787
+ onChange: (e) => setSendAsClient(e.target.value === "client"),
2788
+ style: { ...s.input, fontSize: "12px", padding: "6px 8px", fontWeight: 600 },
2789
+ children: [
2790
+ /* @__PURE__ */ jsx("option", { value: "admin", children: "En tant que : Support" }),
2791
+ /* @__PURE__ */ jsx("option", { value: "client", children: "En tant que : Client" })
2792
+ ]
2793
+ }
2794
+ ),
2795
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: "5px", fontSize: "12px", cursor: "pointer", fontWeight: 600 }, children: [
2796
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => {
2797
+ setIsInternal(e.target.checked);
2798
+ if (e.target.checked) setNotifyClient(false);
2799
+ }, style: { width: "14px", height: "14px", accentColor: C.amber } }),
2800
+ "Note interne"
2801
+ ] }),
2802
+ !isInternal && /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: "5px", fontSize: "12px", cursor: "pointer", fontWeight: 600 }, children: [
2803
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: notifyClient, onChange: (e) => setNotifyClient(e.target.checked), style: { width: "14px", height: "14px", accentColor: "#16a34a" } }),
2804
+ "Envoyer au client"
2805
+ ] }),
2806
+ /* @__PURE__ */ jsx("button", { "data-action": "send-reply", onClick: handleSendReply, disabled: sending || !replyBody.trim() && !replyHtml, style: { ...s.btn(isInternal ? C.amber : notifyClient ? "#16a34a" : C.blue, sending || !replyBody.trim() && !replyHtml), fontSize: "13px", padding: "8px 20px", marginLeft: "auto" }, children: sending ? "Envoi..." : isInternal ? "Ajouter note" : notifyClient ? "Envoyer + Notifier" : "Sauvegarder" }),
2807
+ !isInternal && /* @__PURE__ */ jsx(
2808
+ "button",
2809
+ {
2810
+ onClick: () => setShowSchedule(!showSchedule),
2811
+ disabled: !replyBody.trim() && !replyHtml,
2812
+ style: { ...s.outlineBtn("#7c3aed", !replyBody.trim() && !replyHtml), fontSize: "12px", padding: "8px 12px" },
2813
+ title: "Programmer l'envoi \xE0 une date/heure pr\xE9cise",
2814
+ children: "\u23F0"
2815
+ }
2816
+ )
2817
+ ] }),
2818
+ showSchedule && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "center", marginTop: "8px", padding: "10px 14px", borderRadius: "8px", backgroundColor: "#faf5ff", border: "1px solid #e9d5ff" }, children: [
2819
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "12px", fontWeight: 600, color: "#7c3aed" }, children: "Programmer pour :" }),
2820
+ /* @__PURE__ */ jsx(
2821
+ "input",
2822
+ {
2823
+ type: "datetime-local",
2824
+ value: scheduleDate,
2825
+ onChange: (e) => setScheduleDate(e.target.value),
2826
+ min: (/* @__PURE__ */ new Date()).toISOString().slice(0, 16),
2827
+ style: { ...s.input, fontSize: "12px", width: "auto" }
2828
+ }
2829
+ ),
2830
+ /* @__PURE__ */ jsx(
2831
+ "button",
2832
+ {
2833
+ onClick: handleScheduleReply,
2834
+ disabled: sending || !scheduleDate || !replyBody.trim() && !replyHtml,
2835
+ style: { ...s.btn("#7c3aed", sending || !scheduleDate || !replyBody.trim() && !replyHtml), fontSize: "12px", padding: "6px 14px" },
2836
+ children: sending ? "..." : "\u23F0 Programmer"
2837
+ }
2838
+ ),
2839
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowSchedule(false), style: { background: "none", border: "none", cursor: "pointer", color: "#94a3b8", fontSize: "14px" }, children: "\u2715" })
2840
+ ] }),
2841
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: "11px", color: C.textMuted, marginTop: "4px", textAlign: "right" }, children: [
2842
+ "\u2318",
2843
+ "Enter pour envoyer \xB7 ",
2844
+ "\u2318",
2845
+ "\u21E7",
2846
+ "N note interne"
2847
+ ] })
2848
+ ] }),
2849
+ /* @__PURE__ */ jsx("div", { style: { borderTop: `1px solid ${C.border}`, marginBottom: "16px" } }),
2850
+ /* @__PURE__ */ jsx(
2851
+ QuickActions,
2852
+ {
2853
+ statusTransitions,
2854
+ statusUpdating,
2855
+ onStatusChange: handleStatusChange,
2856
+ snoozeUntil,
2857
+ snoozeSaving,
2858
+ onCancelSnooze: () => handleSnooze(null),
2859
+ showMerge,
2860
+ showExtMsg,
2861
+ showSnooze,
2862
+ onToggleMerge: () => {
2863
+ setShowMerge(!showMerge);
2864
+ setShowExtMsg(false);
2865
+ setShowSnooze(false);
2866
+ },
2867
+ onToggleExtMsg: () => {
2868
+ setShowExtMsg(!showExtMsg);
2869
+ setShowMerge(false);
2870
+ setShowSnooze(false);
2871
+ },
2872
+ onToggleSnooze: () => {
2873
+ setShowSnooze(!showSnooze);
2874
+ setShowMerge(false);
2875
+ setShowExtMsg(false);
2876
+ },
2877
+ onNextTicket: handleNextTicket,
2878
+ showNextTicket,
2879
+ nextTicketId,
2880
+ nextTicketInfo,
2881
+ onCloseNextTicket: () => setShowNextTicket(false)
2882
+ }
2883
+ ),
2884
+ features.ai && /* @__PURE__ */ jsx(
2885
+ AISummaryPanel,
2886
+ {
2887
+ showAiSummary,
2888
+ setShowAiSummary,
2889
+ aiSummary,
2890
+ aiGenerating,
2891
+ aiSaving,
2892
+ aiSaved,
2893
+ handleAiGenerate,
2894
+ handleAiSave
2895
+ }
2896
+ ),
2897
+ features.merge && showMerge && /* @__PURE__ */ jsx(
2898
+ MergePanel,
2899
+ {
2900
+ mergeTarget,
2901
+ setMergeTarget,
2902
+ mergeTargetInfo,
2903
+ setMergeTargetInfo,
2904
+ mergeError,
2905
+ setMergeError,
2906
+ merging,
2907
+ handleMergeLookup,
2908
+ handleMerge
2909
+ }
2910
+ ),
2911
+ features.externalMessages && showExtMsg && /* @__PURE__ */ jsx(
2912
+ ExtMessagePanel,
2913
+ {
2914
+ extMsgBody,
2915
+ setExtMsgBody,
2916
+ extMsgAuthor,
2917
+ setExtMsgAuthor,
2918
+ extMsgDate,
2919
+ setExtMsgDate,
2920
+ extMsgFiles,
2921
+ setExtMsgFiles,
2922
+ sendingExtMsg,
2923
+ handleSendExtMsg,
2924
+ handleExtFileChange
2925
+ }
2926
+ ),
2927
+ features.snooze && showSnooze && /* @__PURE__ */ jsx(SnoozePanel, { snoozeSaving, handleSnooze })
2928
+ ] }),
2929
+ /* @__PURE__ */ jsxs("div", { style: layoutStyles.sideColumn, children: [
2930
+ /* @__PURE__ */ jsx(
2931
+ TimeTrackingPanel,
2932
+ {
2933
+ timeEntries,
2934
+ totalMinutes,
2935
+ timerRunning,
2936
+ timerSeconds,
2937
+ setTimerSeconds,
2938
+ timerDescription,
2939
+ setTimerDescription,
2940
+ handleTimerStart,
2941
+ handleTimerStop,
2942
+ handleTimerSave,
2943
+ handleTimerDiscard,
2944
+ duration,
2945
+ setDuration,
2946
+ timeDescription,
2947
+ setTimeDescription,
2948
+ handleAddTime,
2949
+ addingTime,
2950
+ timeSuccess
2951
+ }
2952
+ ),
2953
+ features.clientHistory && client && /* @__PURE__ */ jsx(
2954
+ ClientHistory,
2955
+ {
2956
+ client,
2957
+ clientTickets,
2958
+ clientProjects,
2959
+ clientNotes,
2960
+ onNotesChange: (v) => {
2961
+ setClientNotes(v);
2962
+ setNotesSaved(false);
2963
+ },
2964
+ onNotesSave: async () => {
2965
+ if (!client) return;
2966
+ setSavingNotes(true);
2967
+ try {
2968
+ const res = await fetch(`/api/support-clients/${client.id}`, {
2969
+ method: "PATCH",
2970
+ headers: { "Content-Type": "application/json" },
2971
+ credentials: "include",
2972
+ body: JSON.stringify({ notes: clientNotes })
2973
+ });
2974
+ if (res.ok) {
2975
+ setNotesSaved(true);
2976
+ setTimeout(() => setNotesSaved(false), 3e3);
2977
+ }
2978
+ } catch {
2979
+ } finally {
2980
+ setSavingNotes(false);
2981
+ }
2982
+ },
2983
+ savingNotes,
2984
+ notesSaved
2985
+ }
2986
+ ),
2987
+ features.activityLog && /* @__PURE__ */ jsx(ActivityLog, { activityLog }),
2988
+ /* @__PURE__ */ jsx("div", { style: s.section, children: /* @__PURE__ */ jsx(
2989
+ "a",
2990
+ {
2991
+ href: "/api/support/export-csv",
2992
+ target: "_blank",
2993
+ rel: "noopener noreferrer",
2994
+ style: { ...s.ghostBtn("#6b7280"), fontSize: "12px", textDecoration: "none", display: "inline-block" },
2995
+ children: "Exporter tous les tickets (CSV)"
2996
+ }
2997
+ ) })
2998
+ ] })
2999
+ ] })
3000
+ ] });
3001
+ };
3002
+ var TicketConversation_default = TicketConversation;
3003
+
3004
+ export { TicketConversation_default as default };