@consilioweb/payload-support 0.6.1 → 0.6.3

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