@consilioweb/payload-support 0.6.2 → 0.6.4

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 (94) 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.js +1 -1
  65. package/dist/views/BillingView/index.js +2 -2
  66. package/dist/views/ChatView/client.js +1 -1
  67. package/dist/views/ChatView/index.js +2 -2
  68. package/dist/views/CrmView/client.js +1 -1
  69. package/dist/views/CrmView/index.js +2 -2
  70. package/dist/views/EmailTrackingView/client.js +1 -1
  71. package/dist/views/EmailTrackingView/index.js +2 -2
  72. package/dist/views/ImportConversationView/client.js +1 -1
  73. package/dist/views/ImportConversationView/index.js +2 -2
  74. package/dist/views/LogsView/client.js +1 -1
  75. package/dist/views/LogsView/index.js +2 -2
  76. package/dist/views/NewTicketView/client.js +1 -1
  77. package/dist/views/NewTicketView/index.js +2 -2
  78. package/dist/views/PendingEmailsView/client.js +1 -1
  79. package/dist/views/PendingEmailsView/index.js +2 -2
  80. package/dist/views/SupportDashboardView/client.js +1 -1
  81. package/dist/views/SupportDashboardView/index.js +2 -2
  82. package/dist/views/TicketDetailView/client.js +2 -2
  83. package/dist/views/TicketDetailView/index.js +2 -2
  84. package/dist/views/TicketInboxView/client.js +1 -1
  85. package/dist/views/TicketInboxView/index.js +2 -2
  86. package/dist/views/TicketingSettingsView/client.js +3 -3
  87. package/dist/views/TicketingSettingsView/index.js +2 -2
  88. package/dist/views/TimeDashboardView/client.js +1 -1
  89. package/dist/views/TimeDashboardView/index.js +2 -2
  90. package/dist/views/shared/AdminViewHeader.js +1 -1
  91. package/dist/views/shared/index.js +5 -5
  92. package/dist/views.js +13 -13
  93. package/package.json +2 -2
  94. package/dist/components/TicketConversation.js +0 -3004
@@ -0,0 +1,188 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ function useReply(id, client, cannedResponses, ticketNumber, ticketSubject, fetchAll, handleNextTicket, replyEditorRef) {
6
+ const fileInputRef = react.useRef(null);
7
+ const [replyBody, setReplyBody] = react.useState("");
8
+ const [replyHtml, setReplyHtml] = react.useState("");
9
+ const [replyFiles, setReplyFiles] = react.useState([]);
10
+ const [isInternal, setIsInternal] = react.useState(false);
11
+ const [notifyClient, setNotifyClient] = react.useState(false);
12
+ const [sendAsClient, setSendAsClient] = react.useState(false);
13
+ const [sending, setSending] = react.useState(false);
14
+ const [showSchedule, setShowSchedule] = react.useState(false);
15
+ const [scheduleDate, setScheduleDate] = react.useState("");
16
+ const handleEditorFileUpload = react.useCallback(async (file) => {
17
+ if (file.size > 5 * 1024 * 1024) return null;
18
+ const formData = new FormData();
19
+ formData.append("file", file);
20
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
21
+ try {
22
+ const res = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
23
+ if (res.ok) {
24
+ const data = await res.json();
25
+ return data.doc?.url || null;
26
+ }
27
+ } catch {
28
+ }
29
+ return null;
30
+ }, []);
31
+ const replaceCannedVariables = react.useCallback((text) => {
32
+ let result = text;
33
+ if (client) {
34
+ result = result.replace(/\{\{client\.firstName\}\}/g, client.firstName || "Client");
35
+ result = result.replace(/\{\{client\.lastName\}\}/g, client.lastName || "");
36
+ result = result.replace(/\{\{client\.company\}\}/g, client.company || "");
37
+ result = result.replace(/\{\{client\.email\}\}/g, client.email || "");
38
+ result = result.replace(/\{\{clientName\}\}/g, client.firstName || "Client");
39
+ }
40
+ result = result.replace(/\{\{ticket\.number\}\}/g, ticketNumber || "");
41
+ result = result.replace(/\{\{ticket\.subject\}\}/g, ticketSubject || "");
42
+ result = result.replace(/\{\{agent\.name\}\}/g, "Support");
43
+ return result;
44
+ }, [client, ticketNumber, ticketSubject]);
45
+ const handleCannedSelect = (e) => {
46
+ const selected = cannedResponses.find((cr) => String(cr.id) === e.target.value);
47
+ if (selected) {
48
+ const body = replaceCannedVariables(selected.body);
49
+ const html = body.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br />");
50
+ setReplyBody(body);
51
+ setReplyHtml(html);
52
+ replyEditorRef.current?.setContent(html);
53
+ }
54
+ e.target.value = "";
55
+ };
56
+ const handleReplyFileChange = (e) => {
57
+ if (e.target.files) {
58
+ const maxSize = 1 * 1024 * 1024;
59
+ const newFiles = Array.from(e.target.files);
60
+ const tooLarge = newFiles.filter((f) => f.size > maxSize);
61
+ if (tooLarge.length > 0) {
62
+ alert(`Fichier(s) trop volumineux (max 1 Mo) : ${tooLarge.map((f) => f.name).join(", ")}`);
63
+ }
64
+ setReplyFiles((prev) => [...prev, ...newFiles.filter((f) => f.size <= maxSize)]);
65
+ }
66
+ if (fileInputRef.current) fileInputRef.current.value = "";
67
+ };
68
+ const uploadFiles = async (files) => {
69
+ const uploadedIds = [];
70
+ for (const file of files) {
71
+ if (file.size > 1 * 1024 * 1024) continue;
72
+ const formData = new FormData();
73
+ formData.append("file", file);
74
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
75
+ const uploadRes = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
76
+ if (uploadRes.ok) {
77
+ const d = await uploadRes.json();
78
+ if (d.doc?.id) uploadedIds.push(d.doc.id);
79
+ }
80
+ }
81
+ return uploadedIds;
82
+ };
83
+ const resetReply = () => {
84
+ setReplyBody("");
85
+ setReplyHtml("");
86
+ setReplyFiles([]);
87
+ setIsInternal(false);
88
+ setNotifyClient(false);
89
+ setSendAsClient(false);
90
+ replyEditorRef.current?.clear();
91
+ };
92
+ const handleSendReply = async () => {
93
+ if (!replyBody.trim() || !id) return;
94
+ setSending(true);
95
+ try {
96
+ const uploadedIds = await uploadFiles(replyFiles);
97
+ const finalBody = replyBody.trim() || (replyHtml ? "[Contenu enrichi]" : "");
98
+ const messageData = {
99
+ ticket: id,
100
+ body: finalBody,
101
+ ...replyHtml ? { bodyHtml: replyHtml } : {},
102
+ authorType: sendAsClient ? "client" : "admin",
103
+ isInternal,
104
+ skipNotification: isInternal || !notifyClient,
105
+ ...sendAsClient && client ? { authorClient: client.id } : {}
106
+ };
107
+ if (uploadedIds.length > 0) {
108
+ messageData.attachments = uploadedIds.map((mid) => ({ file: mid }));
109
+ }
110
+ const res = await fetch("/api/ticket-messages", {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ credentials: "include",
114
+ body: JSON.stringify(messageData)
115
+ });
116
+ if (res.ok) {
117
+ resetReply();
118
+ fetchAll();
119
+ if (!isInternal) handleNextTicket();
120
+ }
121
+ } catch {
122
+ } finally {
123
+ setSending(false);
124
+ }
125
+ };
126
+ const handleScheduleReply = async () => {
127
+ if (!replyBody.trim() || !id || !scheduleDate) return;
128
+ setSending(true);
129
+ try {
130
+ const uploadedIds = await uploadFiles(replyFiles);
131
+ const messageData = {
132
+ ticket: id,
133
+ body: replyBody.trim(),
134
+ ...replyHtml ? { bodyHtml: replyHtml } : {},
135
+ authorType: "admin",
136
+ isInternal: false,
137
+ skipNotification: true,
138
+ scheduledAt: new Date(scheduleDate).toISOString(),
139
+ scheduledSent: false
140
+ };
141
+ if (uploadedIds.length > 0) {
142
+ messageData.attachments = uploadedIds.map((mid) => ({ file: mid }));
143
+ }
144
+ const res = await fetch("/api/ticket-messages", {
145
+ method: "POST",
146
+ headers: { "Content-Type": "application/json" },
147
+ credentials: "include",
148
+ body: JSON.stringify(messageData)
149
+ });
150
+ if (res.ok) {
151
+ resetReply();
152
+ setShowSchedule(false);
153
+ setScheduleDate("");
154
+ fetchAll();
155
+ }
156
+ } catch {
157
+ } finally {
158
+ setSending(false);
159
+ }
160
+ };
161
+ return {
162
+ fileInputRef,
163
+ replyBody,
164
+ setReplyBody,
165
+ replyHtml,
166
+ setReplyHtml,
167
+ replyFiles,
168
+ setReplyFiles,
169
+ isInternal,
170
+ setIsInternal,
171
+ notifyClient,
172
+ setNotifyClient,
173
+ sendAsClient,
174
+ setSendAsClient,
175
+ sending,
176
+ showSchedule,
177
+ setShowSchedule,
178
+ scheduleDate,
179
+ setScheduleDate,
180
+ handleEditorFileUpload,
181
+ handleCannedSelect,
182
+ handleReplyFileChange,
183
+ handleSendReply,
184
+ handleScheduleReply
185
+ };
186
+ }
187
+
188
+ exports.useReply = useReply;
@@ -0,0 +1,187 @@
1
+ "use client";
2
+ import { useRef, useState, useCallback } from 'react';
3
+
4
+ function useReply(id, client, cannedResponses, ticketNumber, ticketSubject, fetchAll, handleNextTicket, replyEditorRef) {
5
+ const fileInputRef = useRef(null);
6
+ const [replyBody, setReplyBody] = useState("");
7
+ const [replyHtml, setReplyHtml] = useState("");
8
+ const [replyFiles, setReplyFiles] = useState([]);
9
+ const [isInternal, setIsInternal] = useState(false);
10
+ const [notifyClient, setNotifyClient] = useState(false);
11
+ const [sendAsClient, setSendAsClient] = useState(false);
12
+ const [sending, setSending] = useState(false);
13
+ const [showSchedule, setShowSchedule] = useState(false);
14
+ const [scheduleDate, setScheduleDate] = useState("");
15
+ const handleEditorFileUpload = useCallback(async (file) => {
16
+ if (file.size > 5 * 1024 * 1024) return null;
17
+ const formData = new FormData();
18
+ formData.append("file", file);
19
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
20
+ try {
21
+ const res = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
22
+ if (res.ok) {
23
+ const data = await res.json();
24
+ return data.doc?.url || null;
25
+ }
26
+ } catch {
27
+ }
28
+ return null;
29
+ }, []);
30
+ const replaceCannedVariables = useCallback((text) => {
31
+ let result = text;
32
+ if (client) {
33
+ result = result.replace(/\{\{client\.firstName\}\}/g, client.firstName || "Client");
34
+ result = result.replace(/\{\{client\.lastName\}\}/g, client.lastName || "");
35
+ result = result.replace(/\{\{client\.company\}\}/g, client.company || "");
36
+ result = result.replace(/\{\{client\.email\}\}/g, client.email || "");
37
+ result = result.replace(/\{\{clientName\}\}/g, client.firstName || "Client");
38
+ }
39
+ result = result.replace(/\{\{ticket\.number\}\}/g, ticketNumber || "");
40
+ result = result.replace(/\{\{ticket\.subject\}\}/g, ticketSubject || "");
41
+ result = result.replace(/\{\{agent\.name\}\}/g, "Support");
42
+ return result;
43
+ }, [client, ticketNumber, ticketSubject]);
44
+ const handleCannedSelect = (e) => {
45
+ const selected = cannedResponses.find((cr) => String(cr.id) === e.target.value);
46
+ if (selected) {
47
+ const body = replaceCannedVariables(selected.body);
48
+ const html = body.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br />");
49
+ setReplyBody(body);
50
+ setReplyHtml(html);
51
+ replyEditorRef.current?.setContent(html);
52
+ }
53
+ e.target.value = "";
54
+ };
55
+ const handleReplyFileChange = (e) => {
56
+ if (e.target.files) {
57
+ const maxSize = 1 * 1024 * 1024;
58
+ const newFiles = Array.from(e.target.files);
59
+ const tooLarge = newFiles.filter((f) => f.size > maxSize);
60
+ if (tooLarge.length > 0) {
61
+ alert(`Fichier(s) trop volumineux (max 1 Mo) : ${tooLarge.map((f) => f.name).join(", ")}`);
62
+ }
63
+ setReplyFiles((prev) => [...prev, ...newFiles.filter((f) => f.size <= maxSize)]);
64
+ }
65
+ if (fileInputRef.current) fileInputRef.current.value = "";
66
+ };
67
+ const uploadFiles = async (files) => {
68
+ const uploadedIds = [];
69
+ for (const file of files) {
70
+ if (file.size > 1 * 1024 * 1024) continue;
71
+ const formData = new FormData();
72
+ formData.append("file", file);
73
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
74
+ const uploadRes = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
75
+ if (uploadRes.ok) {
76
+ const d = await uploadRes.json();
77
+ if (d.doc?.id) uploadedIds.push(d.doc.id);
78
+ }
79
+ }
80
+ return uploadedIds;
81
+ };
82
+ const resetReply = () => {
83
+ setReplyBody("");
84
+ setReplyHtml("");
85
+ setReplyFiles([]);
86
+ setIsInternal(false);
87
+ setNotifyClient(false);
88
+ setSendAsClient(false);
89
+ replyEditorRef.current?.clear();
90
+ };
91
+ const handleSendReply = async () => {
92
+ if (!replyBody.trim() || !id) return;
93
+ setSending(true);
94
+ try {
95
+ const uploadedIds = await uploadFiles(replyFiles);
96
+ const finalBody = replyBody.trim() || (replyHtml ? "[Contenu enrichi]" : "");
97
+ const messageData = {
98
+ ticket: id,
99
+ body: finalBody,
100
+ ...replyHtml ? { bodyHtml: replyHtml } : {},
101
+ authorType: sendAsClient ? "client" : "admin",
102
+ isInternal,
103
+ skipNotification: isInternal || !notifyClient,
104
+ ...sendAsClient && client ? { authorClient: client.id } : {}
105
+ };
106
+ if (uploadedIds.length > 0) {
107
+ messageData.attachments = uploadedIds.map((mid) => ({ file: mid }));
108
+ }
109
+ const res = await fetch("/api/ticket-messages", {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ credentials: "include",
113
+ body: JSON.stringify(messageData)
114
+ });
115
+ if (res.ok) {
116
+ resetReply();
117
+ fetchAll();
118
+ if (!isInternal) handleNextTicket();
119
+ }
120
+ } catch {
121
+ } finally {
122
+ setSending(false);
123
+ }
124
+ };
125
+ const handleScheduleReply = async () => {
126
+ if (!replyBody.trim() || !id || !scheduleDate) return;
127
+ setSending(true);
128
+ try {
129
+ const uploadedIds = await uploadFiles(replyFiles);
130
+ const messageData = {
131
+ ticket: id,
132
+ body: replyBody.trim(),
133
+ ...replyHtml ? { bodyHtml: replyHtml } : {},
134
+ authorType: "admin",
135
+ isInternal: false,
136
+ skipNotification: true,
137
+ scheduledAt: new Date(scheduleDate).toISOString(),
138
+ scheduledSent: false
139
+ };
140
+ if (uploadedIds.length > 0) {
141
+ messageData.attachments = uploadedIds.map((mid) => ({ file: mid }));
142
+ }
143
+ const res = await fetch("/api/ticket-messages", {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ credentials: "include",
147
+ body: JSON.stringify(messageData)
148
+ });
149
+ if (res.ok) {
150
+ resetReply();
151
+ setShowSchedule(false);
152
+ setScheduleDate("");
153
+ fetchAll();
154
+ }
155
+ } catch {
156
+ } finally {
157
+ setSending(false);
158
+ }
159
+ };
160
+ return {
161
+ fileInputRef,
162
+ replyBody,
163
+ setReplyBody,
164
+ replyHtml,
165
+ setReplyHtml,
166
+ replyFiles,
167
+ setReplyFiles,
168
+ isInternal,
169
+ setIsInternal,
170
+ notifyClient,
171
+ setNotifyClient,
172
+ sendAsClient,
173
+ setSendAsClient,
174
+ sending,
175
+ showSchedule,
176
+ setShowSchedule,
177
+ scheduleDate,
178
+ setScheduleDate,
179
+ handleEditorFileUpload,
180
+ handleCannedSelect,
181
+ handleReplyFileChange,
182
+ handleSendReply,
183
+ handleScheduleReply
184
+ };
185
+ }
186
+
187
+ export { useReply };
@@ -0,0 +1,231 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ function useTicketActions(id, fetchAll) {
6
+ const [statusUpdating, setStatusUpdating] = react.useState(false);
7
+ const [showMerge, setShowMerge] = react.useState(false);
8
+ const [mergeTarget, setMergeTarget] = react.useState("");
9
+ const [mergeTargetInfo, setMergeTargetInfo] = react.useState(null);
10
+ const [merging, setMerging] = react.useState(false);
11
+ const [mergeError, setMergeError] = react.useState("");
12
+ const [showExtMsg, setShowExtMsg] = react.useState(false);
13
+ const [extMsgBody, setExtMsgBody] = react.useState("");
14
+ const [extMsgAuthor, setExtMsgAuthor] = react.useState("client");
15
+ const [extMsgDate, setExtMsgDate] = react.useState("");
16
+ const [extMsgFiles, setExtMsgFiles] = react.useState([]);
17
+ const [sendingExtMsg, setSendingExtMsg] = react.useState(false);
18
+ const extFileInputRef = react.useRef(null);
19
+ const [showSnooze, setShowSnooze] = react.useState(false);
20
+ const [snoozeUntil, setSnoozeUntil] = react.useState(null);
21
+ const [snoozeSaving, setSnoozeSaving] = react.useState(false);
22
+ const [showNextTicket, setShowNextTicket] = react.useState(false);
23
+ const [nextTicketId, setNextTicketId] = react.useState(null);
24
+ const [nextTicketInfo, setNextTicketInfo] = react.useState("");
25
+ const handleStatusChange = async (newStatus) => {
26
+ if (!id) return;
27
+ setStatusUpdating(true);
28
+ try {
29
+ await fetch(`/api/tickets/${id}`, {
30
+ method: "PATCH",
31
+ headers: { "Content-Type": "application/json" },
32
+ credentials: "include",
33
+ body: JSON.stringify({ status: newStatus })
34
+ });
35
+ window.location.reload();
36
+ } catch {
37
+ } finally {
38
+ setStatusUpdating(false);
39
+ }
40
+ };
41
+ const handleMergeLookup = async () => {
42
+ if (!mergeTarget.trim()) return;
43
+ setMergeTargetInfo(null);
44
+ setMergeError("");
45
+ try {
46
+ const searchVal = mergeTarget.trim().toUpperCase();
47
+ const res = await fetch(`/api/tickets?where[ticketNumber][equals]=${encodeURIComponent(searchVal)}&limit=1&depth=0`, { credentials: "include" });
48
+ if (res.ok) {
49
+ const d = await res.json();
50
+ if (d.docs?.length > 0) {
51
+ const t = d.docs[0];
52
+ if (String(t.id) === String(id)) {
53
+ setMergeError("Impossible de fusionner un ticket avec lui-m\xEAme");
54
+ } else {
55
+ setMergeTargetInfo({ id: t.id, ticketNumber: t.ticketNumber, subject: t.subject });
56
+ }
57
+ } else {
58
+ setMergeError("Ticket introuvable");
59
+ }
60
+ }
61
+ } catch {
62
+ setMergeError("Erreur de recherche");
63
+ }
64
+ };
65
+ const handleMerge = async () => {
66
+ if (!mergeTargetInfo || !id) return;
67
+ setMerging(true);
68
+ setMergeError("");
69
+ try {
70
+ const res = await fetch("/api/support/merge-tickets", {
71
+ method: "POST",
72
+ headers: { "Content-Type": "application/json" },
73
+ credentials: "include",
74
+ body: JSON.stringify({ sourceTicketId: id, targetTicketId: mergeTargetInfo.id })
75
+ });
76
+ if (res.ok) {
77
+ window.location.href = `/admin/collections/tickets/${mergeTargetInfo.id}`;
78
+ } else {
79
+ const d = await res.json().catch(() => ({}));
80
+ setMergeError(d.error || "Erreur de fusion");
81
+ }
82
+ } catch {
83
+ setMergeError("Erreur r\xE9seau");
84
+ } finally {
85
+ setMerging(false);
86
+ }
87
+ };
88
+ const handleExtFileChange = (e) => {
89
+ if (e.target.files) {
90
+ const maxSize = 1 * 1024 * 1024;
91
+ const newFiles = Array.from(e.target.files);
92
+ const tooLarge = newFiles.filter((f) => f.size > maxSize);
93
+ if (tooLarge.length > 0) {
94
+ alert(`Fichier(s) trop volumineux (max 1 Mo) : ${tooLarge.map((f) => f.name).join(", ")}`);
95
+ }
96
+ setExtMsgFiles((prev) => [...prev, ...newFiles.filter((f) => f.size <= maxSize)]);
97
+ }
98
+ if (extFileInputRef.current) extFileInputRef.current.value = "";
99
+ };
100
+ const handleSendExtMsg = async () => {
101
+ if (!extMsgBody.trim() || !id) return;
102
+ setSendingExtMsg(true);
103
+ try {
104
+ const uploadedIds = [];
105
+ for (const file of extMsgFiles) {
106
+ if (file.size > 1 * 1024 * 1024) continue;
107
+ const formData = new FormData();
108
+ formData.append("file", file);
109
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
110
+ const uploadRes = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
111
+ if (uploadRes.ok) {
112
+ const d = await uploadRes.json();
113
+ if (d.doc?.id) uploadedIds.push(d.doc.id);
114
+ }
115
+ }
116
+ const messageData = {
117
+ ticket: id,
118
+ body: extMsgBody.trim(),
119
+ authorType: extMsgAuthor,
120
+ isInternal: false,
121
+ skipNotification: true
122
+ };
123
+ if (extMsgDate) {
124
+ messageData.createdAt = new Date(extMsgDate).toISOString();
125
+ }
126
+ if (uploadedIds.length > 0) {
127
+ messageData.attachments = uploadedIds.map((mid) => ({ file: mid }));
128
+ }
129
+ const res = await fetch("/api/ticket-messages", {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/json" },
132
+ credentials: "include",
133
+ body: JSON.stringify(messageData)
134
+ });
135
+ if (res.ok) {
136
+ setExtMsgBody("");
137
+ setExtMsgDate("");
138
+ setExtMsgFiles([]);
139
+ setShowExtMsg(false);
140
+ fetchAll();
141
+ }
142
+ } catch {
143
+ } finally {
144
+ setSendingExtMsg(false);
145
+ }
146
+ };
147
+ const handleSnooze = async (days, customDate) => {
148
+ if (!id) return;
149
+ setSnoozeSaving(true);
150
+ try {
151
+ let newSnooze = null;
152
+ if (days !== null) {
153
+ const d = /* @__PURE__ */ new Date();
154
+ d.setDate(d.getDate() + days);
155
+ newSnooze = d.toISOString();
156
+ } else if (customDate) {
157
+ newSnooze = new Date(customDate).toISOString();
158
+ }
159
+ await fetch(`/api/tickets/${id}`, {
160
+ method: "PATCH",
161
+ headers: { "Content-Type": "application/json" },
162
+ credentials: "include",
163
+ body: JSON.stringify({ snoozeUntil: newSnooze })
164
+ });
165
+ setSnoozeUntil(newSnooze);
166
+ setShowSnooze(false);
167
+ fetchAll();
168
+ } catch {
169
+ } finally {
170
+ setSnoozeSaving(false);
171
+ }
172
+ };
173
+ const handleNextTicket = async () => {
174
+ try {
175
+ const res = await fetch("/api/tickets?where[status][equals]=open&sort=updatedAt&limit=1&depth=0", { credentials: "include" });
176
+ if (res.ok) {
177
+ const d = await res.json();
178
+ if (d.docs?.length > 0 && String(d.docs[0].id) !== String(id)) {
179
+ setNextTicketId(d.docs[0].id);
180
+ setNextTicketInfo(`${d.docs[0].ticketNumber} \u2014 ${d.docs[0].subject}`);
181
+ } else {
182
+ setNextTicketId(null);
183
+ setNextTicketInfo("Aucun autre ticket ouvert");
184
+ }
185
+ setShowNextTicket(true);
186
+ }
187
+ } catch {
188
+ }
189
+ };
190
+ return {
191
+ statusUpdating,
192
+ handleStatusChange,
193
+ showMerge,
194
+ setShowMerge,
195
+ mergeTarget,
196
+ setMergeTarget,
197
+ mergeTargetInfo,
198
+ setMergeTargetInfo,
199
+ mergeError,
200
+ setMergeError,
201
+ merging,
202
+ handleMergeLookup,
203
+ handleMerge,
204
+ showExtMsg,
205
+ setShowExtMsg,
206
+ extMsgBody,
207
+ setExtMsgBody,
208
+ extMsgAuthor,
209
+ setExtMsgAuthor,
210
+ extMsgDate,
211
+ setExtMsgDate,
212
+ extMsgFiles,
213
+ setExtMsgFiles,
214
+ sendingExtMsg,
215
+ handleExtFileChange,
216
+ handleSendExtMsg,
217
+ showSnooze,
218
+ setShowSnooze,
219
+ snoozeUntil,
220
+ setSnoozeUntil,
221
+ snoozeSaving,
222
+ handleSnooze,
223
+ showNextTicket,
224
+ setShowNextTicket,
225
+ nextTicketId,
226
+ nextTicketInfo,
227
+ handleNextTicket
228
+ };
229
+ }
230
+
231
+ exports.useTicketActions = useTicketActions;